Safety hardening: unsafe docs, async I/O, recursion and read budgets - #6333
AdityaVG13 wants to merge 7 commits into
Conversation
Atlas UNSAFE-001: every production unsafe block now carries a terse justification at the site. The 16 bare set_var blocks in apply_tui_env are centralized into one documented set_tui_env helper; all other changes are comment-only.
Atlas ASYNC-002: convert std fs calls lexically inside async fns to their tokio::fs equivalents (read/write/rename/metadata/remove_file/ create_dir_all/OpenOptions/try_exists). Coherent same-function twins converted too; search.rs:215 falsified (already behind spawn_blocking via run_blocking_grep). Sync helpers shared with sync callers (write_atomic*, streaming readers, staging) intentionally untouched.
Atlas RESOURCE-002: export walkers (TOML, cap 64), JSON redactor and approval canonicalizer (cap 128, serde-parse-aligned) now carry depth fuel and fail closed past it. canonicalize_json_keys proven bounded (sole input is McpConfig-shaped, no Value fields) — no change. Adds deep-input tests per walker.
Atlas PANIC-002 is one policy decision, not 21 patches. Production locks already fail-stop with lock-naming messages (user registry, session index, coordination slot); 15 of 21 findings are test-support code. Document the rule: expect by default, into_inner only where stale state is safe.
Atlas RESOURCE-001: take(limit+1)+check pattern mirroring the credential store's existing limit. Config/state readers capped at 1 MiB, sub-agent state and stdin patches at 16 MiB, API-key stdin at 8 KiB; worker-log drain capped per call with a pending-line bound. The 5 oauth findings already flow through the budgeted store reader.
Hmbown
left a comment
There was a problem hiding this comment.
Thanks for this — I read the full diff and verified the load-bearing claims. Most of it holds up well; one substantive issue on the file-read path, plus nits.
Verified
set_tui_envSAFETY contract checks out: the telemetry actor thread exists (crates/telemetry/src/actor.rs:81) and production telemetry code never touches the process environment (onlystd::env::consts::OS/ARCH, which are compile-time constants). All six call sites run pre-runtime. Consolidating 15 undocumented blocks into one documented helper is a strict improvement.take(limit+1)+ length-check pattern is correct everywhere, and the 128 depth caps match serde_json's own recursion limit, so parsed input never truncates. New depth tests (config_bundles,approval_cache,persistence) cover the fail-closed behavior.- CI triage:
Version drift,Integrations, frontendLint & Type Check, andTest (windows-latest)all fail identically on the base commit itself (main run 35298989096 @531cddb56, your exact base). None of those four are owned by this PR.Integrationsand frontend lint touch only node suites this PR never modifies.
Required: the file-read path still blocks the runtime
In crates/tui/src/tools/file.rs, the conversion is open-only:
read_window_streamingstill takesstd::fs::Fileand runs its whole read-to-EOF loop inline; the PR just adds an async open plus.into_std().awaitto feed the same blocking loop.hash_file_streaming(a second full-file blocking pass over up to large files) is untouched and still called inline from asyncexecute.
So the two heaviest blocking operations on the hottest tool path remain on the runtime, and the analyzer-count drop here reflects moved call sites rather than moved work. Per the blocking-call convention (#6149, which this PR already cites), please wrap the open+stream+hash section in spawn_blocking at the async call site (keeping the sync open), rather than the async-open-then-into_std round trip. Same question for fim.rs/git_history.rs/tool_result_retrieval.rs only if their reads are large-file-capable — single small reads via tokio::fs are fine as-is.
Nits (non-blocking)
set_tui_envdocs say callers must be on the main thread, but the existing tests callapply_tui_envfrom test threads (serialized viaenv_lock). Consider wording the contract as what it actually is: no concurrent environment access, with tests serializing on the lock.- Fleet drain (
fleet/executor.rs):pending.clear()on a >1 MiB newline-free flood silently drops bytes. A debug log or counter would keep that observable. - Canonicalizer
maxdepthmarker: values differing only past depth 128 now share an approval key. Contrived to exploit and strictly better than the old stack overflow, so not a blocker — noting it for the record.
Before merge
- Address the
file.rsitem above. - Ubuntu/macos
Test,Lint, andSafety gatewere still in progress at review time; those need to go green (or be shown pre-existing like the four above). - The "PR closes an issue" check wants a linked issue — please add
Closes #…if one exists for this hardening work.
The file.rs conversion was open-only: read_window_streaming ran its full read-to-EOF loop inline and hash_file_streaming made a second blocking pass from async execute. Wrap open+stream+hash in spawn_blocking at the call site with the sync open, per the blocking-call convention (Hmbown#6149). Also from the same review: the set_tui_env contract now states no concurrent environment access (tests serialize on the env lock), and the fleet drain logs dropped bytes instead of clearing silently.
|
Note: the cross-reference on this PR's timeline to a pull request on my fork was accidental. It came from an internal review pointer that has since been deleted; GitHub does not retract timeline cross-references, so the stub remains visible. It points to a closed fork PR and has no bearing on this change. For the |
|
Review feedback addressed in file.rs (required): done as prescribed. Open+stream+hash now run in one Sibling reads: checked all three, no changes needed.
Nits: Ubuntu Test failures (6): shown pre-existing, not from this PR. Each of the six fails identically on clean |
|
Ubuntu triage (following up on my review): the run shows 6 failures, all outside this PR's behavioral surface, and with a shared signature pointing at live-catalog drift rather than the diff:
SAFETY comments, I re-ran the failed jobs to test that: if Ubuntu goes green, these were flakes/drift-that-settled and unrelated to the PR. If they fail identically, the next step is re-running the same tests on base-as-of-now to confirm the drift, and base — not this PR — owns the re-baseline. Either way, my requested change (the |
Follow-up to the file.rs review item: the same audit traced up and down the file-tool tree and found three more instances of the same bug class, all fixed here per the blocking-call convention (Hmbown#6149). - Write/edit paths called sync write_atomic_workspace inline: temp create plus fsync plus rename (and a thread::sleep retry loop on Windows). All four call sites now go through one spawn_blocking helper with identical error shapes. - PDF detection ran a sync open plus magic sniff on every read. is_pdf is now async over tokio::fs; its three tests moved to tokio::test with the same assertions. - OCR shelled out to tesseract synchronously from two async execute paths (File read and ImageOcrTool). Both call sites now wrap the whole synchronous call in spawn_blocking. Audited and deliberately left alone: note_file_read (one stat syscall, ten call sites), the canonicalize credential guard (fast path syscalls), list_dir (already pooled), and the PDF extractor (already on tokio::process).
|
Follow-up to the file.rs item: I traced the file-tool tree up and down for the same bug class and found three more instances, fixed in
Audited and deliberately left alone: Verification: |
Summary
Safety hardening across four areas, rebased on current
main(531cddb56).Every undocumented
unsafeblock gets a SAFETY contract, blocking file callsin async code move to
tokio::fs(or upstream'sspawn_blockingconventionwhere it already applied), recursive value walkers get depth fuel, the
lock-poison policy is recorded as fail-stop, and unbounded file and stdin
reads get budgets. No public APIs change and no new dependencies are added.
unsafeblocks; one documentedset_tui_envhelper for TUI env mutationtokio::fsfor blocking calls in async code; defers to upstreamspawn_blockingrefactors where presentdocs/ARCHITECTURE.mdAudit rescan on clean worktrees of base and branch, same analyzer build:
unsafeblocksRemaining deltas are documented-unsafe inventory (info severity, no action)
and same-site re-identifications at shifted line numbers. Net new actionable
findings: 0.
Notes:
convention); this branch keeps its conversions only where upstream has none.
codewhale-secretsafter upstreammoved the redactor there verbatim and unbounded.
Testing
cargo fmt --all -- --checkcargo clippy --workspace --all-targets --all-features --locked(warning-free under the CI allow list)cargo test --workspace --all-features --lockedGates run so far (workspace clippy and test left for CI):
cargo fmt --all -- --checkferrum check -p codewhale-cli -p codewhale-config -p codewhale-secrets -p codewhale-tuiferrum clippyon the same four crates,--all-targetsferrum test -p codewhale-config --lib redact_jsonferrum test -p codewhale-cli --lib config_bundlesferrum test -p codewhale-tui --lib pathological_nestingNo performance benchmarks were run: this change makes no performance claims.
The quantitative evidence is the finding counts and gate results above.
Checklist
No-Issue: proactive hardening sweep; no tracking issue was filed for this work.