Files
msd-core/.changeset/fix-1461-overlay-crash-dos.md

3.6 KiB

type, pr
type pr
Fixed 1475

The capability loader never crashes on a single malformed overlay, and untrusted manifest/tar reads are size-bounded (ADR-1244 D2 invariant) — loadRegistry now makes the WHOLE per-candidate overlay-processing body total: ANY throw from ANY validator or step (including the committed validateCapability, which dereferences a malformed array entry such as gates: [null] / steps: [null] / contributions: [null] before its shape check) drops just that one overlay with a skip-warning instead of escaping the loader, while gatePointsOf is hardened to be total over null/non-array/malformed gates. The final canonical buildRegistry compose stays guarded: a throw on the merged set falls back to the frozen first-party registry plus a warning, records each dropped gate-declaring overlay's declared gate as blocked (incompatibleGateCapIds / blockedGates) so a dropped blocking gate FAILS CLOSED, AND now clears _overlay.commandRoots in the fallback so no dropped overlay retains a stale command root. Previously a malformed-array throw or a compose throw escaped the loader and crashed every consumer (loop-resolver, config-loader, surface, capability-state, gsd-tools). On the source side, the capability resolver/staging now reads every untrusted capability.json (tarball / npm / git / local) via the shared bounded fd-reader (regular-file + 8 MiB cap) instead of a raw fs.readFileSync, so an oversized or FIFO/non-regular extracted-or-local manifest can no longer OOM or hang the resolver; the fetch (realHttpsGet) bounds the downloaded response to 64 MiB; and stageValidated now enforces ONE uniform aggregate byte-budget (MAX_STAGED_BUNDLE_BYTES, 128 MiB) over the STAGED bundle directory via a bounded streaming walk (cumulative byte + entry counters; symlink / non-regular entries rejected) at the common staging chokepoint — AFTER staging and BEFORE validation/promotion — so a huge source tree, git repo, npm package, or gzip/tar bomb is rejected (and its staging dir cleaned up) before promotion, uniformly bounding the RESULT of copyDirRecursive / git clone / npm pack / tar -x that were previously only timeout-bounded. copyDirRecursive itself is now STREAMING and BUDGETED: it enumerates each directory via fs.opendirSync + dir.readSync() (one entry at a time) and threads CUMULATIVE entry (MAX_STAGED_BUNDLE_ENTRIES, 100k) and byte (MAX_STAGED_BUNDLE_BYTES) counters through the recursion, failing closed the MOMENT either cap is exceeded DURING the copy — closing a residual where the former fs.readdirSync(src, { withFileTypes: true }) materialized the ENTIRE directory-entry array into memory at staging time (BEFORE the post-copy budget walk), so a hostile source whose tree held a directory of millions of tiny files (fetch < 64 MiB, but a colossal dirent array) could OOM the process during the copy before the budget could fail closed; the post-copy walk is retained as a cheap belt-and-suspenders re-verification on what actually landed in staging. The spoofable per-member tar-header size parse (parseTarMemberSize, which mis-anchored on BSD tar -tv owner/group columns such as a Jan group → fail-open) was REMOVED in favor of that non-spoofable staged-dir budget; assertSafeTarMembers keeps its unambiguous traversal / symlink / hardlink NAME and TYPE guards. (#1461)