You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 371fb27
Browse filesBrowse the repository at this point in the historyBrowse files
fix(builtins): markkit/fetchkit respect the exec deadline off the async worker (#2870)
The markkit and fetchkit CLIs run on the blocking pool, and their timeouts are capped at the remaining exec budget, so a large document can no longer outlive the exec timeout or stall co-tenants on the worker (TM-DOS-143).
Copy file name to clipboardExpand all lines: crates/bashkit/docs/threat-model.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -35,6 +35,7 @@ through configurable limits.
35
35
| Pagination amplification (TM-DOS-137) | Long `pr -h` headers repeated on many short pages | Check the 16 MiB cap before every append; lease input, rows and output capacity before growth; borrow cells/page slices and retain date timestamps; charge input/work and poll cancellation/deadlines, including skipped pages | MITIGATED |
36
36
| Embedded CLI limit escalation (TM-DOS-139) |`markkit extract x --max-input-size 99999999999`|`markkit` limits can only be lowered below their defaults; `fetchkit` bodies capped by HTTP limits; `--ocr` refused | MITIGATED |
37
37
| Tree-walk memory (TM-DOS-140) |`find`, `du` or `rg` over a tree with millions of files |`FileSystem::walk` answers are bounded batches (10k entries) refilled as `find`/`du` descend; `rg` searches each file as it reads it without holding the text, charges it to the aggregate-input budget, stops at the first refusal and streams its walk; `ls -R` stops at the stdout limit | MITIGATED |
38
+
| Extraction outlives deadline (TM-DOS-143) |`markkit extract` on a huge document under a short exec timeout | markkit/fetchkit run off the async worker; their timeouts are capped at the remaining exec budget | MITIGATED |
38
39
| Root real mount drops limits (TM-DOS-144) | Writing past `filesystem_limits` with a host dir mounted at `/`| The root overlay keeps the configured `FsLimits`| MITIGATED |
Copy file name to clipboardExpand all lines: knowledge/log.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,6 +2,7 @@
2
2
3
3
## 2026-11-06
4
4
5
+
***markkit/fetchkit honor the exec deadline**: a security audit found `markkit extract` on a large document ran synchronously on the tenant's async worker, so a 300 ms `exec` timeout returned after 7+ s and co-tenants on that worker stalled. Both builtins now run on tokio's blocking pool, and their timeouts are capped at the remaining `exec` budget (302 ms in the regression test). See [markkit/fetchkit builtins](runtimes/markkit-fetchkit-builtins.md), TM-DOS-143.
5
6
***`json.dumps` indent preflight**: a security audit found `json.dumps([1], indent=10**12)` (and huge string indents or separators on deep or long data) allocated past the Python memory limit unchecked; the metered allocator cannot refuse, so one tenant aborted the process. The Monty fork now preflights the indent repeat and every separator/indent write, raising `MemoryError`. See [Threat Model](security/threat-model.md), TM-PY-038.
6
7
***Re-entry depth and on-demand stack growth**: a security audit found three ways one tenant could overflow the native stack and abort the shared process: `f(){ eval f; }` and `compgen -C` (plus `fc` and PROMPT_COMMAND) re-entered the interpreter with no depth count; `./s.sh` running itself skipped the `sh s.sh` child-shell guard; and the product of capped nesting kinds (compounds x functions x eval) still exceeded a 2 MiB release worker stack. Re-entry now counts against `max_function_depth` and `MAX_REENTRY_DEPTH` (8), path-invoked scripts take the child-shell guard, and `execute_command` grows the stack on the heap through `stacker` (already in the graph via Monty; not on wasm32). See [Threat Model](security/threat-model.md), TM-DOS-125, TM-DOS-141, TM-DOS-142.
7
8
***Real mount hardening**: a security audit found `chmod 4755` on a read-write real mount set setuid on the host file, and a host dir mounted at `/` lost the configured `filesystem_limits` (the root overlay used defaults). `RealFs::chmod` now passes only `mode & 0o777`, and the root overlay keeps the base limits. See [Threat Model](security/threat-model.md), TM-ESC-010, TM-DOS-144.
| URL egress |`HttpClient` per hop, redirects followed here | fetchkit `HttpTransport` over `HttpClient`|
51
-
| limits |`--max-*`/`--timeout` may lower, never raise, markkit defaults | bashkit `HttpLimits` cap every body |
51
+
| limits |`--max-*`/`--timeout` may lower, never raise, markkit defaults; `timeout` also capped at the remaining `exec` deadline | bashkit `HttpLimits` cap every body; each request's timeout capped at the remaining `exec` deadline |
52
+
| scheduling | runs on tokio's blocking pool (`Handle::block_on`), so the `exec` timeout cancels the caller and co-tenants keep the async worker (TM-DOS-143) | same |
Copy file name to clipboardExpand all lines: knowledge/security/threat-model.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -314,6 +314,7 @@ on default 2 MiB test threads (TM-DOS-044).
314
314
| TM-DOS-138 | Join matching-row output growth | Equal-key row groups multiply generated output and synchronous work before dispatcher capture | Parsed rows, fields, keys, decoded input and generated output own shared-budget capacity before growth. VFS reads charge aggregate input and admit metadata size; output fields append directly without temporary row storage, and the byte-stream copy is admitted. Parsing, grouping (including suppressed matches), comparisons and emissions charge work and poll cancellation/deadlines. Errors abort the request. Ordinary two-row fixtures cover memory/work rejection, cancellation/deadlines, session recovery, and complete pipeline/redirect output |**MITIGATED**|
315
315
| TM-DOS-139 | Embedded CLI limit escalation |`markkit extract x --max-input-size 99999999999 --timeout 1e9` buys a larger CPU/memory budget in a shared process | Host clamps every markkit limit to its default after flags (`CliHost::limits`); fetchkit bodies capped by `HttpLimits`; OCR (host binaries) refused |**MITIGATED**|
316
316
| TM-DOS-140 | Tree-walk memory on huge trees | A tenant mounts or creates a tree with millions of files and runs `find`, `du` or `rg` over it; a bulk prefetch of every listing, or rg collecting every file's text before charging the input budget, holds the whole tree in memory | `FileSystem::walk` answers are bounded batches (`WalkOptions::max_entries`, default 10k entries; a listing is never split); `find`/`du` refill through `builtins/walk_cache.rs` one batch at a time, consume listings as they descend, and fall back to `read_dir` if an answer does not hold the requested directory; rg searches each file as it is read and drops its text, charges it to the aggregate-input budget and stops at the first refusal, and in path order its walk reads files as it finds them, one root after another (no candidate list); `ls` (recursive or one wide directory) stops appending lines at `max_stdout_bytes`. Heap peaks over a 200k/2M-file procedural tree are pinned by `large_tree_stress_tests` (walkers under 8 MiB). Residuals: a non-path `--sort`, `--files`, or nested roots still build the full candidate list (paths + metadata per file, no budget charge) before reading; one wide directory is held whole by `read_dir`. Regressions: `walk_cache::tests::batches_stay_bounded_and_cover_every_directory`, `remote_fs_round_trip_tests::walk_batches_are_bounded_*`, `rg_stops_reading_at_input_budget` | **MITIGATED** |
317
+
| TM-DOS-143 | Document extraction outlives the exec deadline |`markkit extract` on a large document (or `fetchkit` converting a large page) ran synchronously on the tenant's async worker: the `exec` timeout could not interrupt it and co-tenants scheduled on that worker stalled, up to markkit's 60 s default | Both builtins run their CLI on tokio's blocking pool, so the caller's future stays cancellable and the worker free; markkit's cooperative `timeout` and fetchkit's per-request timeout are capped at the remaining `exec` budget. Residual: abandoned extraction keeps a blocking-pool thread until markkit's own (coarse) deadline check fires. fetchkit request timeouts have the HTTP client's 1 s floor, so an abandoned request can outlive a sub-second remaining budget by up to 1 s. Regression: `extraction_stops_at_exec_deadline`|**MITIGATED**|
317
318
| TM-DOS-141 | Re-entry recursion via eval/compgen/fc |`f(){ eval f; }`, `compgen -C f`, `fc` re-exec, or PROMPT_COMMAND re-enter the interpreter without a function frame, so recursion was bounded only by the native stack and aborted the shared process | Each re-entry counts against `max_function_depth` and a separate `MAX_REENTRY_DEPTH` (8); over the cap the command fails `maximum nesting depth exceeded` (exit 2). Regressions: `recursive_eval_is_bounded`, `recursive_compgen_command_is_bounded`|**MITIGATED**|
318
319
| TM-DOS-142 | Product of nesting caps overflows worker stack | Each nesting kind has its own cap, but their product (compound nesting x functions x eval x child shells) can exceed a 2 MiB tokio worker stack in release builds |`execute_command` polls through `interpreter/grow_stack.rs`, which uses `stacker::maybe_grow` (512 KiB red zone, 4 MiB segments) so deep but capped nesting extends the stack on the heap instead of aborting. Not on wasm32 (single-threaded, host stack). Regression: `nested_compounds_inside_recursion_do_not_overflow`|**MITIGATED**|
319
320
| TM-DOS-144 | Root real mount drops FsLimits |`mount_real_readonly(dir)` / `mount_real_readwrite(dir)` at `/` wrapped the host dir in `OverlayFs::new`, whose upper layer used default limits, so a tenant could write past the configured `filesystem_limits`| The root overlay is built with `OverlayFs::with_limits` and the base filesystem's limits. Regression: `root_real_mount_keeps_filesystem_limits`|**MITIGATED**|
0 commit comments