fix(profiler): reach the target through its own mount namespace - #129
mayankpande88 wants to merge 1 commit into
Conversation
Files we stage for the target - and files the target writes for us - were resolved through the container runtime's overlay directory. That is only the same thing when nothing is mounted over the container's root. A pod with readOnlyRootFilesystem: true and an emptyDir at /tmp is the common counter-example, and there everything we put in the target's /tmp lands somewhere the process cannot see. Java flamegraphs failed exactly that way on such pods, with async-profiler reporting "Could not start attach mechanism: No such file or directory": both jattach's .attach_pid handshake file and libasyncProfiler.so, which the JVM dlopens, were written through the overlay. Resolve those paths via /proc/<pid>/root instead, which is the target's whole mount namespace, volumes included - identical to the old path when nothing is mounted there, correct when something is. Same change for jcmd (heap dumps and JFR recordings are written by the JVM) and for the Node heapsnapshot (written by the Node process into its working directory). Also hand the staged profiler to the user the target runs as: we stage as root, and the JVM has to read the library and write its output there.
There was a problem hiding this comment.
Code Review
This pull request refactors how the target container's filesystem is accessed by switching from the container runtime's overlay directory to the target's mount namespace via /proc/<pid>/root. It also introduces ownership adjustment (chown) of the staged profiler files to match the target process's UID/GID, allowing non-root JVMs to access them. The review feedback correctly identifies potential runtime panics in async_profiler.go and jcmd.go if the target PID list is empty, and suggests a safety check for an empty root PID in node_dummy.go.
Verified against the pod that could not be profiledRan against ECK Elasticsearch on the dev cluster — Before (published After (this branch), same pod, 60s CPU profile: with real stacks, not an empty template: No regression on the ordinary case: CI is green (11/11). The test tag has been removed and the dev cluster put back on |
Summary
Files we stage for the target — and files the target writes for us — were resolved through the container runtime's overlay directory (
util.ContainerFileSystem). That is only the same directory when nothing is mounted over the container's root. A pod withreadOnlyRootFilesystem: trueand an emptyDir at/tmpis the common counter-example, and there everything we put in the target's/tmplands somewhere the process cannot see.Java flamegraphs fail exactly that way on such pods, with
Could not start attach mechanism: No such file or directory: both jattach's.attach_pid<pid>handshake file andlibasyncProfiler.so, which the JVM dlopens, were written through the overlay.This resolves those paths via
/proc/<pid>/rootinstead — the target's whole mount namespace, volumes included. It is the same directory as before when nothing is mounted there, and the correct one when something is, so it replaces the old behaviour rather than branching on it. The same change applies to jcmd (the JVM writes heap dumps and JFR recordings itself) and to the Node heapsnapshot (written by the Node process into its working directory).It also hands the staged profiler to the user the target runs as: we stage as root, and the JVM has to read the library and write its output into that directory. Hardened images rarely run as root.
SetUpis reordered in the two JVM profilers — PIDs are now resolved first, because the target's mount namespace is only reachable through one of its PIDs. One consequence: with an explicit--pid, the container runtime is no longer consulted duringSetUpat all.ContainerFileSystemis left in place (it is still the honest answer to "where does the runtime keep this container") but now carries a comment saying what it is not, so the next profiler does not reach for it by reflex.Type of change
Test plan
Unit tests cover the new path helper, the credentials read, and the chown step (root target → no chown; non-root → handed over; unreadable pid → error, not a silent skip). The reordering changed three existing expectations, each updated to the new contract rather than loosened.
Verified against a real read-only-rootfs pod — ECK Elasticsearch on the dev cluster,
readOnlyRootFilesystem: true, an emptyDir mounted at/tmp, running as uid 1000 — where a Java flamegraph currently fails. Evidence to follow in a comment once the test image finishes rolling.Ahead of that, the mechanism was confirmed directly from a hostPID pod on the same node:
The overlay directory shows none of that.
Checklist
go build ./...andgo test ./...pass locally (TestAsyncProfiler_CleanUpandTestExecuteWhenErrorfail on macOS both before and after this change — pre-existing, verified againstmainin a clean worktree; both pass on Linux CI)Related issues
Follows #128, which fixed the image itself; this fixes the next failure it exposed.