Skip to content

Commit efed066

Browse files
author
Daniel Lavie
authored
[USM] Add check-maps subcommand for eBPF map leak detection (#43103)
### What does this PR do? This PR adds a new `usm check-maps` subcommand to system-probe that detects eBPF map leaks in USM TLS monitoring. It also refactors PID-keyed TLS map name management to use a single source of truth with reflection-based automatic discovery. ### Motivation USM's TLS monitoring uses several eBPF maps keyed by pid_tgid (process ID + thread ID). When processes terminate unexpectedly or the cleanup mechanisms fail, entries can leak in these maps. These leaks: - Consume kernel memory - Degrade performance as maps grow - Are difficult to detect and diagnose in production The solution involves a a diagnostic tool that enumerates all PID-keyed TLS maps, validates each entry against running processes, and reports detailed leak statistics. ### Describe how you validated your changes ### Additional Notes This approach can be extended to other map types beyond PID-keyed maps. For example, maps keyed by connection tuples could use a similar pattern with validation logic that checks if the connection still exists in the system. Co-authored-by: daniel.lavie <daniel.lavie@datadoghq.com>
1 parent cc7b409 commit efed066

10 files changed

Lines changed: 665 additions & 20 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2025-present Datadog, Inc.
5+
6+
//go:build linux_bpf
7+
8+
package usm
9+
10+
import (
11+
"fmt"
12+
13+
"github.com/spf13/cobra"
14+
15+
"github.com/DataDog/datadog-agent/cmd/system-probe/command"
16+
"github.com/DataDog/datadog-agent/pkg/network/usm/maps"
17+
)
18+
19+
func makeCheckMapsCommand(_ *command.GlobalParams) *cobra.Command {
20+
return &cobra.Command{
21+
Use: "check-maps",
22+
Short: "Check USM eBPF maps for leaked entries",
23+
Long: `Check USM eBPF maps for leaked entries by validating map keys against system state.
24+
25+
For PID-keyed maps (TLS/SSL argument storage), this command:
26+
- Extracts PIDs from map keys
27+
- Checks if processes still exist in /proc
28+
- Reports entries where the process no longer exists (leaked entries)
29+
30+
This is useful for diagnosing memory leaks in customer environments where
31+
eBPF map entries are not being properly cleaned up.`,
32+
RunE: func(_ *cobra.Command, _ []string) error {
33+
return runCheckMaps()
34+
},
35+
}
36+
}
37+
38+
func runCheckMaps() error {
39+
report, err := maps.CheckPIDKeyedMaps()
40+
if err != nil {
41+
return fmt.Errorf("failed to check maps: %w", err)
42+
}
43+
44+
if report.TotalMapsChecked == 0 {
45+
fmt.Println("No USM eBPF maps found. Is system-probe running with USM enabled?")
46+
return nil
47+
}
48+
49+
// Print the report
50+
fmt.Print(report.String())
51+
52+
return nil
53+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2025-present Datadog, Inc.
5+
6+
//go:build !linux_bpf
7+
8+
package usm
9+
10+
import (
11+
"github.com/spf13/cobra"
12+
13+
"github.com/DataDog/datadog-agent/cmd/system-probe/command"
14+
)
15+
16+
// makeCheckMapsCommand returns nil on platforms without eBPF support
17+
func makeCheckMapsCommand(_ *command.GlobalParams) *cobra.Command {
18+
return nil
19+
}

cmd/system-probe/subcommands/usm/command.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,10 @@ func Commands(globalParams *command.GlobalParams) []*cobra.Command {
2727
usmCmd.AddCommand(sysinfoCmd)
2828
}
2929

30+
// Add check-maps command if available on this platform
31+
if checkMapsCmd := makeCheckMapsCommand(globalParams); checkMapsCmd != nil {
32+
usmCmd.AddCommand(checkMapsCmd)
33+
}
34+
3035
return []*cobra.Command{usmCmd}
3136
}

pkg/network/usm/ebpf_ssl.go

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ package usm
1010
import (
1111
"fmt"
1212
"io"
13+
"reflect"
1314
"regexp"
1415
"time"
1516
"unsafe"
@@ -81,6 +82,46 @@ const (
8182
fdBySSLBioMap = "fd_by_ssl_bio"
8283
)
8384

85+
// pidKeyedTLSMaps is the single source of truth for all TLS eBPF maps that use pid_tgid as keys.
86+
//
87+
// IMPORTANT: Map names must be unique within their first 15 characters due to kernel truncation
88+
// (BPF_OBJ_NAME_LEN - 1). The leak detection system searches maps by truncated names, so names
89+
// like "hash_map_name_10" and "hash_map_name_11" would collide as both truncate to "hash_map_name_1".
90+
//
91+
// When adding a new PID-keyed map:
92+
// 1. Add a new field to this struct with the map name as the value
93+
// 2. Ensure the name is unique within the first 15 characters
94+
// 3. Add the corresponding map cleaner field to sslProgram struct
95+
// 4. Initialize it in initAllMapCleaners() with uint64 key type
96+
// The GetPIDKeyedTLSMapNames() function will automatically include it via reflection.
97+
var pidKeyedTLSMaps = struct {
98+
SSLReadArgs string
99+
SSLReadExArgs string
100+
SSLWriteArgs string
101+
SSLWriteExArgs string
102+
BioNewSocketArgs string
103+
SSLCtxByPIDTGID string
104+
}{
105+
SSLReadArgs: "ssl_read_args",
106+
SSLReadExArgs: "ssl_read_ex_args",
107+
SSLWriteArgs: "ssl_write_args",
108+
SSLWriteExArgs: "ssl_write_ex_args",
109+
BioNewSocketArgs: "bio_new_socket_args",
110+
SSLCtxByPIDTGID: "ssl_ctx_by_pid_tgid",
111+
}
112+
113+
// GetPIDKeyedTLSMapNames returns the names of all TLS eBPF maps that use pid_tgid as keys.
114+
// It uses reflection to extract all field values from pidKeyedTLSMaps struct.
115+
// This ensures the list is automatically kept in sync with the struct definition.
116+
func GetPIDKeyedTLSMapNames() []string {
117+
v := reflect.ValueOf(pidKeyedTLSMaps)
118+
names := make([]string, v.NumField())
119+
for i := 0; i < v.NumField(); i++ {
120+
names[i] = v.Field(i).String()
121+
}
122+
return names
123+
}
124+
84125
var openSSLProbes = []manager.ProbesSelector{
85126
&manager.BestEffort{
86127
Selectors: []manager.ProbesSelector{
@@ -254,25 +295,25 @@ var sharedLibrariesMaps = []*manager.Map{
254295
Name: sslCtxByTupleMap,
255296
},
256297
{
257-
Name: sslReadArgsMap,
298+
Name: pidKeyedTLSMaps.SSLReadArgs,
258299
},
259300
{
260-
Name: sslReadExArgsMap,
301+
Name: pidKeyedTLSMaps.SSLReadExArgs,
261302
},
262303
{
263-
Name: sslWriteArgsMap,
304+
Name: pidKeyedTLSMaps.SSLWriteArgs,
264305
},
265306
{
266-
Name: sslWriteExArgsMap,
307+
Name: pidKeyedTLSMaps.SSLWriteExArgs,
267308
},
268309
{
269-
Name: bioNewSocketArgsMap,
310+
Name: pidKeyedTLSMaps.BioNewSocketArgs,
270311
},
271312
{
272313
Name: fdBySSLBioMap,
273314
},
274315
{
275-
Name: sslCtxByPIDTGIDMap,
316+
Name: pidKeyedTLSMaps.SSLCtxByPIDTGID,
276317
},
277318
}
278319

@@ -538,7 +579,7 @@ func sharedLibrariesConfigureOptions(options *manager.Options, cfg *config.Confi
538579
MaxEntries: cfg.MaxTrackedConnections,
539580
EditorFlag: manager.EditMaxEntries,
540581
}
541-
options.MapSpecEditors[sslCtxByPIDTGIDMap] = manager.MapSpecEditor{
582+
options.MapSpecEditors[pidKeyedTLSMaps.SSLCtxByPIDTGID] = manager.MapSpecEditor{
542583
MaxEntries: cfg.MaxTrackedConnections,
543584
EditorFlag: manager.EditMaxEntries,
544585
}
@@ -570,32 +611,32 @@ func initMapCleaner[K, V interface{}](mgr *manager.Manager, mapName, attacherNam
570611
func (o *sslProgram) initAllMapCleaners() error {
571612
var err error
572613

573-
o.sslReadArgsMapCleaner, err = initMapCleaner[uint64, http.SslReadArgs](o.ebpfManager, sslReadArgsMap, UsmTLSAttacherName)
614+
o.sslReadArgsMapCleaner, err = initMapCleaner[uint64, http.SslReadArgs](o.ebpfManager, pidKeyedTLSMaps.SSLReadArgs, UsmTLSAttacherName)
574615
if err != nil {
575616
return err
576617
}
577618

578-
o.sslReadExArgsMapCleaner, err = initMapCleaner[uint64, http.SslReadExArgs](o.ebpfManager, sslReadExArgsMap, UsmTLSAttacherName)
619+
o.sslReadExArgsMapCleaner, err = initMapCleaner[uint64, http.SslReadExArgs](o.ebpfManager, pidKeyedTLSMaps.SSLReadExArgs, UsmTLSAttacherName)
579620
if err != nil {
580621
return err
581622
}
582623

583-
o.sslWriteArgsMapCleaner, err = initMapCleaner[uint64, http.SslWriteArgs](o.ebpfManager, sslWriteArgsMap, UsmTLSAttacherName)
624+
o.sslWriteArgsMapCleaner, err = initMapCleaner[uint64, http.SslWriteArgs](o.ebpfManager, pidKeyedTLSMaps.SSLWriteArgs, UsmTLSAttacherName)
584625
if err != nil {
585626
return err
586627
}
587628

588-
o.sslWriteExArgsMapCleaner, err = initMapCleaner[uint64, http.SslWriteExArgs](o.ebpfManager, sslWriteExArgsMap, UsmTLSAttacherName)
629+
o.sslWriteExArgsMapCleaner, err = initMapCleaner[uint64, http.SslWriteExArgs](o.ebpfManager, pidKeyedTLSMaps.SSLWriteExArgs, UsmTLSAttacherName)
589630
if err != nil {
590631
return err
591632
}
592633

593-
o.bioNewSocketArgsMapCleaner, err = initMapCleaner[uint64, uint32](o.ebpfManager, bioNewSocketArgsMap, UsmTLSAttacherName)
634+
o.bioNewSocketArgsMapCleaner, err = initMapCleaner[uint64, uint32](o.ebpfManager, pidKeyedTLSMaps.BioNewSocketArgs, UsmTLSAttacherName)
594635
if err != nil {
595636
return err
596637
}
597638

598-
o.sslCtxByPIDTGIDMapCleaner, err = initMapCleaner[uint64, uint64](o.ebpfManager, sslCtxByPIDTGIDMap, UsmTLSAttacherName)
639+
o.sslCtxByPIDTGIDMapCleaner, err = initMapCleaner[uint64, uint64](o.ebpfManager, pidKeyedTLSMaps.SSLCtxByPIDTGID, UsmTLSAttacherName)
599640
if err != nil {
600641
return err
601642
}
@@ -651,7 +692,7 @@ func (o *sslProgram) DumpMaps(w io.Writer, mapName string, currentMap *ebpf.Map)
651692
spew.Fdump(w, key, value)
652693
}
653694

654-
case sslReadArgsMap: // maps/ssl_read_args (BPF_MAP_TYPE_HASH), key C.__u64, value C.ssl_read_args_t
695+
case pidKeyedTLSMaps.SSLReadArgs: // maps/ssl_read_args (BPF_MAP_TYPE_HASH), key C.__u64, value C.ssl_read_args_t
655696
io.WriteString(w, "Map: '"+mapName+"', key: 'C.__u64', value: 'C.ssl_read_args_t'\n")
656697
iter := currentMap.Iterate()
657698
var key uint64
@@ -660,7 +701,7 @@ func (o *sslProgram) DumpMaps(w io.Writer, mapName string, currentMap *ebpf.Map)
660701
spew.Fdump(w, key, value)
661702
}
662703

663-
case sslReadExArgsMap: // maps/ssl_read_ex_args (BPF_MAP_TYPE_HASH), key C.__u64, value C.ssl_read_ex_args_t
704+
case pidKeyedTLSMaps.SSLReadExArgs: // maps/ssl_read_ex_args (BPF_MAP_TYPE_HASH), key C.__u64, value C.ssl_read_ex_args_t
664705
io.WriteString(w, "Map: '"+mapName+"', key: 'C.__u64', value: 'C.ssl_read_ex_args_t'\n")
665706
iter := currentMap.Iterate()
666707
var key uint64
@@ -669,7 +710,7 @@ func (o *sslProgram) DumpMaps(w io.Writer, mapName string, currentMap *ebpf.Map)
669710
spew.Fdump(w, key, value)
670711
}
671712

672-
case sslWriteArgsMap: // maps/ssl_write_args (BPF_MAP_TYPE_HASH), key C.__u64, value C.ssl_write_args_t
713+
case pidKeyedTLSMaps.SSLWriteArgs: // maps/ssl_write_args (BPF_MAP_TYPE_HASH), key C.__u64, value C.ssl_write_args_t
673714
io.WriteString(w, "Map: '"+mapName+"', key: 'C.__u64', value: 'C.ssl_write_args_t'\n")
674715
iter := currentMap.Iterate()
675716
var key uint64
@@ -678,7 +719,7 @@ func (o *sslProgram) DumpMaps(w io.Writer, mapName string, currentMap *ebpf.Map)
678719
spew.Fdump(w, key, value)
679720
}
680721

681-
case sslWriteExArgsMap: // maps/ssl_write_ex_args_t (BPF_MAP_TYPE_HASH), key C.__u64, value C.ssl_write_args_t
722+
case pidKeyedTLSMaps.SSLWriteExArgs: // maps/ssl_write_ex_args_t (BPF_MAP_TYPE_HASH), key C.__u64, value C.ssl_write_args_t
682723
io.WriteString(w, "Map: '"+mapName+"', key: 'C.__u64', value: 'C.ssl_write_ex_args_t'\n")
683724
iter := currentMap.Iterate()
684725
var key uint64
@@ -687,7 +728,7 @@ func (o *sslProgram) DumpMaps(w io.Writer, mapName string, currentMap *ebpf.Map)
687728
spew.Fdump(w, key, value)
688729
}
689730

690-
case bioNewSocketArgsMap: // maps/bio_new_socket_args (BPF_MAP_TYPE_HASH), key C.__u64, value C.__u32
731+
case pidKeyedTLSMaps.BioNewSocketArgs: // maps/bio_new_socket_args (BPF_MAP_TYPE_HASH), key C.__u64, value C.__u32
691732
io.WriteString(w, "Map: '"+mapName+"', key: 'C.__u64', value: 'C.__u32'\n")
692733
iter := currentMap.Iterate()
693734
var key uint64
@@ -705,7 +746,7 @@ func (o *sslProgram) DumpMaps(w io.Writer, mapName string, currentMap *ebpf.Map)
705746
spew.Fdump(w, key, value)
706747
}
707748

708-
case sslCtxByPIDTGIDMap: // maps/ssl_ctx_by_pid_tgid (BPF_MAP_TYPE_HASH), key C.__u64, value uintptr // C.void *
749+
case pidKeyedTLSMaps.SSLCtxByPIDTGID: // maps/ssl_ctx_by_pid_tgid (BPF_MAP_TYPE_HASH), key C.__u64, value uintptr // C.void *
709750
io.WriteString(w, "Map: '"+mapName+"', key: 'C.__u64', value: 'uintptr // C.void *'\n")
710751
iter := currentMap.Iterate()
711752
var key uint64
@@ -797,7 +838,7 @@ func (o *sslProgram) cleanupDeadPids(alivePIDs map[uint32]struct{}) {
797838
})
798839

