Structural prevention of the reviewed bug classes
A wide review on 2026-06-14 (33 reviewers across core/ral/exarch, every
finding adversarially re-read against the real code) confirmed 73 defects. No
unsoundness in the type system, no memory-safety hole in safe Rust, no leak in
the evaluator core — but the serious bugs were not scattered. They collapse
into nine recurring shapes, and in every case the shape was writable because
a constraint that should live in a type (or a lint) lived only in a programmer’s
head.
The decision
For each reviewed bug class, make the bad state unconstructable with a type;
add a lint only to guard the one sanctioned construction site. This is the
discipline clippy.toml already applies to path handling — and two of the
serious bugs (resolve-path against the process cwd, the XDG .. escape) slipped
through gaps in lints that already exist, which is the strongest evidence that
the lever is the right one and merely incomplete.
The structural rewrites are staged behind the immediate point-fixes: the review landed the safe one-line corrections first; the types below are the durable follow-ups, each tracked as its own slice.
The nine shapes
-
Check/use path mismatch — the path authorised ≠ the path used. The XDG under-HOME guard compared an un-normalised prefix while the gate later collapsed
..;resolve-pathcanonicalised against the process cwd while its gate lexed against the logical cwd. Missing type: oneResolvedPath(absolute,./..-collapsed, logical-cwd-anchored), the sole input to bothcapability::check_fs_*and any canonicaliser, and aNormalizedPrefixfor the frozen grant minted by the same lexer the check uses. Extends capability-stage-collapse (the always-frozen prefix) and xdg-resolver-consolidation; the OS enforcer does not save us because it renders the same collapsed surface (two-enforcers, capability-enforcement). -
Cooperative cancellation that never cancels.
cancel/raceflip a flag but never cancel a scope, andspawn_threadhands the worker a freshCancelScope::root()instead ofparent.cancel.child(); the exarch walk builtins never pollprocess::check. Missing shape:spawn_threadrequires a child scope and stores it on the handle,CancelScope::root()drops topub(crate)(session-init only), and acancellable(walk)adaptor polls per item — with theWalkBuilder::buildban as backstop. The machinery exists (CancelScope::childis already there); the wiring is absent. Completes hot-path-cancellation for the builtin/worker sites it did not reach. -
A privileged fd/handle leaks into a confined child. The IPC socket is held across the spawn that launches confined externals and is never re-armed CLOEXEC, so an external can forge the eval response; on Windows the confined child inherits every handle (
bInheritHandles=1, noHANDLE_LIST) and the IPC pipe is squattable. Missing types: an RAIIIpcEndpoint(CLOEXEC / non-inheritable at construction, fd lent only inside a scope), aConfinedSpawnwhose only constructor carries the explicit inherit allow-list and always installsHANDLE_LIST, and aTokened<ChildEvalResponse>so a forged frame is a parse error. This is the unguarded sibling of authenticated-confinement-marker: that decision authenticated the request; the response direction stayed trusted. The OS sandbox confines syscalls but does not revoke an already-open socket — two-enforcers, grant. -
An unchecked range that can invert → slice panic. An untrusted plugin’s
HighlightSpan{start:5,end:2}reaches&mut styles[5..2]and panics the REPL on every keystroke. Missing type: aSpan { start, len }(half-open, never inverted) whose only constructor orders-and-clamps once. The type is the fix. -
Treating a record as an ordered key-stream.
_ed-set [text,cursor]clamps the cursor against the old text becauseValue::Mapiterates sorted and"cursor" < "text"; the bool decoders fold amatches!extractor with no type check. Missing pattern: decode a record into a typed struct by name and act in dependency order, with one strictdecode_boolshared fromdecode_net— never a fold over entries for known fields. -
REPL continuation by token-suffix heuristic, not the parser’s own signal. A comment-to-EOF dropped the
UnterminatedBalancedsignal; a trailing bare=was guessed incomplete, hanging the REPL. Missing type: a structuredIncompletenessthe parser returns, withneeds_continuationreduced tomatches!(err, Incomplete(_))— no last-token matching. (surface-syntax.) -
Provider-error classification by scraping
Displaystrings.from_genairead status only from the streaming error shape, so compaction’s 5xx/429 fell through to non-retryable. Robust shape: classify on genai’s typed error variants and structured status, not the formatted string. (provider.) -
A safety guard compiled out in release. The PWD/OLDPWD guard is a
debug_assert!, so release builds letwithin [env:[PWD:…]]rewrite the shell’s cwd. Missing type: PWD/OLDPWD are derived state, excluded as settable env keys at thewithin [env:]boundary — a hard error namingcd, never a silent drop; thedebug_assertgoes. Relates to env-is-dynamic-only. -
A diagnostic source-identity that is never populated (a dead guard). The cross-source caret guard keys on
SourceLoc.file == Some(f), but every production site setsfile: None, so asourced module’s runtime error draws its caret at the wrong bytes of the top-level script. Missing type: a non-optional source identity (or(SourceId, Span)resolved once at render) on locations that cross a module boundary. (diagnostics.)
Where
The full review record (per-finding evidence, the immediate fixes, the held cross-crate dedups, and the four findings parked for human judgement) lives in the verification transcripts of the 2026-06-14 review run. The durable content — why these shapes recur and how a type forecloses each — is this page.
Decisions
The open questions are resolved (2026-06-15). Slices land incrementally on
main, one commit per class, each gated on check + clippy + tests + the
cross-target checks for any cfg-gated code.
- Class 6. Drive continuation off a real
Incompletenesssignal, parsing on the REPL submit path (one parse per Enter — negligible). - Class 1. Full sweep:
ResolvedPathis the sole path input at everycapability::check_*and canonicaliser, not a localised patch. - Class 3. Adopt the full structural prevention — the RAII
IpcEndpoint, theTokened<ChildEvalResponse>, and theConfinedSpawnhandle-list builder — for the cleanest account, even though theCLOEXECre-arm + WindowsHANDLE_LISTalone close the confirmed forge (an external that never inherits the fd cannot write the channel). The re-arm,HANDLE_LIST, and the pipe squat-hardening (FIRST_PIPE_INSTANCE+ a per-user DACL) are the immediate close; the types are the durable prevention. - Class 8. Setting
PWD/OLDPWDthroughwithin [env: …]is a hard error namingcd, never a silent drop; thedebug_assertgoes. - Class 9. The full
(SourceId, Span)rework — a location carries a source identity resolved once at render — not merely populatingSourceLoc.file. - Held cross-crate dedups. One shared user/home/
USERresolver incore::path, which ral and exarch call directly. - The four uncertain.
exarch-session:2→ an atomic, set-once (static mutretired);typecheck-infer:4→ a defensive recursion bound even absent a reproducing program, matching the unifier’s co-inductive discipline;types-shell:2→ not a defect (OLDPWD = the effective cwd before thecd), documented only;exarch-tui:3→ consolidate the five wrap loops behind one row-builder helper.
A correction this implies
elaborator-ir:1 (forward-declaration masking an earlier command-use of an
acyclic thunk-form let) falsifies the claim in
surface-syntax that “forward declarations cover only
the thunk-form bindings group.rs knots, so the shadow agrees with what
evaluation will see.” Once the fix lands (forward-declaration scoped to the
recursive group), that internals page must be re-verified.
See also capability-freeze, capability-enforcement, esc-non-escalating-interrupt.