fix(jq): replace halt native to stop sandbox-escape via process::exit#1589
Merged
Conversation
Closes #1571. jaq-std 3.0's `halt(N)` native calls `std::process::exit(exit_code)`, and its `defs.jq` wraps that with `def halt: halt(0);` and `def halt_error(...): ..., halt(...);`. Untrusted jq filters can therefore terminate the entire embedding process — escaping the `ExecResult`-based sandbox boundary and causing process-wide DoS across tenants. Strip the upstream `halt` native from `jaq_std::funs::<D>()` and chain in a safe replacement that pops the variable argument and returns a normal jq runtime error ("halt is disabled in the bashkit sandbox"). The wrapper defs (`halt`, `halt_error`) still resolve, so callers see exit code 5 with that message instead of the host being killed. Tests: - halt_does_not_terminate_host_process - halt_with_arg_does_not_terminate_host_process - halt_error_does_not_terminate_host_process The tests reach their assertions (instead of the test harness being killed by std::process::exit), which is the actual regression guard. Spec: TM-INF-023 added to specs/threat-model.md.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
bashkit | aefa2a1 | Commit Preview URL Branch Preview URL |
May 07 2026, 01:31 PM |
The static lint test `no_debug_fmt_in_builtin_source` walks every
builtin source file and rejects `{:?}` formatting (no `// debug-ok`
annotation). The new halt regression tests used `{:?}` to render
`stdout`/`stderr` in their `assert_ne!` failure messages, tripping
the lint and failing the Test/Coverage/Check CI jobs.
Switch to `<{}>` (Display, with delimiters for readability) — same
pattern used by the surrounding tests (e.g. line 269: "stderr: {}").
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1571.
Summary
jaq_std'shalt(N)native callsstd::process::exit(exit_code). Becausedefs.jqwraps it withdef halt: halt(0);anddef halt_error(...): ..., halt(...);, an untrusted jq filter can runhaltorhalt_errorand tear down the entire embedding process — asandbox escape via DoS for any caller hosting bashkit.
Fix
Strip the upstream
haltnative fromjaq_std::funs::<D>()and chainin a safe replacement that pops the variable argument and returns
Error::str("halt is disabled in the bashkit sandbox"). The wrapperdefs (
halt,halt_error) still resolve, so callers see a normal jqruntime error (exit 5) instead of the host being killed.
Removing
haltoutright was the obvious first attempt, but it breaksloading of
defs.jqbecausedef halt: halt(0);anddef halt_error: ...reference the native — every jq invocation wouldfail with "halt/1 is not defined", including unrelated filters like
@tsv. Replacing the native keeps the surface working.Tests
halt_does_not_terminate_host_processhalt_with_arg_does_not_terminate_host_processhalt_error_does_not_terminate_host_processThe tests reach their assertions at all (instead of the test harness
being killed by
std::process::exit), which is the actual regressionguard. All 221 jq unit tests pass.
Spec
specs/threat-model.md: new rowTM-INF-023covering this threatand its mitigation.
Test plan
cargo test -p bashkit --lib --features jq builtins::jq(221/221)cargo fmt --all -- --checkcargo clippy -p bashkit --features jq --all-targets -- -D warningsGenerated by Claude Code