ehmaps

package
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 15, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

Documentation

Overview

Package ehmaps populates the BPF-side CFI / classification / pid-mappings maps from unwind/ehcompile output. Handles population plus lifecycle management via the refcounting layer in this package.

Build-IDs map to 64-bit table_ids via FNV-1a (non-cryptographic; collision resistance is "practically nonexistent" at the scale we care about — a single agent tracking at most a few thousand unique binaries).

Index

Constants

View Source
const BPF_F_INNER_MAP = 0x1000

BPF_F_INNER_MAP enables dynamic sizing of HASH_OF_MAPS inner maps. Defined in linux/bpf.h; redeclared here since cilium/ebpf doesn't re-export it at the package level. Requires kernel 5.10+; our 6.0+ floor covers this trivially.

View Source
const CFIEntryByteSize = 32

CFIEntryByteSize matches bpf/unwind_common.h `struct cfi_entry` (32 bytes after u64 alignment padding; the active data fills offsets 0..25 and the remaining 6 bytes are tail padding the BPF struct expects).

View Source
const ClassificationByteSize = 16

ClassificationByteSize matches bpf/unwind_common.h `struct classification` (16 bytes).

View Source
const MaxPIDMappings = 256

MaxPIDMappings mirrors bpf/unwind_common.h's MAX_PID_MAPPINGS. Keep in lockstep. The BPF inner map must be created with this max_entries value for the walker's bpf_loop bound to hold.

View Source
const PIDMappingByteSize = 32

PIDMappingByteSize matches bpf/unwind_common.h `struct pid_mapping` (32 bytes).

Variables

This section is empty.

Functions

func AttachAllMappings

func AttachAllMappings(t *PIDTracker, pid uint32) (int, error)

AttachAllMappings walks /proc/<pid>/maps, finds every file-backed executable mapping, and calls tracker.Attach once per unique binary path. Returns the count of distinct binaries attached.

Call this once at agent startup to cover the main binary plus every shared library present at that moment. Subsequent mmaps (dlopen, runtime-loaded plugins) are handled by MmapWatcher driving PIDTracker.Run.

Attach failures for individual binaries are logged at Debug and skipped rather than fatal — a process may have exotic mappings (ehcompile-rejectable formats, ELFs without .eh_frame) that we shouldn't fail the whole attach on. The first failure is returned only if NO binary was successfully attached; otherwise we report whatever we managed.

func AttachAllProcesses

func AttachAllProcesses(t *PIDTracker) (pids, tables int, err error)

AttachAllProcesses walks /proc/* and calls AttachAllMappings for every numeric PID directory that still has a live /proc/<pid>/maps. Returns (pidCount, distinctBinaryCount, err). The distinct-binary count comes from observing the TableStore's CFIRules outer map before and after the scan; the walker tolerates individual PID failures (process vanished between listdir and open).

Intended for system-wide startup. After this returns, follow-up tracking relies on per-CPU MmapWatchers + FORK events.

func MarshalCFIEntry

func MarshalCFIEntry(e ehcompile.CFIEntry) []byte

MarshalCFIEntry writes one ehcompile.CFIEntry in the exact byte order the BPF walker expects. Keep in lockstep with bpf/unwind_common.h.

func MarshalClassification

func MarshalClassification(c ehcompile.Classification) []byte

MarshalClassification writes one ehcompile.Classification in BPF layout.

func MarshalPIDMapping

func MarshalPIDMapping(m PIDMapping) []byte

MarshalPIDMapping writes one PIDMapping in BPF layout.

func PopulateCFI

func PopulateCFI(args PopulateCFIArgs) error

PopulateCFI creates a right-sized inner ARRAY, fills it with Entries, and installs it into OuterMap keyed by TableID. Also writes the valid length into LengthMap. On success the inner map stays owned by the kernel (the outer map holds a reference); our userspace handle is closed immediately.

func PopulateClassification

func PopulateClassification(args PopulateClassificationArgs) error

func PopulatePIDMappings

func PopulatePIDMappings(args PopulatePIDMappingsArgs) error

func ReadBuildID

func ReadBuildID(path string) ([]byte, error)

ReadBuildID reads the GNU build-id from an ELF's .note.gnu.build-id. Returns the raw bytes (typically 20 for sha1), or an error if absent.

func ScanAndEnroll

func ScanAndEnroll(t *PIDTracker) (pids, tables int, err error)

ScanAndEnroll walks /proc/* and populates pid_mappings entries for every executable mapping of every PID, WITHOUT compiling CFI. Returns (pidCount, distinctBinaryCount, err).

Used by --unwind auto -a (Option A2 lazy mode). The deferred compile happens via AttachCompileOnly when the BPF walker emits a miss event for a sampled (pid, table_id) pair.

Build-id reads are cached so each unique binary's build-id is parsed exactly once across all PIDs. ~30,000× cheaper than AttachAllProcesses on typical desktops (100s of µs vs tens of seconds).

func ScanAndEnrollFromTree

func ScanAndEnrollFromTree(procRoot string, t *PIDTracker) (pids, tables int, err error)

ScanAndEnrollFromTree is the testable variant of ScanAndEnroll: takes the proc-tree root as a parameter so unit tests can run against a synthetic tree built in t.TempDir().

func TableIDForBuildID

func TableIDForBuildID(buildID []byte) uint64

TableIDForBuildID hashes a build-id (raw bytes, typically 20) to the u64 key used across cfi_rules, cfi_classification, and pid_mapping.table_id. Empty input returns the FNV-1a offset basis, which is fine — the caller should validate that a missing build-id doesn't collide with a real one.

Types

type MmapEventKind

type MmapEventKind int

MmapEventKind distinguishes event records emitted by MmapWatcher.

const (
	MmapEvent MmapEventKind = iota + 1
	ExitEvent
	ForkEvent
)

type MmapEventRecord

type MmapEventRecord struct {
	Kind     MmapEventKind
	PID      uint32 // TGID
	TID      uint32 // thread ID (equals PID when the group leader itself)
	Addr     uint64
	Len      uint64
	PgOff    uint64
	Prot     uint32
	Filename string
}

MmapEventRecord is a parsed PERF_RECORD_MMAP2 or PERF_RECORD_EXIT. Fields are populated based on Kind — Exit uses PID + TID (to distinguish group-leader exit from per-thread exit); Mmap uses all the mapping fields.

type MmapWatcher

type MmapWatcher struct {
	// contains filtered or unexported fields
}

MmapWatcher owns one perf_event fd + ring buffer and delivers parsed MMAP2 and EXIT records via Events(). Construct with NewMmapWatcher; always Close() when done. Close is idempotent and synchronous — it waits for the reader goroutine to finish before unmapping the ring.

func NewMmapWatcher

func NewMmapWatcher(pid uint32) (*MmapWatcher, error)

NewMmapWatcher attaches a per-TID watcher to pid. Requires CAP_PERFMON (or CAP_BPF / root). The watcher delivers PERF_RECORD_MMAP2, PERF_RECORD_EXIT, and PERF_RECORD_FORK records to Events(). Note that only mmaps issued by the exact TID we opened against generate records — see newMmapWatcher for background on the inherit restriction.

func NewSystemWideMmapWatcher

func NewSystemWideMmapWatcher(cpu int) (*MmapWatcher, error)

NewSystemWideMmapWatcher attaches to CPU `cpu` with pid=-1 — sees MMAP2/EXIT/FORK from any task that runs on that CPU. Combine N instances (one per online CPU) via MultiCPUMmapWatcher for whole-system coverage.

func (*MmapWatcher) Close

func (w *MmapWatcher) Close() error

Close stops the reader goroutine and releases the fd + mapping. Idempotent. Waits for the reader goroutine to return before unmapping so an in-flight drain() can't fault on unmapped memory.

func (*MmapWatcher) Events

func (w *MmapWatcher) Events() <-chan MmapEventRecord

Events returns the channel of parsed records. Closed when the watcher shuts down (via Close or unrecoverable ring error).

type MultiCPUMmapWatcher

type MultiCPUMmapWatcher struct {
	// contains filtered or unexported fields
}

MultiCPUMmapWatcher owns one MmapWatcher per online CPU and fans their events into one channel. Used by dwarfagent in system-wide mode: per-PID watchers can't do -a because they only see mmaps from the specific TID they attach to, and inherit=1 is forbidden on fds we need to mmap (EINVAL from perf_mmap). Per-CPU (pid=-1, cpu=N) is the standard workaround.

func NewMultiCPUMmapWatcher

func NewMultiCPUMmapWatcher(cpus []int) (*MultiCPUMmapWatcher, error)

NewMultiCPUMmapWatcher opens one SystemWideMmapWatcher per element of cpus. On any error, every watcher opened so far is closed and (nil, err) is returned.

func (*MultiCPUMmapWatcher) Close

func (m *MultiCPUMmapWatcher) Close() error

Close stops fan-in goroutines, closes every child watcher, and releases the merged channel. Idempotent.

func (*MultiCPUMmapWatcher) Events

func (m *MultiCPUMmapWatcher) Events() <-chan MmapEventRecord

Events returns the merged channel. Matches the MmapWatcher.Events() signature so PIDTracker.Run can consume either shape.

type PIDMapping

type PIDMapping struct {
	VMAStart uint64
	VMAEnd   uint64
	LoadBias uint64
	TableID  uint64
}

PIDMapping is the Go-side twin of bpf/unwind_common.h `struct pid_mapping`. Describes one contiguous load of a binary into a process's address space.

func LoadProcessMappings

func LoadProcessMappings(pid int, binPath, openPath string, tableID uint64) ([]PIDMapping, error)

LoadProcessMappings reads /proc/<pid>/maps and returns one PIDMapping per executable-mapped range of binPath. The load bias is computed from the ELF's executable PT_LOAD — "vma_start - file_offset" is wrong for PIE binaries where PT_LOAD vaddr differs from file offset (Rust's release output has a 0x1000 hole).

binPath is the symbolic path (used for matching against /proc/<pid>/maps entries — only its basename is compared). openPath is the actual file to elf.Open; it may equal binPath in the common case or differ when the symbolic path is unreachable (deleted-but-mapped binary, sidecar / mount- namespace cases) and the caller routed I/O through /proc/<pid>/map_files.

type PIDTracker

type PIDTracker struct {
	// contains filtered or unexported fields
}

PIDTracker holds per-PID state for the hybrid unwinder. Each Attach populates pid_mappings for that PID and takes a TableStore reference for every unique binary in the process's address space. Detach reverses both.

Attach is called once per binary in the target's address space. Subsequent calls for the same PID with a different binPath append to the pid_mappings array. The integration test exercises the full flow via MmapWatcher events driving Attach automatically.

func NewPIDTracker

func NewPIDTracker(store *TableStore, pidMappings, pidMapLengths *ebpf.Map) *PIDTracker

NewPIDTracker wires a tracker around an already-loaded set of BPF maps. Caller owns the maps; the tracker does not close them.

func (*PIDTracker) Attach

func (t *PIDTracker) Attach(pid uint32, binPath, openPath string) error

Attach walks /proc/<pid>/maps for binPath, acquires CFI via the store, and installs a pid_mappings row. Safe to call multiple times with different binPaths for the same PID — mappings accumulate.

binPath is the cache key (the symbolic path; stable across PIDs). openPath is the file actually opened to read the build-id and compile CFI; pass "" to use binPath. openPath differs only when the symbolic path is unreachable from the agent's namespace (deleted-but-mapped binary, sidecar / mount-namespace cases) and the caller routed I/O through /proc/<pid>/map_files. The pid_mappings table itself does not need either path — it only stores va ranges keyed by tableID.

func (*PIDTracker) AttachCompileOnly

func (t *PIDTracker) AttachCompileOnly(pid uint32, binPath, openPath string) error

AttachCompileOnly compiles CFI for binPath and registers the tableID for refcount-tracked release. Assumes pid_mappings already has an entry for this binary (set by a prior EnrollWithoutCompile). Used by the lazy CFI miss drainer.

binPath is the cache key (symbolic path). openPath is the file actually opened for ehcompile; pass "" to use binPath. See Attach for the rationale.

Skips LoadProcessMappings + PopulatePIDMappings — the enrolled state already covers them. Calling this for a binary that was NOT enrolled would leave pid_mappings empty for it; the walker would still hit MAPPING_NOT_FOUND and fall through to FP-only.

func (*PIDTracker) Detach

func (t *PIDTracker) Detach(pid uint32) error

Detach removes the PID from the pid_mappings map and releases all binaries it held. Safe to call for an unknown PID (no-op).

func (*PIDTracker) EnrollWithoutCompile

func (t *PIDTracker) EnrollWithoutCompile(pid uint32, binPath, openPath string, buildIDCache map[string][]byte) error

EnrollWithoutCompile populates pid_mappings for binPath under pid WITHOUT compiling CFI. Used by the lazy mode (Option A2) to give the walker enough mapping info to classify per-frame, deferring the expensive ehcompile.Compile call to the first sample miss.

binPath is the cache key (symbolic path; also the key used in buildIDCache so two PIDs mapping the same binary share one build-id read). openPath is the file actually opened to read the build-id and match against /proc/<pid>/maps; pass "" to use binPath. They differ only when the symbolic path is unreachable from the agent's namespace.

buildIDCache is shared across calls so each unique binary's build-id is read exactly once across all PIDs. Caller owns the cache.

Does NOT increment the TableStore refcount — compile-time refcounting is handled by AttachCompileOnly when the drainer compiles on demand.

func (*PIDTracker) Run

func (t *PIDTracker) Run(ctx context.Context, w mmapEventSource, observers ...func(MmapEventRecord))

Run blocks consuming events from the watcher until ctx is canceled or the watcher's event channel closes. Call from a goroutine. On MmapEvent with an executable filename, auto-attaches the PID if we haven't seen that (pid, path) already. On ExitEvent (group-leader only), detaches.

Observers (if any) run BEFORE the tracker's own dispatch for each event — they see every event including those the tracker itself would filter out. Used by dwarfagent.session to keep a procmap Resolver's cache in sync with MMAP2/EXIT events.

func (*PIDTracker) SetOnNewExec

func (t *PIDTracker) SetOnNewExec(fn func(pid uint32))

SetOnNewExec registers a hook that Run calls when it observes a new group-leader fork (PERF_RECORD_FORK with pid == tid), which the kernel emits for every new process created via fork/exec. Pass nil to clear the hook. Must be called before Run to avoid races.

The hook runs synchronously on Run's goroutine. When no hook is registered (the default), Run performs a single nil check and skips dispatch entirely — zero producer overhead when --inject-python is off.

type PopulateCFIArgs

type PopulateCFIArgs struct {
	TableID   uint64
	Entries   []ehcompile.CFIEntry
	OuterMap  *ebpf.Map // cfi_rules (HASH_OF_MAPS)
	LengthMap *ebpf.Map // cfi_lengths (HASH)
}

PopulateCFIArgs bundles what the caller already has in memory — an already- compiled set of rules plus the outer and length maps from the loaded BPF program.

type PopulateClassificationArgs

type PopulateClassificationArgs struct {
	TableID   uint64
	Entries   []ehcompile.Classification
	OuterMap  *ebpf.Map // cfi_classification
	LengthMap *ebpf.Map // cfi_classification_lengths
}

PopulateClassificationArgs mirrors PopulateCFIArgs but for classification.

type PopulatePIDMappingsArgs

type PopulatePIDMappingsArgs struct {
	PID       uint32
	Mappings  []PIDMapping
	OuterMap  *ebpf.Map // pid_mappings
	LengthMap *ebpf.Map // pid_mapping_lengths
}

PopulatePIDMappingsArgs installs a list of mappings for one PID. The inner map is always sized at MaxPIDMappings so the BPF walker's bpf_loop bound is a compile-time constant.

type RefcountTable

type RefcountTable struct {
	// contains filtered or unexported fields
}

RefcountTable tracks which (tableID, PID) pairs currently reference a CFI table. A table stays in the BPF maps until the last PID releases it. Zero-value is not usable; construct via NewRefcountTable.

Operations are safe for concurrent use. Acquire and Release return the post-operation refcount so callers can decide whether to install or evict the actual BPF-side table.

func NewRefcountTable

func NewRefcountTable() *RefcountTable

NewRefcountTable creates an empty RefcountTable.

func (*RefcountTable) Acquire

func (r *RefcountTable) Acquire(tableID uint64, pid uint32) int

Acquire records that `pid` now references `tableID`. Idempotent — a repeat acquire for the same (tid, pid) does NOT double-count. Returns the resulting refcount (number of distinct PIDs holding this tableID).

func (*RefcountTable) Release

func (r *RefcountTable) Release(tableID uint64, pid uint32) int

Release records that `pid` no longer references `tableID`. Returns the resulting refcount. Releasing an untracked (tid, pid) is a no-op (returns 0).

type TableStore

type TableStore struct {
	CFIRules          *ebpf.Map
	CFILengths        *ebpf.Map
	CFIClassification *ebpf.Map
	CFIClassLengths   *ebpf.Map
	// contains filtered or unexported fields
}

TableStore owns the BPF-side cfi_* outer maps and composes refcount tracking with actual map population. Wraps Populate{CFI,Classification} with refcounting so callers don't hand-manage table lifetimes.

func NewTableStore

func NewTableStore(cfi, cfiLen, cls, clsLen *ebpf.Map) *TableStore

NewTableStore wires up a TableStore around already-loaded BPF maps (typically from the agent's perf_dwarf program load). The caller owns the maps; TableStore does not close them.

func (*TableStore) AcquireBinary

func (s *TableStore) AcquireBinary(binPath, openPath string, pid uint32) (tableID uint64, compiled bool, err error)

AcquireBinary ensures CFI for `binPath` is installed and references it on behalf of `pid`. Returns the tableID plus a boolean indicating whether a fresh compile happened (false means the refcount was simply incremented on an existing table).

binPath is the cache key (the symbolic path, stable across PIDs — two processes mapping the same /usr/lib/libc.so.6 share one compile result). openPath is the file actually opened for build-id + ehcompile; pass "" to use binPath. openPath differs from binPath only when the symbolic path is unreachable from the agent's namespace (deleted-but- mapped binary, sidecar / mount-namespace cases) and the caller routed I/O through /proc/<pid>/map_files.

func (*TableStore) ReleaseBinary

func (s *TableStore) ReleaseBinary(tableID uint64, pid uint32) error

ReleaseBinary drops `pid`'s reference to `tableID`. If the refcount hits zero, evicts the inner maps (best-effort — eviction errors are returned but the refcount is still decremented).

func (*TableStore) SetOnCompile

func (s *TableStore) SetOnCompile(fn func(path, buildID string, ehFrameBytes int, dur time.Duration))

SetOnCompile installs an observer callback that fires after each successful CFI compile in AcquireBinary. Pass nil to disable. Not safe to call concurrently with AcquireBinary; set once at construction time.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL