Skip to content

Add Linux NativeJail hard resource enforcement - #103

Merged
senamakel merged 146 commits into
mainfrom
tinybox-native-limits
Oct 24, 2026
Merged

senamakel merged 146 commits into
mainfrom
tinybox-native-limits

Conversation

@senamakel

@senamakel senamakel commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds a TinyBox bus 2.1 native resource request for Linux CPU, memory, and pids limits while preserving the 1.22 and 2.0 request shapes. Linux runs the target inside a systemd transient scope and Bubblewrap namespaces, applies the requested cgroup limits, and refuses unsupported disk, networking, and subprocess-denial requests. The older all-four-dimensions request remains fail-closed.

The trusted supervisor closes inherited non-stdio descriptors, prevents target environment values from reaching the launcher, uses exact CPU quota conversion, and reaps the complete workload scope on normal exit, cancellation, and resource close. Live tests check cgroup values and pressure events, allowed and denied files, manager and inherited-FD escape paths, fractional CPU throttling, output/status events, and detached descendants.

This is Linux NativeJail phase-two progress for tinybox#27; it does not complete the wider epic or claim resource support on macOS/Windows. PR #100's contract and workspace changes are now on main. Keep this PR in draft until TinyBox publishes the compatible native resource contract and the OpenHuman host adapter has been updated for that released contract.

Validation

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • TINYBOX_REQUIRE_NATIVE_RESOURCE_TESTS=1 cargo test -p tinybox-module native_ -- --nocapture (49 passed)
  • TINYBOX_REQUIRE_NATIVE_RESOURCE_TESTS=1 cargo test -p tinybox-module resource_aware_bus_exec_cancellation_reports_events_and_reaps_descendants (1 passed)
  • cargo test --workspace --all-features (passed)
  • scripts/check-coverage.sh (all source files at least 90%)
  • actionlint .github/workflows/ci.yml

A dedicated Ubuntu CI job installs Bubblewrap and starts the runner user's systemd manager, verifies transient scopes, then requires the live native proof tests instead of allowing them to skip.

Adds a Linux-only test asserting the native jail refuses a config with an
explicit disk_bytes limit it cannot enforce. This guards against older serde
behavior silently dropping the unknown field, which would otherwise turn an
unsupported hard limit into an unrestricted native launch.

Auto-committed-on: dragonfly
Introduce NativeJailResourceRequest with optional CPU, memory, pids and
disk limits, and NativeJailRequest extending NativeJailConfig for
contract 2.1 members. Disk is modelled explicitly so backends that
cannot enforce a hard quota must refuse rather than silently ignore it.

Auto-committed-on: dragonfly
Bumps the contract version to 2.1 and exports the new native jail request
types along with the wire name for the resource-aware jailed exec member.
The 2.0 request shapes are left unchanged so existing modules keep binding.

Auto-committed-on: dragonfly
The wire snapshot tests still expected contract version 2.0, so they now assert 2.1 to match the current protocol version.

Auto-committed-on: dragonfly
Add a test asserting that the 2.1 resource request stays additive to the
jail config and that disk_bytes survives serialization explicitly. It also
pins the resource version constant so future bumps are caught.

Auto-committed-on: dragonfly
Introduce supports_native_jail_resources so hosts can verify a module
advertises the resource-aware native jail members before sending a 2.1
request. Tests were updated to cover the new capability check and to keep
the legacy all-resource request failing closed on Linux.

Auto-committed-on: dragonfly
CPU millis were divided by ten as integers, so any quota below ten
millis rounded down to zero and systemd saw no limit at all. The quota
is now formatted with its tenth-percent digit retained.

Auto-committed-on: dragonfly
Introduce NativeJailCommand and native_jail_argv to construct the
systemd-run and bubblewrap launcher for the namespace-backed native
directory jail. The builder validates request shapes and rejects
unsupported dimensions such as subprocess denial rather than silently
rounding or dropping limits.

Auto-committed-on: dragonfly
Re-export NativeJailCommand and native_jail_argv from the sandbox module so
callers can build and inspect native jail invocations without reaching into
private module paths.

Auto-committed-on: dragonfly
Reformatted long expressions, argument lists, and import ordering to satisfy rustfmt, with no behavioural change.

Auto-committed-on: dragonfly
The root directory check now uses a match on the canonicalize result so a
failed canonicalization is treated as invalid instead of relying on the
`is_err_or` helper. A test covers native jail argv generation to confirm
limits, namespace flags, and the target environment are preserved.

Auto-committed-on: dragonfly
Reformatted the native jail command test to satisfy rustfmt, wrapping long
assertions and the systemd delimiter lookup across multiple lines. No test
behaviour changed.

Auto-committed-on: dragonfly
Add an optional native_resources field carrying the Contract 2.1
resource subset for the namespace-backed native jail, leaving the
legacy all-four-dimensions resources field untouched. Preflight now
derives constraints from whichever of the two fields is set.

Auto-committed-on: dragonfly
The validation path previously skipped checks when a jail requested native
resources instead of portable ones, allowing zero or empty resource requests to
reach the backend. Added checks that reject zero values for cpu, memory, pids,
and disk limits, and reject requests where no native resource is set at all.

Auto-committed-on: dragonfly
…quests

Return ConstraintNotEnforced instead of InvalidResourceLimit when a
native backend receives a request with no resource limits set. The empty
request is valid input, so the error should describe the backend's
inability to enforce the constraint rather than blame the caller.

Auto-committed-on: dragonfly
Add a distinct 2.1 subset request path that applies CPU, memory, pids and
disk limits through the trusted namespace launcher while preserving the
existing 2.0 config. The jail now mounts tmpfs over /run and /sys so a
same-UID workload cannot reach a user-scope manager socket or host cgroup
filesystem to escape its transient resource scope.

Auto-committed-on: dragonfly
Introduce a start_jailed_exec_with_resources entry point that wraps the
native jail request in a Resources variant, and thread a NativeJailRequest
enum through the internal start_with_jail and start_with_preparation
helpers so existing callers pass the Legacy variant. This prepares the
spawn path to distinguish resource-backed jails from legacy configs.

Auto-committed-on: dragonfly
Introduce a collector variant that prepares the jail with resource
limits and wires it to the resource backend. The execution stream now
passes the resolved request mutably so the jail can adjust it, and jail
handling uses NativeJailRequest throughout.

Auto-committed-on: dragonfly
Adds the contract 2.1 resource-aware jailed exec member to the bus
contract and module method list, and routes it through the resource
backend. The native jail now binds denied non-directory paths to
/dev/null instead of masking them with a tmpfs, and only applies
MemorySwapMax when a memory limit is set.

Auto-committed-on: dragonfly
The native jail now rejects roots, working directories, and grants whose
canonical paths differ from the requested paths, and refuses to bind over
/run, /sys, or /proc. Network access is always denied via a private network
namespace, which also isolates AF_UNIX abstract sockets and closes the
same-UID user-manager escape path. Tests were updated to use the real
workspace paths instead of the removed /workspace bind.

Auto-committed-on: dragonfly
# Conflicts:
#	crates/tinybox-bus/src/lib.rs
#	crates/tinybox-bus/src/shell.rs
#	crates/tinybox-module/src/tinybus_module/README.md
#	crates/tinybox-module/src/tinybus_module/native_jail.rs
#	crates/tinybox-module/src/tinybus_module/resources_stream.rs
# Conflicts:
#	crates/tinybox-linux/src/sandbox/args.rs
Add a per-execution jail backend that validates the negotiated 2.1
resource request and binds the launcher argv produced from it, so a
spawn is only allowed when the command matches the validated plan.
Resource limits are enforced through the native launcher while the
target environment is carried inside bubblewrap.

Auto-committed-on: dragonfly
Dropped a redundant `std::path::PathBuf` import that was already covered by the combined `std::path::{Path, PathBuf}` import on the next line.

Auto-committed-on: dragonfly
The native jail backend now clones program and argument OsStrings directly instead of converting through raw bytes. The legacy collector call also passes the default jail backend explicitly to match the updated signature.

Auto-committed-on: dragonfly
The expected argument list for the namespace jail test no longer includes
RestrictAddressFamilies, matching the current set of properties passed to
systemd-run.

Auto-committed-on: dragonfly
The resource-aware namespace jail test now reads the launcher's cgroup-v2
membership and asserts the configured memory, swap, pids and CPU limits are
actually applied. A short sleep was added to the jailed script so the cgroup
can be inspected while the process is still alive.

Auto-committed-on: dragonfly
The Unix ownership and mode checks in validate_trusted_program are now
compiled only on Unix targets, with a non-Unix stub returning an
Unsupported error. This keeps the crate building on platforms that lack
the Unix metadata extensions while making the failure explicit.

Auto-committed-on: dragonfly
senamakel and others added 8 commits October 21, 2026 16:09
The sandbox test module is now compiled only on Linux targets, and the
native jail tests call native_jail_argv_with_programs with explicit
systemd-run and bubblewrap paths instead of resolving them internally.
This keeps the tests hermetic and lets them run on hosts where the
trusted launchers are not installed at the usual locations.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
…c/sandbox/mod_tests.rs

Auto-committed-on: cuttlefish
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Auto-committed-on: cuttlefish
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Auto-committed-on: cuttlefish
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Auto-committed-on: cuttlefish
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Auto-committed-on: cuttlefish
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Auto-committed-on: cuttlefish
Co-authored-by: Medulla <medulla@tinyhumans.ai>
@senamakel senamakel closed this Oct 21, 2026
@senamakel senamakel reopened this Oct 21, 2026
@senamakel
senamakel marked this pull request as ready for review October 24, 2026 06:59
@tinysweeper

tinysweeper Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Tiny Sweeper review

⏳ Reviewing 347a49880f35 now. The last completed report, when available, remains below until this pass finishes.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-24T07:03:40.868095Z d344acd Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

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

Diagnostics

Total spend: $0.0034

@tinysweeper tinysweeper Bot added the priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. label Oct 24, 2026
Co-authored-by: Medulla <medulla@tinyhumans.ai>
@senamakel
senamakel merged commit 858e31d into main Oct 24, 2026
16 checks passed

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d344acd789

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

"--noprofile".to_owned(),
"--norc".to_owned(),
"-c".to_owned(),
"[ -d /proc/self/fd ] || exit 125; shopt -s nullglob; fdpaths=(/proc/self/fd/*); ((${#fdpaths[@]} > 0)) || exit 125; for fdpath in \"${fdpaths[@]}\"; do fd=${fdpath##*/}; case $fd in 0|1|2|*[!0-9]*) continue;; esac; eval \"exec ${fd}>&-\" || exit 125; done; unit=; for arg in \"$@\"; do case $arg in --unit=*) unit=${arg#--unit=};; esac; done; [ -n \"$unit\" ] || exit 125; \"$@\"; status=$?; if ! /usr/bin/systemctl --user stop \"$unit\"; then /usr/bin/systemctl --user is-active --quiet \"$unit\" && exit 125; fi; exit \"$status\"".to_owned(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep target arguments out of cleanup-unit selection

For resource-aware executions, the supervisor scans every launcher argument for --unit=, including target argv and environment values after Bubblewrap's separator. A target argument such as --unit=unrelated.service therefore replaces the generated scope name, and the trusted host-side supervisor subsequently stops that user service. Executing the unit-selection prefix confirms this substitution; systemctl --help describes stop UNIT... as “Stop (deactivate) one or more units”. This gives target-controlled data access to a host-manager operation despite the namespace restrictions. Pass the generated cleanup unit separately instead of rediscovering it in untrusted arguments.

AGENTS.md reference: AGENTS.md:L48-L52

Useful? React with 👍 / 👎.

Comment on lines +344 to +347
.any(|path| {
["/run", "/sys", "/proc"]
.iter()
.any(|protected| path.starts_with(protected))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject grants that replace the protected filesystem root

A resource-aware request with read_only: ["/"] passes this validation because / does not start with /run, /sys, or /proc. Its bind is emitted after the private mounts, restoring access to the host's user-manager socket and defeating the resource-scope escape protection. The Bubblewrap documentation confirms filesystem operations are “applied in the order they are given”. Unlike the existing jail builder's replaces_private_mount check, this validator does not reject /. Reject grants that cover protected mounts, not only paths underneath them.

AGENTS.md reference: AGENTS.md:L48-L52

Useful? React with 👍 / 👎.

Comment on lines +462 to +466
for (source, destination, mode) in request
.read_only
.iter()
.map(|path| (path, path, "--ro-bind"))
.chain(request.read_write.iter().map(|path| (path, path, "--bind")))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Apply nested read-only grants after their writable ancestors

When a read-only grant is nested inside an additional writable grant, this loop mounts the read-only child first and then overlays its writable parent, making the supposedly protected child writable. For example, read_only=[/data/vendor] and read_write=[/data] permit modifications to /data/vendor. The Bubblewrap documentation states filesystem operations are “applied in the order they are given as arguments”. The existing jail builder sorts mounts shallowest-first and already has a regression test for this exact grant combination; preserve that ordering in the resource-aware builder too.

AGENTS.md reference: AGENTS.md:L48-L52

Useful? React with 👍 / 👎.

Comment on lines +490 to +491
for (key, value) in &request.env {
argv.extend(["--setenv".into(), key.clone(), value.clone()]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Transport target environment values outside launcher argv

For resource-aware requests containing credentials in the target environment, these --setenv arguments place the credential values directly into the host-side Bash, systemd-run, and Bubblewrap command lines. Other host users can read those values through /proc/<pid>/cmdline on ordinary Linux procfs configurations, even though the variables are installed only inside the sandbox. The existing Bubblewrap launcher explicitly avoids this exposure by transporting arguments through a private file and --args descriptor (crates/tinybox-jail/src/bwrap/mod.rs, “Environment and credentials”). Reuse an equivalent protected argument transport for this launcher.

Useful? React with 👍 / 👎.

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