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
- func AttachAllMappings(t *PIDTracker, pid uint32) (int, error)
- func AttachAllProcesses(t *PIDTracker) (pids, tables int, err error)
- func MarshalCFIEntry(e ehcompile.CFIEntry) []byte
- func MarshalClassification(c ehcompile.Classification) []byte
- func MarshalPIDMapping(m PIDMapping) []byte
- func PopulateCFI(args PopulateCFIArgs) error
- func PopulateClassification(args PopulateClassificationArgs) error
- func PopulatePIDMappings(args PopulatePIDMappingsArgs) error
- func ReadBuildID(path string) ([]byte, error)
- func ScanAndEnroll(t *PIDTracker) (pids, tables int, err error)
- func ScanAndEnrollFromTree(procRoot string, t *PIDTracker) (pids, tables int, err error)
- func TableIDForBuildID(buildID []byte) uint64
- type MmapEventKind
- type MmapEventRecord
- type MmapWatcher
- type MultiCPUMmapWatcher
- type PIDMapping
- type PIDTracker
- func (t *PIDTracker) Attach(pid uint32, binPath, openPath string) error
- func (t *PIDTracker) AttachCompileOnly(pid uint32, binPath, openPath string) error
- func (t *PIDTracker) Detach(pid uint32) error
- func (t *PIDTracker) EnrollWithoutCompile(pid uint32, binPath, openPath string, buildIDCache map[string][]byte) error
- func (t *PIDTracker) Run(ctx context.Context, w mmapEventSource, observers ...func(MmapEventRecord))
- func (t *PIDTracker) SetOnNewExec(fn func(pid uint32))
- type PopulateCFIArgs
- type PopulateClassificationArgs
- type PopulatePIDMappingsArgs
- type RefcountTable
- type TableStore
Constants ¶
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.
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).
const ClassificationByteSize = 16
ClassificationByteSize matches bpf/unwind_common.h `struct classification` (16 bytes).
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.
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 ¶
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 ¶
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 ¶
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 ¶
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).
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.