799840
if err := o.deleteDeadPidsInSSLCtxMap(alivePIDs); err != nil {
800-
log.Debugf("SSL map %q cleanup error: %v", sslCtxByPIDTGIDMap, err)
841+
log.Debugf("SSL map %q cleanup error: %v", pidKeyedTLSMaps.SSLCtxByPIDTGID, err)
801842
}
802843
}
803844

pkg/network/usm/ebpf_ssl_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,3 +311,47 @@ func TestSSLMapsCleanup(t *testing.T) {
311311
t.FailNow()
312312
}
313313
}
314+
315+
// TestPIDKeyedMapNameUniqueness verifies that all PID-keyed TLS map names are unique
316+
// within their first 15 characters to prevent collisions from kernel truncation.
317+
//
318+
// eBPF map names are limited to 15 characters by the kernel (BPF_OBJ_NAME_LEN - 1).
319+
// The leak detection system searches maps by truncated names, so names like
320+
// "hash_map_name_10" and "hash_map_name_11" would collide as both truncate to
321+
// "hash_map_name_1".
322+
//
323+
// This test ensures we catch such collisions at compile/test time rather than
324+
// discovering them in production.
325+
func TestPIDKeyedMapNameUniqueness(t *testing.T) {
326+
names := GetPIDKeyedTLSMapNames()
327+
require.NotEmpty(t, names, "No PID-keyed map names found")
328+
329+
truncated := make(map[string]string)
330+
for _, name := range names {
331+
truncName := name
332+
if len(name) > 15 {
333+
truncName = name[:15]
334+
}
335+
336+
if existing, found := truncated[truncName]; found {
337+
t.Errorf("Map name collision detected:\n"+
338+
" Map 1: %q\n"+
339+
" Map 2: %q\n"+
340+
" Both truncate to: %q\n"+
341+
"Map names must be unique within their first 15 characters due to kernel limitation (BPF_OBJ_NAME_LEN - 1).",
342+
existing, name, truncName)
343+
}
344+
truncated[truncName] = name
345+
}
346+
347+
// Log all truncated names for reference
348+
t.Logf("Current PID-keyed TLS map names and their truncated forms:")
349+
for _, name := range names {
350+
if len(name) > 15 {
351+
truncName := name[:15]
352+
t.Logf(" %q -> %q (truncated)", name, truncName)
353+
} else {
354+
t.Logf(" %q (no truncation)", name)
355+
}
356+
}
357+
}

0 commit comments

Comments
 (0)