Skip to content

sandbox: mirror the dynamic loader into the namespaces sandbox - #796

Merged
magurotuna merged 3 commits into
mainfrom
fix/sandbox-loader-discovery
Aug 2, 2026
Merged

sandbox: mirror the dynamic loader into the namespaces sandbox#796
magurotuna merged 3 commits into
mainfrom
fix/sandbox-loader-discovery

Conversation

@magurotuna

Copy link
Copy Markdown
Member

What

Fixes external plugins failing to start on non-FHS distros (NixOS, Guix).
The namespaces sandbox's bind plan only mirrored FHS loader locations
(/lib, /lib64, /usr/lib*, ...); on NixOS/Guix the dynamic loader
lives under /nix/store / /gnu/store, so exec of any dynamically-linked
plugin failed with ENOENT and go-plugin reported a misleading
"Unrecognized remote plugin message" handshake error. The probe did not
catch it because it never execs a real binary.

Changes

  • bindPlan now discovers the plugin binary's dynamic loader from its ELF
    .interp section (debug/elf) and mirrors the loader file and its
    directory
    (glibc keeps libc.so.6 etc. next to the loader) into the
    sandbox, deduplicated against the FHS entries. No distro-specific
    hardcoding — covers NixOS, Guix, and any future layout; static binaries
    are unaffected (no-op).
  • New tests in internal/sandbox/loader_bind_test.go (+
    testdata/loaderhelper/, a small dynamically-linked helper):
    • unit: bindPlan includes the binary's exact loader path,
    • integration: a dynamically-linked binary actually execs inside the
      namespaces sandbox.

Verification

  • internal/sandbox tests pass, including -race
  • go test ./cmd/clawpatrol/ plugin-spawn tests (TestExampleSocksTunnel,
    TestExternalCredentialInjectsAuthorizationThroughBuiltInHTTPS, ...)
    previously failed on NixOS; they now pass (only pre-existing
    environmental Landlock failure remains on this host)
  • gofmt -l . clean

Closes #794

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

Preview removed (PR closed).

@magurotuna
magurotuna requested a review from avocet-bot August 2, 2026 06:20
@avocet-bot

avocet-bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Avocet review status

State: Completed
Target: bebacb9e7ea7
Requested by: magurotuna
Model: Claude Opus
Attempt: 1 of 3
Updated: 2026-08-02T06:37:03.144Z
Formal review: #796 (review)

The formal GitHub review is the authoritative result. This comment only reports operational status.

…he test on linux

Binding the loader file itself broke FHS distros: the loader lives
inside a /lib* directory that bindPlan already mirrors read-only, so
creating the bind target file fails with EPERM. The directory bind
covers the loader and its sibling libc either way.

@avocet-bot avocet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review model: Claude Opus

⚠️ Review of an earlier PR revision.

Reviewed base: 438b1bcd9ddd9ac1cb6ecb6abdb6667f6b035363
Reviewed commit: bebacb9e7ea7cdd56147d320235eaae35d4125cc
Current PR base: 438b1bcd9ddd9ac1cb6ecb6abdb6667f6b035363
Current PR head: c76295d0a6de4ff5d0c5cd4dbc4467d5a4304d9a
Verdict for the reviewed commit: REQUEST_CHANGES (informational only)

This review applies only to the reviewed comparison above.
Newer commits and their integration with the current base were not reviewed.
Re-request review to review the current PR revision.

stage1_linux.go:269,311-314,321-332,353 — On standard FHS distros the new loader binds break namespaces-sandbox startup for dynamically-linked (cgo/non-Go) plugins, a regression from the prior working behavior.

