Skip to content

Run Windows jails under AppContainer through the owned-process backend - #164

Open
M3gA-Mind wants to merge 4 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/22-windows-appcontainer
Open

M3gA-Mind wants to merge 4 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/22-windows-appcontainer

Conversation

@M3gA-Mind

Copy link
Copy Markdown
Collaborator

Summary

On Windows every jail request was refused, so the AppContainer backend was never used. AppContainer can run work only through the owned-process API, because it can't hand back a std::process::Child. So default_backend() is "unsupported" on Windows by design. But the module used default_backend() for every jailed path, and native_jail::prepare refused any non-Unix host outright.

  • The native shell, StartJailedExec and stdio paths take default_owned_backend(). Off Windows that picks the same backend as before: only AppContainer overrides is_owned_available. So nothing changes on Linux or macOS.
  • prepare admits a backend that is owned-available, instead of requiring Unix plus is_available.
  • StdioSession spawns through spawn_owned_captured_with and holds an OwnedChild. Its pipes are converted back into standard child pipes, so the async plumbing is unchanged. ChildStatus now carries the signal, so a signalled stdio child still reports StdioExit::Signal on Unix.
  • JailStatus reports the owned backend and its owned availability.

What AppContainer can't enforce stays refused, now with the reason in the message. The error codes are unchanged:

  • allow_subprocess: false: "cannot enforce subprocess";
  • require_process_isolation: AppContainer reports no isolation level;
  • read denials overlapping the root, a read-only grant or a writable grant: AppContainer can't carve denials.

Related issue

Closes #22. This corrects our earlier comment there that this "looks done": the backend existed, but nothing selected it.

API or behavior changes

  • Windows: jailed native shell, jailed exec and jailed stdio now run under AppContainer, where they used to be refused.
  • ChildStatus::signal() and ChildStatus::with_signal() are new; from_code is unchanged.
  • Native-jail refusals keep their codes. Their messages now say why, with the old wording as a prefix.

Validation

  • Linux, on our Linux CI runner at a7ca4f5:
    • 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-jail -p tinybox-module -p tinybox-host: 617 passed, 0 failed, with no failures unique to this branch.
    • stdio tests repeated 10 times: all passed.
    • Per-file line coverage: stdio.rs 93.00%, native_jail.rs 92.34%, tinybox-jail/src/jail.rs 92.99% (gate 90%).
    • Red controls, each restored afterwards:
      • restoring main's is_available gate fails a_backend_usable_only_through_the_owned_api_is_admitted ("no native jail backend is available on this host");
      • dropping the signal from ChildStatus fails an_owned_child_ended_by_a_signal_reports_the_signal.
  • Windows proof: this PR's windows-latest jobs, including run_native_shell_runs_a_jailed_command_under_appcontainer_on_windows and on_windows_the_owned_selection_is_appcontainer.
  • macOS: this PR's macOS jobs.

Tests

  • off_windows_the_owned_selection_is_the_plain_selection and on_windows_the_owned_selection_is_appcontainer check backend selection on each side.
  • ChildStatus carries no signal from a code, and an owned child killed by SIGKILL reports signal 9 (Unix).
  • An AppContainer-shaped fake backend (owned-only, no Subprocess, no isolation, no carving), run on every OS:
    • a default MCP-style jail is admitted;
    • a jail with allow_subprocess: false is refused naming subprocess, for both enforcement modes;
    • require_process_isolation is refused saying so;
    • a denial overlapping a read-only grant is refused with "cannot carve denials";
    • a denial inside a writable grant is refused with "cannot carve a denial".
  • Windows: a jailed echo through RunNativeShell under AppContainer exits 0 with its output.
  • Red controls: main's admission gate fails the owned-only admission test; dropping the signal fails the signal test.

Documentation

The module README's native-jail section, plus default_owned_backend rustdoc.

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

On Windows every jail request was refused. AppContainer can run work only
through the owned-process API (it cannot return a std::process::Child), so
default_backend() is "unsupported" there by design. But the module used
default_backend() for every jailed path, and native_jail::prepare refused
any non-Unix host outright.

- The native shell, StartJailedExec and stdio paths take
  default_owned_backend(). Off Windows it picks the same backend as before
  (only AppContainer overrides is_owned_available), so nothing changes there.
- prepare admits a backend that is owned-available instead of requiring
  Unix plus is_available.
- StdioSession spawns through spawn_owned_captured_with and holds an
  OwnedChild. Its pipes are converted back into std child pipes, so the
  async plumbing is unchanged. ChildStatus now carries the signal, so a
  signalled stdio child still reports StdioExit::Signal on Unix.
- JailStatus reports the owned backend and its owned availability.

What AppContainer cannot enforce stays refused, now with the reason in
the message (same error codes): a jail with allow_subprocess false
("cannot enforce subprocess"), require_process_isolation (it reports no
isolation), and read denials overlapping the root, a grant or a writable
grant (it cannot carve denials).

Closes tinyhumansai#22
@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: 3210ebdd-8423-48c1-b9ba-3041e7821122
  • 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: fafa86cc1b81e53100011c3a4c526a1490813648

Verdict: Ready for maintainer review · Priority: none
Coverage: Reviewed 15 of 15 files.
Pull request purpose (title): Run Windows jails un…

Diagnostics and supporting details

Full summary

Pull request purpose (title): Run Windows jails under AppContainer through the owned-process backend. Purpose (title and description): run Windows jails under AppContainer through the owned-process backend. Previously every Windows jail request was refused, because AppContainer can only run work through the owned-process API and cannot return a `std::process::Child`, while the module used `default_backend()` everywhere and `native_jail::prepare` refused any non-Unix host. The pull request routes every jailed path (native shell, `StartJailedExec`, stdio) through `default_owned_backend()`, admits backends that are owned-available in `prepare`, moves `StdioSession` onto `OwnedChild` with pipe conversion and `ChildStatus`-based signal reporting, reports the owned backend in `JailStatus`, adds signal plumbing to `ChildStatus`, and gives refusals specific reasons while keeping their error codes. It also adds cmd.exe verbatim `/C` argument handling in the Windows command-line builder, which the description does not call out. Off Windows the selected backend is unchanged. Latest push delta: unavailable — the review state has the prior reviewed head and rendered evidence but no trusted base-to-head diff comparison, so the whole pull-request diff above is not a push delta.

Review snapshot

Change surface Files Review signal Count
Production 9 Active findings 0
Tests 5 Noted findings 0
Documentation 1 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

15 files changed, 462 insertions, 62 deletions.

Features

  • Added — `ChildStatus::signal()` / `with_signal()`: `ChildStatus` gains an optional signal field plus a builder and accessor, populated from `ExitStatusExt::signal()` on Unix, so a signalled child is distinguishable from an exit code. `from_code` behaviour is unchanged. (crates/tinybox-jail/src/jail.rs impl ChildStatus {, crates/tinybox-jail/src/jail.rs impl OwnedProcess for StdOwnedProcess {)
  • Modified — Owned backend for every jailed path: Native shell, `StartJailedExec` and stdio now select `default_owned_backend()` instead of `default_backend()`; on Windows that is AppContainer, and off Windows it is the same backend the plain selection picks, so behaviour there is unchanged. `default_owned_backend` rustdoc is updated to state this. (crates/tinybox-module/src/tinybus_module/native_shell/mod.rs impl ShellContext {, crates/tinybox-module/src/tinybus_module/resources_stdio.rs impl Resources {, crates/tinybox-module/src/tinybus_module/resources_stream.rs fn select_collector(, crates/tinybox-jail/src/lib.rs pub fn default_backend() -> Arc<dyn JailBackend> {)
  • Modified — `prepare` admits owned-available backends: The Unix-only plus `is_available()` gate is replaced by `is_owned_available()`, so a backend usable only through the owned-process API is admitted; the noop backend is still refused. (crates/tinybox-module/src/tinybus_module/native_jail.rs pub(super) fn prepare()
  • Modified — Refusal messages with reasons: Refusals keep `UNSUPPORTED_OPERATION` / `INVALID_FILE_PATH` codes but now name the cause (unavailable backend, denial overlapping root or grants, backend that cannot carve denials, process-isolation requirement, unenforceable constraints) via `refused`, `invalid_because` and `cannot_enforce`, so hosts branching on the code are unaffected. (crates/tinybox-module/src/tinybus_module/native_jail.rs fn unavailable() -> Error {, crates/tinybox-module/src/tinybus_module/native_jail.rs fn required_resource_constraints(, crates/tinybox-module/src/tinybus_module/native_jail.rs pub(super) fn prepare()
  • Modified — `JailStatus` reports the owned backend: `jail_status` now reports `default_owned_backend()` and `is_owned_available()`, so Windows reports AppContainer as the backend and as available instead of "unsupported". (crates/tinybox-module/src/tinybus_module/mod.rs impl BoxService {)
  • Modified — `StdioSession` on `OwnedChild`: Stdio spawns through `spawn_owned_captured_with`, holds an `OwnedChild`, converts owned pipes back to standard child pipes so the async pumps are unchanged, derives the process group from `local_process_group_id` (0 when the backend owns its own grouping, as a Windows Job Object does), terminates via `terminate()`, and maps `ChildStatus` to `StdioExit::Signal`/`Code`/`Unknown`. (crates/tinybox-module/src/tinybus_module/stdio.rs impl StdioSession {, crates/tinybox-module/src/tinybus_module/stdio.rs pub(super) struct StdioSession {, crates/tinybox-module/src/tinybus_module/stdio.rs fn terminate(group: i32, child: &mut Child) {, crates/tinybox-module/src/tinybus_module/stdio.rs async fn reap(child: Arc<StdMutex<Option<Child>>>, shared: Arc<Shared>, group: i, crates/tinybox-module/src/tinybus_module/stdio.rs async fn pump(mut pipe: impl AsyncRead + Unpin, shared: Arc<Shared>, which: Pipe, crates/tinybox-module/src/tinybus_module/stdio.rs fn drain(buffers: &mut Buffers, max: usize) -> StdioChunk {)
  • Internal refactor — `reject_planted_links` extraction: The symlinked-grant rejection moved into a named helper called before canonicalization, preserving the raw-spelling ordering the check depends on. (crates/tinybox-module/src/tinybus_module/native_jail.rs pub(super) fn prepare()
  • Removed — Unix-only and `is_available()` gate in `prepare`: The unconditional non-Unix refusal and the plain-availability requirement are removed in favour of owned availability; Windows hosts are no longer refused outright. (crates/tinybox-module/src/tinybus_module/native_jail.rs pub(super) fn prepare()
  • 1 additional supported feature(s) omitted by the configured limit.

Tests

  • integration — A jailed `echo` through `RunNativeShell` under AppContainer exits 0 with `jailed` on stdout, and `jail_status` reports the owned backend.: The tests lane answered that the owned-backend switch is covered by the admission test plus a self-consistent `jail_status` test; the tests lane also noted the stdio rewrite to `OwnedChild` is not covered by any test in this diff but found no concrete misbehaving input. The Windows tests are cfg-gated and this review did not execute them. (crates/tinybox-module/src/tinybus_module/mod_tests.rs, crates/tinybox-module/src/tinybus_module/mod_tests.rs async fn run_native_shell_returns_finished_output_after_module_cleanup())

Findings

No active actionable findings.

Before merge

None.

How this fits together

flowchart LR
  n0["..._when_no_candidate_can_return_owned_child<br/>changed"]:::changed
  n1["ChildStatus<br/>changed"]:::changed
  n2["default_backend<br/>changed"]:::changed
  n3["wait_for_child<br/>changed"]:::changed
  n4["JailBackend"]:::impacted
  n5["clone"]:::impacted
  n6["spawn"]:::impacted
  n7["io"]:::impacted
  n8["prepare"]:::impacted
  n0 -->|uses| n4
  n2 -->|uses| n4
  n2 -->|calls| n5
  n3 -->|uses| n1
  n3 -->|uses| n7
  n4 -->|uses| n7
  n6 -->|uses| n4
  n6 -->|calls| n5
  n8 -->|uses| n4
  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
  • Positive: The critique lane is answered, but its findings list is empty while its summary text mentions one finding; that finding is not present in the supplied evidence and is not reproduced here.
  • Lane summary: Reviewed 8 files; 1 finding. _The code index is behind this pull request (indexed at `38deeb420619`), so retrieved context may be out of date._

security

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The security lane answered with 0 findings across 14 reviewed files; `crates/tinybox-module/src/tinybus_module/README.md` was not security-reviewed as prose.
  • Lane summary: Reviewed 7 files; 0 findings. 1 file was not security-reviewed: crates/tinybox-module/src/tinybus_module/README.md (prose or tabular data). _The code index is behind this pull request (indexed at `38deeb420619`), so retrieved context may be out of date._

tests

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The tests lane answered that the substantial new behaviour carries direct tests: the `/C` verbatim tail and the narrow-quoting guard, the signal field with a real SIGKILL child, the refusal reason strings, and the owned-only backend admission. It also noted the stdio `OwnedChild` rewrite has no direct test in this diff, but found no concrete misbehaving input and therefore no defect.
  • Lane summary: No findings. The substantial new behaviour is the cmd.exe verbatim tail in `build_command_line` and the `ChildStatus` signal field, and both carry direct tests: `a_cmd_script_after_slash_c_reaches_cmd_verbatim` fails if the `/C` pass-through stops, `other_programs_keep_command_line_to_argv_quoting` pins that the change is narrow, and `an_owned_child_ended_by_a_signal_reports_the_signal` actually kills a child with SIGKILL and asserts both `signal() == Some(9)` and `code() == None`, so it cannot pass vacuously. The preflight message changes are pinned by tests that match on the reason text, and the owned-backend switch is covered by `a_backend_usable_only_through_the_owned_api_is_admitted` (owned-only backend with `is_available() == false` is admitted) plus the self-consistent `jail_status` test. _The code index is behind this pull request (indexed at `38deeb420619`), so retrieved context may be out of date._

commits

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

description

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: The new commits implement the `ChildStatus` signal plumbing that the description already announced (previously the diff changed `exit_of` to call `status.signal()` without the method existing, so this closes that compile gap), add a cmd.exe verbatim-argument path with tests, the Windows AppContainer end-to-end test, the ownership-shaped fake-backend admission tests, and update `JailStatus`/README. The description covers the behavior changes, the new `ChildStatus::signal`/`with_signal` API, and the Windows test; the only notable unreported addition is the `build_command_line` cmd.exe handling, which is a mechanical enabler of the jailed-shell path. Looks accurate; I found no defect. _The code index is behind this pull request (indexed at `38deeb420619`), so retrieved context may be out of date._
Evidence and run details
  • Models: ladder/vectors-oai3, deepseek-v4.1-flash
  • Spend: $0.010455
  • Tokens: 1198026 input · 16167 output · 430464 cached · 770 embedding
  • Updated: 2026-11-12T07:32:31Z
  • Continuity: summary cache chain restarted at the storage ceiling.
Head State Pass summary
a7ca4f5ad80b incomplete 0 active finding(s), 0 resolved finding(s) (at 2026-11-11T13:04:12Z)
54737bf3e1bc ready for maintainer review 0 active finding(s), 0 resolved finding(s) (at 2026-11-11T14:15:56Z)
fafa86cc1b81 ready for maintainer review 0 active finding(s), 0 resolved finding(s) (at 2026-11-12T07:32:31Z)

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 could not complete coverage (7 unanswered item(s)), so this is not an approval.

Diagnostics

Total spend: $0.0377

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

# Conflicts:
#	crates/tinybox-module/src/tinybus_module/native_jail_tests.rs
…il status

Under AppContainer the native shell's cmd invocation failed: the spawner
re-quoted every argument by CommandLineToArgvW rules, so the script cmd was
handed as an already-quoted raw argument reached it as \"script\" and ran
as a program name. cmd takes everything after /C verbatim, so for cmd.exe
those arguments now pass through unchanged.

jail_status_reports_the_detected_backend_without_starting_a_process still
compared against the legacy selection, which is "unsupported" on Windows;
it now checks the owned backend jail_status reports.
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.

tinybox-jail: Windows AppContainer backend cannot be enabled (unsafe policy, no waitable Child)

1 participant