Skip to content

Refuse a missing deny_read inside a 2.1 jail's mounts, and document the mask gap - #169

Open
M3gA-Mind wants to merge 6 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/104-late-deny-read
Open

M3gA-Mind wants to merge 6 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/104-late-deny-read

Conversation

@M3gA-Mind

Copy link
Copy Markdown
Collaborator

Summary

Item 6 of #104 asks about deny_read paths created after the jail starts.

The 2.1 native resource jail. Since #107, the bubblewrap jail refuses a missing deny_read inside a mount, because a path created later would not be covered. The 2.1 native resource jail had no such check. It masked a missing path with --ro-bind /dev/null <path>, and bubblewrap then creates that path on the host: an empty, read-only file in the caller's tree. This was shown on bubblewrap 0.9.0. The 2.1 path now does what the bwrap jail does:

  • a missing denial inside a mount is refused (NotFound, the same message);
  • one outside every mount is skipped, since the sandbox never shows it.

A documented gap. A mask lasts only as long as the path it covers. A denial is a mount over that path's directory entry. When a host process replaces the path while the jail runs (an atomic rename, as credential helpers and editors write, or delete and recreate), the kernel drops that mount from the jail's namespace, and the jail sees the new file. This was shown on bubblewrap 0.9.0 and Linux 7.0 for both a file and a directory mask. bubblewrap can't prevent it.

The bwrap backend's known-gaps docs now say so, with what a host should do instead: deny a credential store outside every grant, so it's never mounted at all, or deny its parent directory.

The deny_read docs are also corrected. They said missing paths are "ignored" (bus) or "retained so backend rules can cover later creation" (jail), and bubblewrap does neither.

Related issue

Part of #104 (item 6). Items 1-5 are #160 and #167; item 6's seccomp filter is #161. The last part, resource limits on legacy bwrap jails, follows once #159 lands.

API or behavior changes

  • StartJailedExecWithResources with a missing deny_read path inside the root or a grant is now refused before launch. Before, it was accepted and an empty file was created on the host.
  • No API or contract change.

Validation

On our Linux CI runner (bubblewrap 0.9.0), at 780e281695. The head differs only by applying cargo fmt's patch:

  • cargo clippy --all-targets --all-features -- -D warnings: 0 errors, same as main. cargo build and cargo doc: ok.
  • cargo test -p tinybox-linux sandbox: 41 passed.
  • cargo test -p tinybox-module native_jail: 21 passed, all live tests run (none skipped).
  • cargo test --workspace --no-fail-fast: no failures unique to this branch.
  • Red control: without the new check, a_missing_denial_inside_a_mount_is_refused_and_one_outside_is_skipped fails (no refusal).
  • Separately, a bubblewrap probe on the same runner showed:
    • a --ro-bind /dev/null mask and a --tmpfs mask both stop covering their path once the host replaces it;
    • masking a missing path creates it on the host.

Tests

  • a_missing_denial_inside_a_mount_is_refused_and_one_outside_is_skipped: inside the root, refused with NotFound, and nothing is created on the host; outside every mount, the build succeeds and the path isn't in the argv.

Documentation

The known gaps in tinybox-jail/src/bwrap/mod.rs (the mask lifetime), NativeJailConfig::deny_read in tinybox-bus, and Jail::deny_read in tinybox-jail.

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

…he mask gap

The 2.1 native resource jail masked a missing deny_read path with
--ro-bind /dev/null, which makes bwrap create an empty file at that path in
the caller's tree; and a path created later was not covered anyway. As the
bwrap jail does since tinyhumansai#107, a missing denial inside a mount is now refused
and one outside every mount is skipped.

The docs said missing denials are ignored, or retained to cover later
creation, which bubblewrap does neither. They also did not say that a mask
lasts only as long as the path it covers: a host that replaces a denied path
inside a grant (an atomic rename) exposes the new file to a running jail,
shown on bubblewrap 0.9.0 and Linux 7.0. That is now a documented known gap
with what a host should do instead.

Part of 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: 5d6fc8d0-c270-42ec-8d1e-fbf06b39ea70
  • 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: 93c77d6f40933b55cc677f7a84739251e625b7d0

Findings

  • high: Add the missing disable_userns field to the test request ((pull request description))
    Problem: `NativeJailCommand` has a `disable_userns: bool` field (`crates/tinybox-linux/src/sandbox/args.rs`), between `allow_subprocess` and `cpu_millis`. The new test's closure builds the
    Suggested fix:
            allow_net: false,
            allow_subprocess: true,
            disable_userns: true,
            cpu_millis: Some(1000),
    

Resolved this pass

  • Add the missing disable_userns field to the test request
  • Add the missing disable_userns field to the test request

Verdict: Changes requested · Priority: high
Coverage: Reviewed 5 of 5 files.

Diagnostics and supporting details

Full summary

Pull request purpose (title): Refuse a missing deny_read inside a 2.1 jail's mounts, and document the mask gap. This pull request closes item 6 of #104 for the 2.1 native resource jail: a missing `deny_read` path inside a mount is now refused before launch instead of being masked with `--ro-bind /dev/null`, which made bubblewrap create an empty file on the host, and a missing denial outside every mount is skipped when the argv is built. The bwrap backend's known-gaps documentation now records that a mask lasts only as long as the path it covers, and the `deny_read` docs in `tinybox-bus` and `tinybox-jail` are corrected to describe per-backend handling. One blocking finding remains open: the new test builds `NativeJailCommand` without its `disable_userns` field, so the tinybox-linux test target will not compile and the new behaviour is never exercised. The latest-push-delta section is unavailable; the whole-pull-request diff is described here, not a push delta.

Review snapshot

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

Completeness: Complete
Test assessment: No supported feature-to-test mapping was available; this does not mean tests are absent or passed.

What changed

The native jail argv builder runs a preflight check, `refuse_missing_denials_inside_mounts`, that returns `NotFound` when a `deny_read` path does not exist and lies inside the root, a read-only grant, or a read-write grant. The argv construction loop filters out denials whose `symlink_metadata` fails, so a missing denial outside every mount is never masked. Documentation in `NativeJailConfig::deny_read`, `Jail::deny_read`, and the bwrap known-gaps list is updated to state that bubblewrap refuses a missing denial inside a mount, that a mask lasts only until the host replaces the path, and what a host should do instead. A new Linux-only test covers both branches, but it omits the `disable_userns` field of `NativeJailCommand`, which prevents the tinybox-linux test target from compiling.

Features

  • Added — Preflight refusal of missing deny_read paths inside mounts: A `StartJailedExecWithResources` request with a missing `deny_read` path inside the root or a grant is now refused with `NotFound` before launch, instead of being accepted and causing bubblewrap to create an empty read-only file in the caller's tree. (crates/tinybox-linux/src/sandbox/args.rs fn validate_native_jail_command(request: &NativeJailCommand) -> std::io::Result<, crates/tinybox-linux/src/sandbox/args.rs fn native_bwrap_argv()
  • Modified — Skip missing denials outside every mount when building argv: A missing `deny_read` path that lies outside the root and all grants is no longer emitted as a mask argument, since the sandbox never shows it; only existing denials are masked. (crates/tinybox-linux/src/sandbox/args.rs fn native_bwrap_argv()
  • Modified — Corrected deny_read documentation across bus, jail, and bwrap backend: `NativeJailConfig::deny_read` and `Jail::deny_read` now describe per-backend handling of missing paths rather than claiming they are ignored or retained, and the bwrap known-gaps section documents that a mask is dropped when the host unlinks or renames over the denied path, with guidance to deny outside every grant or deny the parent directory. (crates/tinybox-bus/src/jail.rs pub struct NativeJailConfig {, crates/tinybox-jail/src/jail.rs pub struct Jail {, crates/tinybox-jail/src/bwrap/mod.rs)

Tests

No supported feature-to-test mapping was produced. Test execution is not inferred.

Before merge

  • Address Add the missing disable_userns field to the test request ((pull request description)).
Agent review details

critique

  • Conclusion: Failure
  • Scope reviewed: all assigned evidence
  • Lane summary: Reviewed 2 files; 1 finding. (1 already raised on this pull request)

security

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The security lane reports no security defect introduced: the existing validation still runs first, the new error is only raised for missing paths under existing mounts, and present denials are still masked.
  • Lane summary: Reviewed 2 files; 0 findings.

tests

  • Conclusion: Failure
  • Scope reviewed: all assigned evidence
  • Lane summary: The doc updates are harmless and the new `a_missing_denial_inside_a_mount_is_refused_and_one_outside_is_skipped` test genuinely pins both new branches (refusal with no host mutation, and the skip of a denial outside every mount). The test target still cannot compile, though: the `NativeJailCommand` literal in it omits the existing `disable_userns` field, so the behaviour the test intends to guard is never exercised. (1 already raised on this pull request)

commits

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The commits lane reports no sensitive content in committed files.
  • Lane summary: No sensitive content in committed files. (1 earlier finding(s) still open)

description

  • Conclusion: Failure
  • Scope reviewed: all assigned evidence
  • Positive: The description lane reports the description matches the diff: the 2.1 native path refuses a missing denial inside a mount and skips one outside it, and the mask-lifetime gap is documented in the bubblewrap known gaps and the bus and jail `deny_read` docs.
  • Lane summary: The description matches the diff: the 2.1 native path now refuses a missing denial inside a mount and skips one outside it, and the mask-lifetime gap is documented in the bubblewrap known gaps, the bus and jail `deny_read` docs. One blocking problem remains in the test added by this revision.
  • Evidence: (pull request description) — Add the missing disable_userns field to the test request
Evidence and run details
  • Models: ladder/vectors-oai3, deepseek-v4.1-flash
  • Spend: $0.014778
  • Tokens: 441855 input · 36997 output · 131200 cached · 376 embedding
  • Updated: 2026-11-12T10:12:56Z
Head State Pass summary
9928f973a85c ready for maintainer review 0 active finding(s), 0 resolved finding(s) (at 2026-11-11T14:13:28Z)
4acfba48cd65 ready for maintainer review 0 active finding(s), 0 resolved finding(s) (at 2026-11-11T18:30:14Z)
99dd8f447388 ready for maintainer review 0 active finding(s), 0 resolved finding(s) (at 2026-11-12T07:31:41Z)
a9eb478d7a90 changes requested 1 active finding(s), 0 resolved finding(s) (at 2026-11-12T08:35:24Z)
93c77d6f4093 changes requested 1 active finding(s), 2 resolved finding(s) (at 2026-11-12T10:12:56Z)

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.0335

@tinysweeper tinysweeper Bot added the priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. label Nov 11, 2026

@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.0022

@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.

Requesting changes: 1 lane(s) blocking, worst finding is high.

Fix or reply to the findings below and push. The next review clears this automatically once they are gone — you should not need to dismiss anything by hand.

Diagnostics

Total spend: $0.0188

read_write: Vec::new(),
deny_read,
allow_net: false,
allow_subprocess: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

priority high critique likely

Add the missing disable_userns field to the test request

`NativeJailCommand` declares a `disable_userns` field that `native_bwrap_argv` reads (`if request.disable_userns {`), and the only in-repo constructor in `crates/tinybox-module/src/tinybus_module/native_jail.rs` sets it (`disable_userns: bwrap_disables_userns()`). The struct is a plain `#[derive(Debug, Clone)]` type with no `Default`, so a struct literal must name every field: this literal in `a_missing_denial_inside_a_mount_is_refused_and_one_outside_is_skipped` omits `disable_userns`, and `cargo test -p tinybox-linux` fails with E0063 (missing field `disable_userns` in initializer of `NativeJailCommand`). Add the field so the file compiles.

[RULE] missing-struct-field ·

@tinysweeper tinysweeper Bot added priority: p1 Next. Wrong behaviour a user will hit, or a security weakness behind a condition. and removed priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. labels Nov 12, 2026

@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.

Requesting changes: 3 lane(s) blocking, worst finding is high.

Fix or reply to the findings below and push. The next review clears this automatically once they are gone — you should not need to dismiss anything by hand.

Diagnostics

Total spend: $0.0148

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

Labels

priority: p1 Next. Wrong behaviour a user will hit, or a security weakness behind a condition.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant