Skip to content

Resolve bwrap grant symlinks link by link, and fix the follow-ups - #167

Merged
M3gA-Mind merged 4 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/104-bwrap-symlink-followups
Nov 12, 2026
Merged

M3gA-Mind merged 4 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/104-bwrap-symlink-followups

Conversation

@M3gA-Mind

Copy link
Copy Markdown
Collaborator

Summary

Four of the six follow-ups from the #99 security review (#104):

  • Nested host aliases (item 3). reject_symlinked_grants only checked a grant below a prefix that names a writable root. So a host alias pointing into a directory inside the root (/aliases/v -> <root>/vendor, with vendor replaced by a planted link) was never checked. Each grant is now resolved one component at a time, following every link, and refused if a link on the way lives inside a writable root or grant. A link cycle is refused after 40 hops instead of being followed.
  • base_dir canonicalization (item 2). bwrap compared an uncanonicalized $XDG_RUNTIME_DIR or TMPDIR against canonical grants, so a runtime directory reached through a symlink into a grant was accepted. Candidates are now canonicalized before the check, and the canonical path is returned.
  • Test kind (item 4). The module's planted-link test asserts INVALID_FILE_PATH, not just is_err().
  • Docs (item 5). The module and jail READMEs say that a grant through a link inside a writable root is refused, host-created links included (pnpm, monorepos). The bus doc and the 1.20 spec no longer say a missing deny_read path is "ignored": since Refuse missing deny_read paths before bwrap spawn #107, bwrap refuses one inside a mount.

Related issue

Part of #104. Item 1 (fd-bound grants via openat2 + --ro-bind-fd) is #160, and item 6's seccomp filter is #161. What is left of item 6 (resource limits on legacy bwrap jails, a late deny_read) is tracked there.

API or behavior changes

  • A grant whose resolution passes through a symlink inside a writable root or grant is refused (InvalidInput, or INVALID_FILE_PATH from the module). That includes a host alias into the root's subdirectories, which used to pass.
  • bwrap's argument file goes in the canonical runtime directory.

Validation

On our Linux CI runner (bubblewrap 0.9.0):

  • At 62fbaba337, the first build of this change:
    • cargo build: ok.
    • cargo test -p tinybox-jail -p tinybox-module: 480 passed, 0 failed, with no failures unique to this branch.
    • The live bwrap tests: 45 passed.
  • Red controls, each restored afterwards:
    • main's jail.rs with this branch's tests: a_host_alias_into_a_writable_root_cannot_reach_a_planted_link fails;
    • not canonicalizing the args directory's base: an_args_directory_reached_through_a_symlink_into_a_grant_is_refused fails.
  • At the head, 8366bb0, which differs only by cargo fmt and two test-helper lint fixes:
    • cargo fmt --all -- --check: clean.
    • cargo clippy --all-targets --all-features -- -D warnings: 0 errors, same as main.
    • cargo build: ok.
    • cargo test -p tinybox-module native_jail: 21 passed.

Tests

  • A host alias into a writable root can't reach a planted link, whether the grant is the alias itself or a path under it. An alias into a real directory of the root is allowed. A grant through a link cycle is refused.
  • An args directory reached through a symlink into a grant falls back to the temp dir; the existing preference test now compares canonical paths.
  • The module test asserts the refusal kind.
  • Red controls: main's jail.rs fails the nested-alias test; dropping the canonicalization fails the base_dir test.

Documentation

crates/tinybox-module/src/tinybus_module/README.md, crates/tinybox-jail/README.md, tinybox-bus/src/jail.rs and docs/specs/contract-1-20.md.

Checklist

  • The change is focused on one logical change
  • No new #[allow(...)], #[ignore], or relaxed lints
  • No secrets, tokens, or .env contents in the diff or the description

Four of the six follow-ups in tinyhumansai#104:

