Skip to content

fix(tool-sandbox): fall back to the interpreter's directory for RPATH-less ELF dependencies - #1650

Draft
Ekleog wants to merge 1 commit into
nolabs-ai:mainfrom
Ekleog:ekleog/fix-nixos
Draft

fix(tool-sandbox): fall back to the interpreter's directory for RPATH-less ELF dependencies#1650
Ekleog wants to merge 1 commit into
nolabs-ai:mainfrom
Ekleog:ekleog/fix-nixos

Conversation

@Ekleog

@Ekleog Ekleog commented Aug 16, 2026

Copy link
Copy Markdown

Linked Issue

Closes #1649

Summary

resolve_shared_library's ELF dependency-closure walk only searched a
NEEDED library's own DT_RPATH/DT_RUNPATH plus a hardcoded FHS default
list (/lib, /usr/lib, etc.). On Nix/NixOS this fails whenever a shared
object has no RPATH/RUNPATH of its own — e.g. libgcc_s.so.1 (from the
gcc-*-libgcc package) needing libc.so.6 by bare soname — because none of
the FHS defaults exist as such there, so nono run refused to start any
command whose dependency closure reached that library.

The real dynamic linker resolves this case fine: a Nix-built ld.so has its
own directory compiled in as its default search path, so an RPATH-less
NEEDED entry still resolves through it. This PR threads the top-level
binary's own ELF interpreter (PT_INTERP) directory through the recursive
closure walk as an extra fallback search directory — tried after the
referencing object's own RPATH/RUNPATH and before the FHS defaults — so
resolve_shared_library mirrors that real resolution order instead of
hardcoding anything Nix-specific.

On FHS/non-Nix systems this is a no-op: the interpreter directory there is
normally /lib or /lib64, already covered by the existing default list.
The ELF_LIB_CACHE memoization key was widened to include the interpreter
directory so a resolution computed under one binary's interpreter doesn't
leak into a different binary's closure within the same launch.

Verified directly against libgcc_s.so.1 on NixOS with readelf -d (empty
RUNPATH) and ldd (resolves via the interpreter's own directory).

Agent Disclosure (if applicable)

This PR was prepared with AI assistance (Claude Code). Files/sections
consulted: crates/nono-cli/src/tool-sandbox/platform/linux.rs
(elf_dependency_closure, resolve_elf_recursive, resolve_shared_library,
parse_elf/parse_dynamic, and the surrounding ELF_*_CACHE memoization),
this repository's AGENTS.md (Coding Agent Contribution Policy, coding
standards, security considerations), and the linked issue.

Test Plan

  • cargo test -p nono-cli --bins tool_sandbox::linux — 34 passed, including
    6 new/changed tests covering the fallback, its precedence under RPATH,
    the not-found error path, and the real PT_INTERP-parsing path via
    std::env::current_exe().
  • cargo fmt --all -- --check and cargo build --release -p nono-cli — clean.
  • Manual repro: nono run --profile claude --allow-cwd -- ~/.nix-profile/bin/claude --dangerously-skip-permissions --help on NixOS no longer fails with the ELF-resolution error.

Checklist

  • An issue exists and is linked above
  • All commits are signed-off, using DCO
  • All new code follows the project's coding standards (CLAUDE.md) and is covered by tests
  • Public-facing changes are paired with documentation updates — n/a, this is internal sandbox-initialization behavior with no public API or documented contract change

Agent Compliance Check (Required for AI/Automated PRs)

  • I am not prohibited from contributing under this policy
  • An issue already exists
  • I disclosed that I am an agent in the issue discussion
  • I described my intent and approach in the issue discussion
  • I reviewed repository coding and security rules for the affected area
  • I provided required attribution for reused or adapted code — n/a, no reused/adapted code
  • I did not use forbidden patterns such as unwrap/expect
  • I used NonoError where required
  • I validated and canonicalized all relevant paths
  • This PR matches the approved or disclosed issue scope

…-less ELF dependencies

Closes nolabs-ai#1649

`resolve_shared_library`'s ELF dependency-closure walk only searched a
`NEEDED` library's own `DT_RPATH`/`DT_RUNPATH` plus a hardcoded FHS default
list (`/lib`, `/usr/lib`, etc.). On Nix/NixOS this fails whenever a shared
object has no RPATH/RUNPATH of its own — e.g. `libgcc_s.so.1` (from the
`gcc-*-libgcc` package) needing `libc.so.6` by bare soname — because none of
the FHS defaults exist as such there, so `nono run` refused to start any
command whose dependency closure reached that library.

