Skip to content

symbolize: require CAP_CHECKPOINT_RESTORE instead of degrading around it - #41

Merged
dpsoft merged 1 commit into
mainfrom
refactor/require-checkpoint-restore
Aug 21, 2026
Merged

symbolize: require CAP_CHECKPOINT_RESTORE instead of degrading around it#41
dpsoft merged 1 commit into
mainfrom
refactor/require-checkpoint-restore

Conversation

@dpsoft

@dpsoft dpsoft commented Aug 21, 2026

Copy link
Copy Markdown
Owner

169 lines removed. symbolize/local.go goes 357 → 241.

What this deletes, and why it existed

blazesym resolves mappings through /proc/<pid>/map_files/, whose magic symlinks the kernel refuses without CAP_CHECKPOINT_RESTORE. When a test binary was setcap'd with only cap_bpf,cap_perfmon, symbolization silently resolved nothing — every user frame came back as a bare hex address while reporting zero failures.

The fix at the time added a fallback path: retry with no_map_files, latch that setting, log once via compare-and-swap, probe capabilities at startup, and detect permission errors by string-matching blazesym's message because the C API drops errno. Plus five LocalStats fields to manage it.

All of that existed to tolerate a capability configuration this project does not support. CAP_CHECKPOINT_RESTORE is already in the documented required set (CLAUDE.md, spec §11, which states it covers /proc/<pid>/map_files), and three other code paths already depend on it directly — unwind/procmap/parse.go, unwind/dwarfagent/miss_drainer.go, symbolize/debuginfod/buildid.go.

A test-harness constraint had been absorbed into the product.

Gone: the latch, the CAS log, the startup capability probe, the string match, the no_map_files retry, the test seam, and five stats fields. Kept: RawAddrBatches, which reports genuine per-process resolution failures — real signal.

Failing loudly instead

NewLocalSymbolizer now refuses with a typed error rather than degrading:

symbolize: cannot follow /proc/<pid>/map_files/ (open …: operation not permitted):
every user-space frame would resolve to a bare hex address.
Grant CAP_CHECKPOINT_RESTORE - sudo setcap … - or run as root

A hard error rather than a logged flag, because nothing in the repo read LocalStats — a log plus a flag would have been silent degradation with extra steps. Detection is an actual open() of a real map_files entry: the exact kernel gate, a typed errno, immune to user-namespace quirks and to Permitted-not-yet-Effective. Only a definite EPERM is treated as a verdict.

Fixing the cause, not just the symptom

Test provisioning is corrected so the same pressure does not return: the gate's capability check and every documented setcap line now require cap_bpf,cap_perfmon,cap_checkpoint_restore — the set the product actually needs. A run that would emit hex frames now skips with a reason rather than passing green with degraded output.

For the phase gate, a human runs:

sudo setcap cap_bpf,cap_perfmon,cap_checkpoint_restore+ep /home/diego/gpuprobe.test

Note on CAP_SYS_ADMIN

Not used, and not needed. It grants map_files too, but CAP_CHECKPOINT_RESTORE is narrower and already required. The one place CAP_SYS_ADMIN would apply — perf_uprobe instead of uprobe_multi — costs more code (one link per probe rather than one link with cookies) and buys only the removal of the Linux 6.6 floor. That is a deployment-reach decision, not a simplification.

Verification

go build, go vet, all listed packages, -race, golangci-lint 0 issues. Three pre-existing gofmt offenders (symbolize/hist.go, symbolize/debuginfod/stats.go, and a test helper) are untouched by this branch — a go1.26 alignment change already on main.

Also spotted and deliberately left alone: perfagent/agent.go:327 calls caps.SetFlag without a following caps.SetProc(), so that raise is a no-op. Harmless for +ep binaries, unrelated to this change.

⚠️ main is currently red on TestKernelStackResolution (no user-side function in profile), which predates this branch and touches none of this code — reruns in flight to confirm flakiness.

blazesym's process source reaches the file behind every mapping through
/proc/<pid>/map_files/, and the kernel's proc_map_files_get_link() refuses
to follow those magic symlinks without CAP_CHECKPOINT_RESTORE (or
CAP_SYS_ADMIN). To tolerate a test binary setcap'd with only
cap_bpf,cap_perfmon, symbolize/local.go had grown a permanent second
resolution path and the machinery to steer it: a process-lifetime latch, a
compare-and-swap so the log fired once, a startup CapEff probe, five
LocalStats fields, a test seam to inject the two failure kinds, and a string
match on blazesym's "permission denied" text because its C API drops the
errno.

None of that bought anything a supported deployment needs.
CAP_CHECKPOINT_RESTORE is already in the required set documented by README.md,
SECURITY.md and perfagent/agent.go, and unwind/procmap, unwind/dwarfagent and
symbolize/debuginfod already depend on it directly.

So: map_files is the single path, and NewLocalSymbolizer refuses rather than
degrades. It probes once by open()ing a real map_files entry for itself, which
exercises the exact kernel gate (not a CapEff bitmask, which is wrong about
non-initial user namespaces and about Permitted-not-Effective) and yields a
typed errno, which is what removes the dependency on blazesym's error text.
Only a definite EPERM is a verdict; an inconclusive probe never blocks
startup. The error names the missing capability and the setcap line.

A log plus a Stats flag was the alternative, and it is silent degradation with
extra steps: no caller in this repository reads LocalStats, and a startup log
is long gone by the time a sixty-second run writes a profile.pb.gz full of
0x7f addresses. A profile whose every user frame is a bare address is not
degraded, it is useless.

LocalStats.RawAddrBatches stays: a pid that exits before its /proc entry can
be read is a genuine per-process failure and real signal. That path still
returns hex frames with Reason == FailureMissingSymbols and err == nil, which
gpuprobe.Stats.StacksUnresolved consumes.

gpuprobe's gate now requires CAP_CHECKPOINT_RESTORE too, so a run that would
produce hex frames skips with a clear reason instead of passing green -
which is exactly what it did while 63 sampled stacks resolved to nothing.

CAP_SYS_ADMIN is untouched; the uprobe_multi attach path stays as it is.

symbolize/local.go 357 -> 241 lines; 414 deletions against 245 insertions
across the branch.
@dpsoft
dpsoft merged commit b1ad831 into main Aug 21, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant