Skip to content

Seatbelt deny_read: keep the jail scratch usable under a denied path, and support name/pattern read denials (0.1.36) #89

Description

@M3gA-Mind

Summary

Two gaps in the macOS Seatbelt backend's deny_read (contract 1.20, v0.1.35) stop a host from denying all the credential files it knows about. Target: 0.1.36.

  1. The jail's own scratch directory can't sit under a denied path. A host therefore can't deny its whole config/state folder.
  2. Denials are exact paths only. A host can't deny a credential file name wherever it appears, for example a project's .npmrc.

OpenHuman's host side is tinyhumansai/openhuman#7917, which mitigates a P1 on tinyhumansai/openhuman#7824 for $HOME-level stores. These two items are what's left.

1. Scratch directory vs. deny_read

What happens now:

  • native_shell creates the per-call scratch at <state_dir>/artifacts/sandbox-scratch/shell-*, points TMPDIR/TEMP/TMP at it, and pushes it onto jail.read_write after native_jail::prepare has checked the denials (tinybox-module/src/tinybus_module/native_shell/mod.rs, around lines 58-109).
  • render_profile emits (deny file-read* file-write* (subpath …)) as the profile's last rule, which overrides every write grant above it (tinybox-jail/src/macos.rs, around lines 194-203).

Effect: OpenHuman's state_dir is inside its config folder (~/.openhuman/workspace). Denying that folder would deny the scratch dir, so every jailed command would lose its TMPDIR. The admission check can't catch this, because the grant is added after it runs. So OpenHuman denies only four named credential files and leaves the rest of its state folder readable.

Proposed (either works):

  • (a) Carve the scratch back out. On Seatbelt, re-allow the jail's own scratch dir after the deny_read block, e.g. (allow file-read* file-write* (subpath "<scratch>")) as the final rule.
  • (b) Move the scratch. Create it outside any denied path, such as a module-owned temp root, or refuse a state_dir that a denial covers.

Tests:

  • a deny_read entry that contains state_dir still leaves the scratch dir readable and writable;
  • a sibling file under the denied path is still unreadable.

2. Name/pattern read denials

Today: NativeJailConfig.deny_read takes absolute paths. A host that protects credential names at any depth (OpenHuman's is_always_forbidden matches .ssh, .aws, .npmrc, .netrc, .git-credentials, .pgpass, .docker/config.json, .cargo/credentials.toml as path segments) can only send the $HOME copies. A command can then build $PWD/.npmrc at runtime and read a project registry token, because Seatbelt reads are allow-default.

Proposed:

  • A contract addition (1.21): for example deny_read_names: Vec<String> (single segments) and deny_read_pairs: Vec<[String; 2]> (consecutive segments), both empty by default.
  • Seatbelt renders them as (deny file-read* (regex #"(^|/)\.npmrc(/|$)"))-style rules.
  • Names must be validated and escaped for the SBPL regex: no /, no NUL, and regex metacharacters escaped.
  • Backends that can't express patterns (Landlock, AppContainer) answer Unsupported under Enforced, as the existing path denials do. Landlock is deny-default anyway, so a host sends these only for allow-default backends.

Tests:

  • profile rendering and escaping;
  • a denied name is unreadable at depth 1 and depth 3 under the root;
  • a similar name (.npmrc.bak) stays readable;
  • the grant count limit includes the new fields.

Out of scope

Which extra stores to protect (~/.config/gh, gcloud, browser profiles, agent CLIs) is the host's policy decision, not TinyBox's.

Activity

  1. added
    priority: p2Soon. Real but survivable — a rough edge, a gap, a thing that will bite later.
    on Oct 16, 2026
  2. tinysweeper commented on Oct 16, 2026

    @tinysweeper

    Two gaps in the macOS Seatbelt backend's deny_read: (1) the jail's per-call scratch directory can sit under a denied path because it is granted after admission and the deny block is rendered last, so hosts cannot deny their whole state/config folder without breaking TMPDIR; (2) denials only accept absolute paths, so credential file names (e.g. .npmrc) cannot be denied at any depth on allow-default backends. This is new functionality plus a fix to rule ordering, not an outage; it has a workaround (denying only named files), so p2.

    Labelled priority: p2.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    priority: p2Soon. Real but survivable — a rough edge, a gap, a thing that will bite later.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions