Skip to content

Commit 3108659

Browse files
sarg3ntclaude
andcommitted
fix(agent): nsenter mode finds host binary via /proc/1/root
Distroless agent containers ship no util-linux, so the previous SpawnNsenter argv of {"nsenter", "--target", "1", ...} bailed at exec with "executable file not found in \$PATH" — observed live on mjolnir running ghcr.io/sarg3nt/gearbox/gearbox-agent:latest: level=ERROR msg="console: spawner failed" error="pty: start failed: exec: \"nsenter\": executable file not found in \$PATH" With `pid: host` the kernel exposes the host's root filesystem at /proc/1/root (PID 1's magic root symlink resolved by the host's mount namespace). The agent can call the host's nsenter binary via that path without bundling util-linux into the distroless image. resolveHostNsenter stat()s a small candidate list: /proc/1/root/usr/bin/nsenter (Debian/Ubuntu/modern RHEL) /proc/1/root/bin/nsenter (older trees) /proc/1/root/usr/sbin/nsenter /proc/1/root/sbin/nsenter /usr/bin/nsenter (in-container fallback for non-distroless agent flavors) nsenterUsable now requires the resolution to succeed so the capabilities envelope honestly reports `host_console: false` when no nsenter is reachable — the dashboard would rather hide the action than offer a shell that immediately fails. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 10f939e commit 3108659

1 file changed

Lines changed: 45 additions & 3 deletions

File tree

gearbox-agent/internal/api/console/pty/nsenter_linux.go

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,9 @@ func runningInContainer() bool {
8383

8484
// nsenterUsable reports whether nsenter into PID 1's namespaces would
8585
// work right now. Requires:
86-
// - the nsenter binary somewhere on PATH (checked by Spawner at exec
87-
// time; here we just verify the rest)
86+
// - the nsenter binary reachable from the container (probed against
87+
// resolveHostNsenter; distroless containers don't ship util-linux,
88+
// so we expect to find it on the host via /proc/1/root)
8889
// - /proc/1/ns/mnt readable (proves we have access to the host's mount
8990
// ns reference)
9091
// - that ns differs from our own (proves there's actually a host to
@@ -104,9 +105,46 @@ func nsenterUsable() bool {
104105
// exist because distroless).
105106
return false
106107
}
108+
if resolveHostNsenter() == "" {
109+
return false
110+
}
107111
return true
108112
}
109113

114+
// hostNsenterCandidates is the search list for the host's nsenter
115+
// binary as seen from inside the container. With pid:host the kernel
116+
// exposes the host's root filesystem at /proc/1/root (a magic symlink
117+
// resolved by the host PID 1's mount namespace), so absolute paths
118+
// under /proc/1/root/... reach the host's util-linux install.
119+
//
120+
// Order: most-common location first; /usr/bin covers Debian/Ubuntu/
121+
// modern RHEL; /bin covers older trees; /usr/sbin and /sbin cover
122+
// distros that consider nsenter a privileged util.
123+
var hostNsenterCandidates = []string{
124+
"/proc/1/root/usr/bin/nsenter",
125+
"/proc/1/root/bin/nsenter",
126+
"/proc/1/root/usr/sbin/nsenter",
127+
"/proc/1/root/sbin/nsenter",
128+
}
129+
130+
// resolveHostNsenter returns the first reachable nsenter binary path
131+
// from hostNsenterCandidates, or "" if none exist. Cached results are
132+
// not appropriate — operator upgrades on the host could move the
133+
// binary; the call is cheap (a few stat()s).
134+
func resolveHostNsenter() string {
135+
for _, p := range hostNsenterCandidates {
136+
if st, err := os.Stat(p); err == nil && !st.IsDir() {
137+
return p
138+
}
139+
}
140+
// Last-ditch: maybe the container itself ships nsenter (some
141+
// non-distroless agent flavors might). exec.LookPath uses $PATH.
142+
if _, err := os.Stat("/usr/bin/nsenter"); err == nil {
143+
return "/usr/bin/nsenter"
144+
}
145+
return ""
146+
}
147+
110148
// SpawnNsenter wraps SpawnUnix in an nsenter invocation. The argv
111149
// becomes: nsenter --target 1 --mount --uts --ipc --net --pid --
112150
// <user shell>. The host shell is whatever the operator configured
@@ -127,8 +165,12 @@ func SpawnNsenter(ctx context.Context, command []string, runAs string, cols, row
127165
if runAs != "" {
128166
return nil, fmt.Errorf("nsenter: run-as UID override not supported in this mode (got %q)", runAs)
129167
}
168+
nsenterBin := resolveHostNsenter()
169+
if nsenterBin == "" {
170+
return nil, errors.New("nsenter: binary not found (looked under /proc/1/root and /usr/bin)")
171+
}
130172
argv := append([]string{
131-
"nsenter",
173+
nsenterBin,
132174
"--target", "1",
133175
"--mount", "--uts", "--ipc", "--net", "--pid",
134176
"--",

0 commit comments

Comments
 (0)