Mechanism: for a typical glibc binary the ELF interpreter is /lib64/ld-linux-x86-64.so.2 (confirmed for the test helper on this repo). loaderBinds emits two binds — the interpreter file and its directory /lib64. dedupeBinds collapses only exact src strings, so the loader directory /lib64 is deduped against the FHS entry (line 257) but the loader file survives. Because loaderBinds is appended after the FHS list (line 269), the deduped plan binds /lib64 (ro) at index 3 and then the loader file /lib64/ld-linux-x86-64.so.2 afterward. By the time the loader-file bind runs, its parent root/lib64 is already a recursive read-only bind (remountRO applied in the same bindIntoRoot call). bindIntoRoot unconditionally creates the file mountpoint via os.OpenFile(target, O_CREATE|O_WRONLY, 0o644) (line 353); opening for write under a read-only mount returns EROFS, so bindIntoRoot errors, setupNamespaceRoot/stage1Main fail, and Stage1 exits 87 — the plugin never starts. This holds whenever the interpreter lives under any FHS-bound dir (/lib, /lib64, /usr/lib, multiarch subdirs), i.e. all mainstream distros.

I could not exercise the live mount here (unprivileged user namespaces are blocked in this environment, so Probe() returns landlock and TestDynamicBinaryRunsInNamespacesSandbox skips), but the failure follows directly from the bind ordering and standard read-only-filesystem semantics. Note the plan-only TestBindPlanIncludesLoader passes and gives false confidence because it never runs bindIntoRoot.

Required fix: don't emit a redundant loader bind when the interpreter already sits under an FHS-mirrored directory (e.g. skip a bind whose src is contained by an earlier planned bind), or order the loader binds before the FHS dir binds so the loader file's placeholder is created on the writable tmpfs and later shadowed by the dir bind. Extend dedup/containment handling accordingly, and ensure the e2e namespaces test actually runs (not just the plan assertion) on a standard-distro CI runner so this path is covered.

@magurotuna
magurotuna requested a review from avocet-bot August 2, 2026 13:42
@avocet-bot

avocet-bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Avocet review status

State: Completed
Target: 1fa5af52722b
Requested by: magurotuna
Model: Claude Opus
Attempt: 1 of 3
Updated: 2026-08-02T13:45:37.021Z
Formal review: #796 (review)

The formal GitHub review is the authoritative result. This comment only reports operational status.

@avocet-bot avocet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review model: Claude Opus

Model verdict: APPROVE (informational only)

Formal review policy did not authorize an authoritative event (required_checks_missing).

Re-review of the prior finding: FIXED. Previously loaderBinds returned both the interpreter file and its directory; the file bind survived dedup and, being ordered after the read-only /lib64 bind, hit EROFS in bindIntoRoot's OpenFile(O_CREATE|O_WRONLY) placeholder step — breaking dynamically-linked plugins on standard FHS distros. The new loaderBinds (stage1_linux.go:311) returns only filepath.Dir(interp). On standard x86-64 the loader dir is /lib64, an exact FHS entry, so dedupeBinds drops it (confirmed on this host). Directory binds never create a file placeholder — they MkdirAll (a no-op on an already-present dir) then Mount (which does not write to the underlying fs) — so even a multiarch subdir loader (/lib/x86_64-linux-gnu under an already-RO /lib) no longer fails. The NixOS/Guix case now binds the store loader dir and works. Tests pass/skip correctly here (namespaces backend unavailable, so the e2e test skips to landlock); gofmt and vet clean; the added //go:build linux guard is correct.

Non-blocking: on non-FHS distros, mirroring only the loader's directory brings glibc's co-located libs but not shared libraries the binary pulls from other store paths via RPATH; a purely libc-linked plugin (like the test helper) works, but richer non-Go plugins may still miss transitive deps. Not a regression from base and out of scope for this fix.

@magurotuna
magurotuna merged commit b2a84fa into main Aug 2, 2026
5 checks passed
@magurotuna
magurotuna deleted the fix/sandbox-loader-discovery branch August 2, 2026 14:01
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.

sandbox: namespaces backend cannot exec dynamically-linked plugins on non-FHS distros (NixOS, Guix) — probe reports the backend as available

2 participants