Skip to content

feat(jail): deny reads of host-chosen paths in native jails (contract 1.20) - #87

Open
M3gA-Mind wants to merge 2 commits into
tinyhumansai:mainfrom
M3gA-Mind:feat/native-jail-deny-read
Open

M3gA-Mind wants to merge 2 commits into
tinyhumansai:mainfrom
M3gA-Mind:feat/native-jail-deny-read

Conversation

@M3gA-Mind

@M3gA-Mind M3gA-Mind commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This adds NativeJailConfig::deny_read (contract 1.20): absolute paths a jailed child may neither read nor write, even inside the root or a grant.

Why it's needed: tinyhumansai/openhuman#7824 accepts Seatbelt as folder confinement for the macOS desktop sandbox, where writes are allowed only in the selected folder. Review flagged a P1 there. Seatbelt reads are allow-default, so with network on a command can read ~/.ssh, ~/.gnupg, ~/.aws or Keychains and exfiltrate them. The jail request had no way to deny reads, so per openhuman's submodule-ownership rule the capability lands here first.

What changes:

  • Seatbelt profile: it now ends with (deny file-read* file-write* (subpath …)) over the denied paths. It comes after the write allows, which it overrides, and subpath matches a single file as well as a tree.
  • Module admission (tinybus_module/native_jail.rs):
    • Denials are validated like grants and count against MAX_NATIVE_JAIL_GRANTS.
    • Absent paths are dropped. Existing ones are canonicalized, so a symlinked store is denied at its target.
    • A denial that is the root, an ancestor of it, or covers a writable grant is refused as INVALID_FILE_PATH.
    • A denial inside the root or a grant is refused as unavailable when the backend cannot subtract from grants.
  • JailBackend::carves_denials(): new method, default false, and only Seatbelt returns true. Landlock and AppContainer are allow-list backends, so a path outside every grant is already unreachable there.

Related issue

Part of tinyhumansai/openhuman#7819 (host PR: tinyhumansai/openhuman#7824).

API or behavior changes

These changes are additive and not breaking:

  • NativeJailConfig gains deny_read: Vec<String>. It is serde-default and omitted when empty, so 1.19 requests decode and re-encode unchanged. Rust struct literals need the new field.
  • Jail gains pub deny_read: Vec<PathBuf>, which Jail::new initialises and Jail::canonicalize covers.
  • JailBackend gains carves_denials(), which has a default implementation.
  • CONTRACT_VERSION is now (1, 20). The new spec is docs/specs/contract-1-20.md.

Validation

Commands actually run, with their outcome:

  • cargo fmt --all -- --check: clean
  • cargo clippy --all-targets --all-features -- -D warnings: not run locally (fleet rule: no local lint/test). I ran cargo check -p tinybox-module -p tinybox-jail -p tinybox-bus --tests on macOS instead, and it finished clean.
  • cargo build --all-targets --all-features: not run (see above)
  • cargo test --all-features: not run locally

module-ci runs on this PR. Run 38693710696 at a8f8ccf caught a clippy lint and a Windows-only test gate, both fixed in a66ff39.

Tests

  • tinybox-bus/src/jail_tests.rs: deny_read defaults empty, a 1.19 request round-trips byte-for-byte, and a non-empty list round-trips.
  • tinybox-bus/src/lib_tests.rs: the contract version snapshot is 1.20.
  • tinybox-jail/src/macos_tests.rs: with no denials the profile has no file-read* rule. With denials there is one deny file-read* file-write* block after the write allows, listing every path. Seatbelt carves denials.
  • tinybox-module/src/tinybus_module/native_jail_tests.rs:
    • Denials outside every grant are kept on any backend, canonicalized, with the absent path dropped.
    • Inside a read grant they are refused unless the backend carves.
    • A denial over the root, its ancestor or a writable grant is refused.
    • Malformed denials are refused.
  • Existing NativeJailConfig literals in tests got deny_read: Vec::new().

Deliberately not tested: Landlock and AppContainer spawning with denials. They are unchanged and never grant a denied path.

Documentation

  • New docs/specs/contract-1-20.md.
  • tinybus_module/README.md gets the version line and the StartJailedExec admission paragraph.

After merge, in openhuman (not in this PR):

  1. Module release and repin.
  2. #7824's host side: jail_request fills deny_read for folder_confined from the same credential floor is_always_forbidden uses (SENSITIVE_COMPONENTS, expanded against $HOME), plus Keychains and the single-file stores.

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

… 1.20)

A Seatbelt jail is allow-default for reads, so a folder-confined command
could read the user's credential stores and, with network on, send them
out. The jail request had no way to say otherwise.

Add NativeJailConfig::deny_read (serde default, omitted when empty) and
bump the contract to 1.20. The module validates denials like grants,
drops paths that do not exist, canonicalizes the rest, and refuses a
denial that covers the root or a writable grant, or that lies inside the
root or a grant on a backend that cannot subtract from grants
(JailBackend::carves_denials, false by default). Seatbelt ends its
profile with a file-read*/file-write* deny over the paths.

Part of tinyhumansai/openhuman#7819
@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

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

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 708f80e0-a442-46d9-b7fb-3e38f103b4fa
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

M3gA-Mind added a commit to M3gA-Mind/openhuman that referenced this pull request Oct 15, 2026
Document the two postures jail_isolation picks, who each is accepted
for, and the known limitation of folder confinement: Seatbelt reads are
allow-default, so credential stores stay readable until TinyBox's
deny_read (tinyhumansai/tinybox#87) is released and pinned.
clippy::assert-is-empty rejects assert!(vec.is_empty()) in the bus test;
compare against an empty Vec instead. The module's denial test called
prepare(), which refuses every native jail off Unix, so it failed on
Windows; gate it on cfg(unix) like the other prepare tests.
@tinysweeper

tinysweeper Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Tiny Sweeper review

⚠️ Review failed for a66ff39f9886. the review of #87 did not finish within 900s

M3gA-Mind added a commit to M3gA-Mind/openhuman that referenced this pull request Oct 15, 2026
Document the two postures jail_isolation picks, who each is accepted
for, and the known limitation of folder confinement: Seatbelt reads are
allow-default, so credential stores stay readable until TinyBox's
deny_read (tinyhumansai/tinybox#87) is released and pinned.
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.

1 participant