Skip to content

fix(jq): replace halt native to stop sandbox-escape via process::exit#1589

Merged
chaliy merged 2 commits into
mainfrom
fix/issue-1571-jq-halt-block
May 7, 2026
Merged

fix(jq): replace halt native to stop sandbox-escape via process::exit#1589
chaliy merged 2 commits into
mainfrom
fix/issue-1571-jq-halt-block

Conversation

@chaliy
Copy link
Copy Markdown
Contributor

@chaliy chaliy commented May 7, 2026

Closes #1571.

Summary

jaq_std's halt(N) native calls std::process::exit(exit_code). Because
defs.jq wraps it with def halt: halt(0); and
def halt_error(...): ..., halt(...);, an untrusted jq filter can run
halt or halt_error and tear down the entire embedding process — a
sandbox escape via DoS for any caller hosting bashkit.

Fix

Strip the upstream halt native from jaq_std::funs::<D>() and chain
in a safe replacement that pops the variable argument and returns
Error::str("halt is disabled in the bashkit sandbox"). The wrapper
defs (halt, halt_error) still resolve, so callers see a normal jq
runtime error (exit 5) instead of the host being killed.

Removing halt outright was the obvious first attempt, but it breaks
loading of defs.jq because def halt: halt(0); and
def halt_error: ... reference the native — every jq invocation would
fail with "halt/1 is not defined", including unrelated filters like
@tsv. Replacing the native keeps the surface working.

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 at all (instead of the test harness
being killed by std::process::exit), which is the actual regression
guard. All 221 jq unit tests pass.

Spec

  • specs/threat-model.md: new row TM-INF-023 covering this threat
    and its mitigation.

Test plan

  • cargo test -p bashkit --lib --features jq builtins::jq (221/221)
  • cargo fmt --all -- --check
  • cargo clippy -p bashkit --features jq --all-targets -- -D warnings

Generated by Claude Code

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.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 7, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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: {}").
@chaliy chaliy merged commit efcfef6 into main May 7, 2026
33 of 34 checks passed
@chaliy chaliy deleted the fix/issue-1571-jq-halt-block branch May 7, 2026 13:44
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.

DeepSec: untrusted jq filters can terminate the host process via jaq-std halt

1 participant