- reject_symlinked_grants resolved a grant only below a prefix that names
  a writable root, so a host alias pointing into a directory inside the
  root (/aliases/v -> <root>/vendor, with vendor replaced by a planted
  link) was never checked. It now resolves each grant one component at a
  time, following every link, and refuses one that passes through a link
  living inside a writable root or grant. A link cycle is refused after
  40 hops instead of being followed.
- bwrap's base_dir compared an uncanonicalized $XDG_RUNTIME_DIR or TMPDIR
  against canonical grants, so a runtime directory reached through a
  symlink into a grant was accepted. Candidates are canonicalized before
  the check, and the canonical path is returned.
- The module's planted-link test asserts INVALID_FILE_PATH instead of
  just is_err().
- The module and jail READMEs say that a grant through a link inside a
  writable root is refused, host-created links (pnpm, monorepos)
  included. The bus doc and the 1.20 spec no longer say a missing
  deny_read path is ignored: since tinyhumansai#107 bwrap refuses one inside a mount.

Not in this change: fd-bound grants via openat2/--ro-bind-fd (item 1)
and the known gaps in item 6 (seccomp, resource limits, late deny_read).

Refs tinyhumansai#104
@coderabbitai

coderabbitai Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 5482bd60-9892-40a0-8131-78327baf1ab8
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@tinysweeper

tinysweeper Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Tiny Sweeper review

Reviewed head: 54b1edd22ddd1138147b18822aaa2ee9628f5711

Verdict: Ready for maintainer review · Priority: none
Coverage: Reviewed 9 of 9 files.
Pull request purpose (title): Resolve bwrap grant…

Diagnostics and supporting details

Full summary

Pull request purpose (title): Resolve bwrap grant symlinks link by link, and fix the follow-ups. Tiny Sweeper completed its review; deterministic results follow.

Review snapshot

Change surface Files Review signal Count
Production 3 Active findings 0
Tests 3 Noted findings 0
Documentation 3 Resolved findings 0
Configuration 0 Pending checks/questions 0

Completeness: Complete
Test assessment: Test coverage is assessed from changed tests and lane evidence; execution is not claimed without trusted check data.

What changed

Whole-pull-request behavior (the latest-push-delta section is unavailable: the review state has the prior reviewed head and rendered evidence but no trusted base-to-head diff, so no push delta is described here). `reject_symlinked_grants` in crates/tinybox-jail/src/jail.rs now canonicalizes the writable ancestors, then walks each grant component by component through a `pending` work queue, pushing symlink targets back onto the queue and clamping back-tracking `..` components lexically; a link encountered strictly inside a canonical writable path is refused with `InvalidInput` naming the link, a missing component ends the walk, and more than `MAX_LINK_HOPS` (40) links is refused as too many symlinks. This replaces the previous prefix-anchored `reject_symlink_below` helper and closes the host-alias-into-a-writable-subdirectory gap, while still allowing an alias into a real (non-link) directory of the root. In crates/tinybox-jail/src/bwrap/launcher.rs, `base_dir` now `filter_map`s candidates through `canonicalize`, so a runtime directory reached through a symlink into a grant is detected and the temp dir wins, and the canonical candidate is what is returned. In the module, crates/tinybox-module/src/tinybus_module/native_jail_tests.rs adds `assert_refused_as_an_invalid_path` and applies it to the planted-link cases so the failure kind, not merely failure, is checked. Documentation in crates/tinybox-jail/README.md, crates/tinybox-module/src/tinybus_module/README.md, crates/tinybox-bus/src/jail.rs and docs/specs/contract-1-20.md is updated to match, including the semantic shift for host-created links and the bwrap refusal of a missing denial inside a mount.