The real dynamic linker resolves this case fine: a Nix-built `ld.so` has its
own directory compiled in as its default search path, so an RPATH-less
`NEEDED` entry still resolves through it. This PR threads the *top-level*
binary's own ELF interpreter (`PT_INTERP`) directory through the recursive
closure walk as an extra fallback search directory — tried after the
referencing object's own RPATH/RUNPATH and before the FHS defaults — so
`resolve_shared_library` mirrors that real resolution order instead of
hardcoding anything Nix-specific.

On FHS/non-Nix systems this is a no-op: the interpreter directory there is
normally `/lib` or `/lib64`, already covered by the existing default list.
The `ELF_LIB_CACHE` memoization key was widened to include the interpreter
directory so a resolution computed under one binary's interpreter doesn't
leak into a different binary's closure within the same launch.

Verified directly against `libgcc_s.so.1` on NixOS with `readelf -d` (empty
`RUNPATH`) and `ldd` (resolves via the interpreter's own directory).

- `cargo test -p nono-cli --bins tool_sandbox::linux` — 34 passed, including
  6 new/changed tests covering the fallback, its precedence under RPATH,
  the not-found error path, and the real `PT_INTERP`-parsing path via
  `std::env::current_exe()`.
- `cargo fmt --all -- --check` and `cargo build --release -p nono-cli` — clean.
- Manual repro: `nono run --profile claude --allow-cwd -- ~/.nix-profile/bin/claude --dangerously-skip-permissions --help` on NixOS no longer fails with the ELF-resolution error.

- [x] An issue exists and is linked above
- [x] All commits are signed-off, using [DCO](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin)
- [x] All new code follows the project's coding standards ([CLAUDE.md](CLAUDE.md)) and is covered by tests
- [x] Public-facing changes are paired with documentation updates — n/a, this is internal sandbox-initialization behavior with no public API or documented contract change

This PR was prepared with AI assistance (Claude Code). Files/sections
consulted: `crates/nono-cli/src/tool-sandbox/platform/linux.rs`
(`elf_dependency_closure`, `resolve_elf_recursive`, `resolve_shared_library`,
`parse_elf`/`parse_dynamic`, and the surrounding `ELF_*_CACHE` memoization),
this repository's `AGENTS.md` (Coding Agent Contribution Policy, coding
standards, security considerations), and the linked issue.

- [x] I am not prohibited from contributing under this policy
- [x] An issue already exists
- [x] I described my intent and approach in the issue discussion
- [x] I reviewed repository coding and security rules for the affected area
- [x] I provided required attribution for reused or adapted code — n/a, no reused/adapted code
- [x] I did not use forbidden patterns such as unwrap/expect
- [x] I used NonoError where required
- [x] I validated and canonicalized all relevant paths
- [x] This PR matches the approved or disclosed issue scope

Signed-off-by: Léo Gaspard <leo@gaspard.io>
@github-actions github-actions Bot added bug Something isn't working nono-cli size/medium labels Aug 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR Review Summary

Size

Metric Value
Lines added +207
Lines removed -18
Total changed 225
Classification Medium (50–300 lines)

Affected crates

  • crates/nono-cli — CLI changes. Verify argument parsing, flag documentation, and UX behaviour across supported platforms.

Blast radius — Contained

This PR touches: source code


Updated automatically on each push to this PR.

@jocelynthode

Copy link
Copy Markdown

Hey I tried to build nono from this PR. It fixed the command_policies failing, but I hit then another bug. When having a command_policies defined and trying to run for example pi. It fails. This seems related to the fact that pi is a wrapper shell script (common in nixpkgs)

any #!-script on $PATH fails ELF parsing in parse_elf(), the error propagates out of add_outer_exec_file_with_deps, and the entry is silently skipped (debug-level "outer exec gate skipped"). Neither the script nor its shebang interpreter is added to the execute allow-list, so launching it under any profile that has command policies fails with exit 127.

Not sure if fixing this should be part of this PR or a new issue.

@SequeI

SequeI commented Aug 26, 2026

Copy link
Copy Markdown
Member

Fixing both issues in one if the scope doesnt expand greatly would be very appreciated!

@jocelynthode

Copy link
Copy Markdown

@Ekleog Is that something you would be willing to look into? I'm not well versed in Rust and would not feel comfortable vibe coding and submitting a PR that I would not thoroughly understand.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working nono-cli size/medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tool-sandbox: ELF dependency resolution fails on Nix/NixOS when a NEEDED library has no RPATH/RUNPATH (e.g. gcc's libgcc_s.so.1 → libc.so.6)

3 participants