fix(mft): harden all 5 NTFS parsers against panic-on-malformed-bytes (WI-5.2)#349
Merged
Conversation
…(WI-5.2) The daemon builds with `panic = "abort"`, so any panic while parsing an untrusted on-disk MFT record is a whole-process DoS. The five parser entry points sliced untrusted `data[a..b]` and did raw `offset + N` / `name_len * 2` arithmetic under block-level `#[expect(indexing_slicing)]`. `index.rs` / `index_extension.rs` sit on the live USN-journal update path, so this is reachable in production. Changes (Category 5, "Bugs Rust Won't Catch" audit): - Every untrusted-`data` read now goes through `.get()` or bounds-safe `rd_u16`/`rd_u32`/`rd_u64` helpers; every byte-derived offset/length uses `checked_add`/`checked_mul` (or `saturating_*` where overflow is provably unreachable, with inline justification). `name_len * 2` overflow closed. - Behaviour preserved exactly: a malformed field yields the same skip/default the original `if X+N <= len` guards produced — no new error type, the parsers keep their `bool`/skip-bad-record contract. - `indexing_slicing` expects narrowed (not removed): only internal arena indices remain (`index.records[..]`, `fragment.streams[..]`, `frs_to_idx[..]`), keyed by self-minted indices; reasons updated to say so. - `#![warn(clippy::arithmetic_side_effects)]` enabled module-scoped in all 5 parser files — under workspace `-D warnings` this is effectively deny inside the parsers, a regression guard against new raw arithmetic on disk bytes. (Workspace-wide deny deferred: 1766 benign sites would hard-error.) - New regression test `malformed_records_do_not_panic`: a `RecordBuilder` forges 8 records that pass the header gate, run through all 3 live parser entry points, asserting no panic (offset-past-EOF, oversized length, name_len*2 overflow, non-resident size past EOF, reparse offset past EOF, zero-length attr, garbage body). Partial WI-5.3 (cache-deserializer fuzz still pending). Verified: native + `cargo xwin` (Windows) `--all-targets -D warnings` clippy clean; `cargo nextest run -p uffs-mft` 191 passed; downstream uffs-core/uffs-cli build clean; anti-pattern gate shows no new violations. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
The daemon builds with
panic = "abort", so any panic while parsing an untrusted on-disk MFT record is a whole-process DoS. The five NTFS parser entry points sliced untrusteddata[a..b]and did rawoffset + N/name_len * 2arithmetic, suppressed by block-level#[expect(indexing_slicing)].index.rs/index_extension.rssit on the live USN-journal incremental-update path (usn/windows.rs), so this is reachable in production — not dead code.This is WI-5.2 (Category 5: panic = DoS) of the "Bugs Rust Won't Catch" audit.
Changes
dataread now goes through.get()or bounds-saferd_u16/rd_u32/rd_u64helpers (return 0 on OOB).checked_add/checked_mul(orsaturating_*where overflow is provably unreachable, with an inline justification comment). The classicname_len * 2UTF-16 overflow is closed.if X+N <= lenguards produced. No new error type; the parsers keep theirbool/skip-bad-record contract (step-4 caller behaviour unchanged).indexing_slicingexpects narrowed, not removed — only internal arena indices remain (index.records[..],fragment.streams[..],frs_to_idx[..]), keyed by indices this code itself mints; each reason updated to say so.#![warn(clippy::arithmetic_side_effects)]enabled module-scoped in all 5 parser files. Under the workspace-D warningsthis is effectively deny inside the parsers — any new raw+/*/[..]on a byte-derived value fails the build there. (Workspace-wide deny is deliberately deferred: 1766 benign sites would hard-error; documented in the tracker.)malformed_records_do_not_panic: aRecordBuilderforges 8 records that pass the header gate and runs them through all 3 live entry points, asserting no panic (offset-past-EOF, oversized length,name_len*2overflow, non-resident size past EOF, reparse offset past EOF, zero-length attr, garbage body). This is the partial WI-5.3 deliverable; cache-deserializer fuzz remains a follow-up.Files
crates/uffs-mft/src/io/parser/{unified,index,index_extension,fragment,fragment_extension,mod}.rs+ tracker doc.parse/merger.rswas checked and left unchanged (itsindexing_slicingcovers only internal vectors — 0 untrusted slices).Verification
cargo clippy -p uffs-mft --all-targets --all-features -D warnings(native) — cleancargo xwin clippy ... --target x86_64-pc-windows-msvc -D warnings(Windows cross, covers the cfg(windows) USN path) — cleancargo nextest run -p uffs-mft— 191 passed, 3 skipped (was 190; +1 new test)uffs-core/uffs-clibuild — cleanNote on behaviour parity
index_extension.rsusesexisting_name_count + name_idx; the deprecatedfragment_extension.rsuses the legacyexisting_name_count - 1 + name_idx. Each file's existing formula was preserved verbatim (only+→saturating_add). This divergence predates this PR; no semantic change is introduced.