Features

  • Modified — Grant resolution follows symlinks component by component: `reject_symlinked_grants` resolves a grant one component at a time, following every link, and refuses with `InvalidInput` when any link it passes through lives strictly inside a canonical writable root or grant. Host-created aliases pointing into a writable root, which no prefix of the grant's own spelling revealed, are now refused; a grant is still passed when the alias targets a real directory of the root. (crates/tinybox-jail/src/jail.rs, crates/tinybox-jail/src/jail_tests.rs, crates/tinybox-module/src/tinybus_module/native_jail_tests.rs)
  • Added — Link-cycle hop limit: A grant whose resolution passes through more than 40 links (the kernel `ELOOP` bound) is refused as `InvalidInput` instead of being followed, so a mutual link cycle yields a refusal rather than an unbounded walk. (crates/tinybox-jail/src/jail.rs, crates/tinybox-jail/src/jail_tests.rs)
  • Modified — bwrap args directory canonicalized before the grant check: `base_dir` canonicalizes the `$XDG_RUNTIME_DIR` and temp-dir candidates before testing them against canonical grants and returns the canonical path, so a runtime directory reached through a symlink into a grant is rejected and the temp dir is chosen; candidates that fail to canonicalize are skipped. (crates/tinybox-jail/src/bwrap/launcher.rs pub(super) fn base_dir(, crates/tinybox-jail/src/bwrap/launcher_tests.rs fn the_args_directory_prefers_xdg_runtime_dir_but_never_one_inside_a_grant() ->)
  • Modified — Module planted-link test asserts the refusal kind: The module-level test now checks that a grant through a planted link fails with `tinybox_bus::INVALID_FILE_PATH` rather than any error, pinning the user-visible refusal classification (invalid path, not unavailable backend) for read-only, read-write, and aliased-root spellings. (crates/tinybox-module/src/tinybus_module/native_jail_tests.rs fn a_symlink_planted_in_the_writable_root_cannot_redirect_a_grant())
  • Modified — Documented refusal of grants through links in a writable root: The jail and module READMEs now state that a grant whose resolution passes through a symlink inside the root or a writable grant is refused, host-created links included, and that pnpm/monorepo workspaces must grant real paths instead of those links. (crates/tinybox-jail/README.md spawns, never the core process itself., crates/tinybox-module/src/tinybus_module/README.md)
  • Modified — `deny_read` docs describe retained missing paths: The bus config doc and the 1.20 contract spec no longer say non-existent denial paths are ignored: they are kept and re-resolved before spawn, except that the bubblewrap backend refuses a missing denial inside one of the jail's mounts because masking it would create the path on the host. (crates/tinybox-bus/src/jail.rs pub struct NativeJailConfig {, docs/specs/contract-1-20.md at least 20, which `tinybox_bus::is_compatible` already enforces.)

Tests

  • unit — A host alias into a real (non-link) directory of a writable root is still accepted as a grant.: Guards the new refusal against over-blocking legitimate grants. (crates/tinybox-jail/src/jail_tests.rs)
  • integration — A symlink planted in the writable root cannot redirect a grant, and the module reports it as `INVALID_FILE_PATH` for read-only, read-write, and aliased-root spellings.: Strengthened assertion on an existing test, pinning the wire error rather than any failure. (crates/tinybox-module/src/tinybus_module/native_jail_tests.rs fn a_symlink_planted_in_the_writable_root_cannot_redirect_a_grant())

Findings

No active actionable findings.

Before merge

None.

How this fits together

flowchart LR
  n0["NativeJailConfig<br/>changed"]:::changed
  n1["Jail<br/>changed"]:::changed
  n2["..._planted_link_however_the_root_is_spelled<br/>changed"]:::changed
  n3["...the_writable_root_cannot_redirect_a_grant<br/>changed"]:::changed
  n4["...ded_per_jail_so_shared_network_is_refused<br/>changed"]:::changed
  n5["tempdir"]:::impacted
  n6["spawn_captured_with"]:::impacted
  n7["prepare_with_resources"]:::impacted
  n8["config"]:::impacted
  n9["...cpu_limit_throttles_the_live_native_scope"]:::impacted
  n10["...s_a_workload_that_touches_past_its_budget"]:::impacted
  n2 -->|calls| n5
  n2 -->|tests| n5
  n3 -->|calls| n5
  n3 -->|tests| n5
  n3 -->|calls| n8
  n3 -->|tests| n8
  n4 -->|calls| n5
  n4 -->|tests| n5
  n4 -->|calls| n8
  n4 -->|tests| n8
  n6 -->|uses| n1
  n7 -->|uses| n1
  n8 -->|uses| n0
  n9 -->|calls| n5
  n9 -->|tests| n5
  n9 -->|calls| n6
  n9 -->|tests| n6
  n9 -->|calls| n7
  n9 -->|tests| n7
  n9 -->|calls| n8
  n9 -->|tests| n8
  n10 -->|calls| n5
  n10 -->|tests| n5
  n10 -->|calls| n6
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading
Agent review details

critique

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: Reviewed 9 files; 3 findings. _The code index is behind this pull request (indexed at `5c64ef5ce29b`), so retrieved context may be out of date._

security

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The security lane answered, covering 6 files with 0 findings; the three prose/spec files were recorded as out of scope for security review rather than unexamined.
  • Lane summary: Reviewed 6 files; 0 findings. 3 files were not security-reviewed: crates/tinybox-jail/README.md (prose or tabular data), crates/tinybox-module/src/tinybus_module/README.md (prose or tabular data), docs/specs/contract-1-20.md (prose or tabular data). _The code index is behind this pull request (indexed at `5c64ef5ce29b`), so retrieved context may be out of date._

tests

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The tests lane answered and reported that the new branches (hop limit, read_link failure, relative grants) are exercised or trivially safe and that the new cases cover the alias-into-writable-root, allowed real-directory alias, link cycle, and symlinked runtime directory, with no defect worth blocking on; no test execution is claimed by this review.
  • Lane summary: The change replaces the lexical symlink check with a component-by-component resolver and canonicalizes the bwrap args-file base directory, and it ships tests for the new alias-into-writable-root case, the allowed real-directory alias, the link cycle, and the symlinked runtime directory. The new branches (hop limit, read_link failure, relative grants) are exercised or trivially safe, and the module test now pins the refusal to INVALID_FILE_PATH rather than any error. I found no defect worth blocking on. _The code index is behind this pull request (indexed at `5c64ef5ce29b`), so retrieved context may be out of date._

commits

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The commits lane answered with no sensitive content in committed files and no findings.
  • Lane summary: No sensitive content in committed files. _The code index is behind this pull request (indexed at `5c64ef5ce29b`), so retrieved context may be out of date._

description

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: The description matches the diff: the nested-alias resolution, the `base_dir` canonicalization, the module test's assertion on `INVALID_FILE_PATH`, and the doc updates are all present and described accurately, including the behaviour change for host aliases into a writable root. I found no defect in the changed code that I can anchor to a concrete failing input. _The code index is behind this pull request (indexed at `5c64ef5ce29b`), so retrieved context may be out of date._
Evidence and run details
  • Models: ladder/vectors-oai3, deepseek-v4.1-flash
  • Spend: $0.006634
  • Tokens: 528779 input · 8881 output · 187904 cached · 651 embedding
  • Updated: 2026-11-11T19:37:00Z
  • Continuity: summary cache chain restarted at the storage ceiling.
Head State Pass summary
8366bb08b124 ready for maintainer review 0 active finding(s), 0 resolved finding(s) (at 2026-11-11T13:14:11Z)
299949dfa21f ready for maintainer review 0 active finding(s), 0 resolved finding(s) (at 2026-11-11T14:15:36Z)
e8bfef10e372 ready for maintainer review 0 active finding(s), 0 resolved finding(s) (at 2026-11-11T18:29:47Z)
54b1edd22ddd ready for maintainer review 0 active finding(s), 0 resolved finding(s) (at 2026-11-11T19:37:00Z)

tinysweeper 0.1.2

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

tinysweeper found nothing blocking. Approving.

Diagnostics

Total spend: $0.0336

@tinysweeper tinysweeper Bot added the priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. label Nov 11, 2026
@M3gA-Mind
M3gA-Mind merged commit cf9fc7c into tinyhumansai:main Nov 12, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant