PLAN: Differencing phase 9 — coverage fuzzing of the locator parsers¶
Phase 9 of PLAN-differencing.md.
Goal¶
Discharge the master plan's success criterion that crates/vhd and
crates/vhdx "parse parent locator structures and are clean under the
new fuzz targets, including the adversarial fixtures from phase 2".
The survey narrows that considerably. The emit side is already
fuzzed — phases 5 and 6 extended fuzz_create_emitters as they went —
and the phase 2 adversarial fixtures are already seeded into the
corpus. What is left is the read side: the VHD parent-locator
parsers are reached by no fuzz target at all, and the VHDX one is
reachable only through a path nobody has measured.
Planning effort¶
Medium, as PLAN-differencing.md specifies for this phase. The pattern is well established — there are 40 existing targets to copy — and the survey has already done the expensive part. The single judgement worth arguing about is decision 3.
Review effort¶
Medium. Nothing in this phase changes what instar reads or writes;
a wrong answer costs fuzz coverage, not correctness. The exception is
step 9e, which may touch src/crates/vhd/src/lib.rs, and decision 7
bounds that to a docstring.
Scope¶
In scope:
- A buffer-based fuzz target driving the VHD parent-locator read path:
VhdParentInfo::parse, the eight-entry locator table, thelocator_defectplacement rules,preferred_locator's precedence and ambiguity logic, anddecode_name. - Measuring, rather than assuming, whether the existing
fuzz_vhdx_metadatatarget reachesvhdx::parse_parent_locator, and adding a direct VHDX target only if it does not. - Corpus routing so the phase 2 adversarial locator fixtures reach the new target, and a hand-built minimal differencing seed so the target is not cold on a machine with no testdata.
- Proving the target can fail, by planting a policy defect and watching it get caught.
- The three stale-figure defects the survey found in the fuzz plumbing, all in files this phase already edits.
Out of scope:
- Fuzzing the compose path. That is phase 15 and cannot be done before phases 11 to 13 give instar a compose path at all.
- The differential fuzzer's silent degradation
(#590). Same word
in the name, different program: that is
scripts/differential-fuzz.pycross-checking against libyal tools, not coverage-guided fuzzing of a parser. - Fixing any parser defect the fuzzing finds, unless it is in code phases 3 to 7 added — see decision 6.
- The plan-reference cleanup in docstrings (#592) beyond the single line this phase makes stale (decision 7).
What the survey found¶
Surveyed against c416abd (phase 8's merge commit). The master plan's
one-line description of this phase — "coverage fuzzing of the locator
parsers" — is still accurate, but four of its unstated assumptions
were not.
1. The emit side is already fuzzed; this phase is read-side only. The master plan was written before phases 5 and 6 executed, and each extended the fuzz targets as it went rather than deferring to here:
| Commit | Phase | Target | Lines |
|---|---|---|---|
ac9a227 |
3 | fuzz_vhdx_metadata |
+6 |
ee07a2f |
5 | fuzz_create_emitters |
+10 |
d04ed54 |
5 review | fuzz_create_emitters |
+23 |
1bd8679 |
6 | fuzz_create_emitters |
+6 |
fuzz_create_emitters now feeds parent_unique_id,
parent_timestamp and parent_data_write_guid from the corpus and
drives both differencing emitters
(src/fuzz/fuzz_targets/fuzz_create_emitters.rs:80-84, :164-203,
:264). ac9a227 passes a 1 MiB metadata region length specifically
"so that the parent locator staging path stays reachable"
(src/fuzz/fuzz_targets/fuzz_vhdx_metadata.rs:57-61). Nothing in this
phase should re-fuzz the emitters.
2. The VHD read path is reached by no fuzz target. Grepped for each function rather than for the crate:
| Function | Location | Fuzzed today |
|---|---|---|
VhdParentInfo::parse |
src/crates/vhd/src/lib.rs:1018 |
no |
VhdParentLocatorTable::parse |
:836 |
no |
VhdParentLocator::parse |
:589 |
no |
locator_defect |
:707 |
no |
preferred_locator |
:885 |
no |
VhdParentInfo::decode_name |
:1064 |
no |
fuzz_vhd_footer calls only VhdFooter::parse,
VhdDynamicHeader::parse, compute_checksum and the geometry
helpers (src/fuzz/fuzz_targets/fuzz_vhd_footer.rs:6-31).
fuzz_vhd_bat calls only VhdState::init (:19). Neither reaches
the parent fields: VhdDynamicHeader::parse and VhdParentInfo::parse
are deliberately separate entry points, because — as the crate's own
docstring says — most callers should not "pay for parent fields none
of them read" (src/crates/vhd/src/lib.rs:974-977).
The crate already knows this is the gap. VhdParentInfo::parse's
docstring ends: "No I/O and no allocation: this is the entry point
phase 9 points a fuzz target at" (:1016-1017).
3. The VHDX read path is reachable but unmeasured.
vhdx::parse_parent_locator (src/crates/vhdx/src/lib.rs:1015) is
called from parse_metadata (:1134, at :1435), and
fuzz_vhdx_metadata calls parse_metadata directly at offsets
[0, 0x10000, 0x30000] with a 1 MiB length
(src/fuzz/fuzz_targets/fuzz_vhdx_metadata.rs:62-71). So the path
exists. Whether libFuzzer reaches it — a locator item requires a
well-formed region table, a metadata table, and an item whose offset
clears METADATA_ITEMS_MIN_OFFSET (src/crates/vhdx/src/lib.rs:513)
— is a coverage question, and nobody has run the coverage report.
Step 9a answers it with a number instead of a judgement.
4. The corpus is already correct; only the routing is missing.
All fifteen phase 2 differencing fixtures are in tests/manifest.json,
including the six adversarial locator images:
all typed vpc, plus vhdx-diff-{parent,child} typed vhdx.
Seeding is driven by the manifest, not by a directory walk
(:516-535), and routing is by detected format through
FORMAT_TO_TARGETS (:27-38). A new target therefore gets these
fixtures only by being added to that map — there is no fallback.
Corrected during execution — this paragraph originally claimed the
six adversarial fixtures were already seeded into four targets, and
that was wrong. Step 9b checked the corpus rather than the manifest
and found none of them in any target directory. They are 4,198,912
bytes (4,201,984 for conflicting) against a MAX_SEED_SIZE of
4,194,304 (scripts/extract-fuzz-corpus.py:55), and copy_seed
refuses any untruncated file above it (:68). They miss by about
4.5 KB of footer and header overhead on a 10 MiB nominal disk.
Confirmed independently by content hash and by
find src/fuzz/corpus -type f -size +4190k returning nothing.
The class is wider than these six: 43 of the 208 manifest fixtures,
21%, are dropped this way, across luks, qcow2, raw, vhdx, vmdk and
vpc, and nothing reports which — copy_seed returns False and the
caller counts it as skipped alongside genuinely absent files. Filed
as #593. Step 9d
takes the part this phase needs, per decision 10; the rest is out of
scope.
The lesson is the one this plan lineage keeps relearning: membership in the manifest was checked, membership in the corpus was inferred. Grep for the thing itself.
5. Every consumer of these parsers outside the crates is a test.
VhdParentInfo::parse, decode_name, preferred_locator and
vhdx::parse_parent_locator are called only from
src/crates/create/tests/round_trip.rs. No operation calls any of
them; that is phase 11's job. This is worth stating plainly because it
is the obvious objection to this phase — fuzzing a parser with no
production consumer — and the answer is in the next finding.
6. There is a production consumer, and it is a second decoder of the
same bytes. The info op prints backing file: for a differencing
VHD, but does not use VhdParentInfo at all: it decodes the raw
512-byte parent-name field itself with
shared::decode_utf16_field_nul_terminated, gated on
VhdDynamicHeader::parse returning Some
(src/operations/info/src/main.rs:902-911). So two independent
decoders read the same attacker-controlled field, one of them in a
no_main guest binary that cannot run cargo test at all. The
comment at :902-910 records that the gate exists precisely because
without it "an image with disk_type = 4 and an arbitrary
data_offset gets 512 bytes of unrelated file content decoded as
UTF-16BE and printed". That is the kind of claim a fuzz target should
be holding down.
7. Three stale figures in the fuzz plumbing, all in files this phase edits anyway, none of them about differencing:
.github/workflows/coverage-fuzz.yml:128hardcodesN_TARGETS=32when there are 40 targets. It is used only on the manual-dispatch path with no explicit target list, where it caps per-target duration at450 * 60 / N_TARGETS; at 32 it computes a cap 25% too generous for the 450-minute budget, so a "fuzz everything for a long time" dispatch can overrun into the 480-minute job timeout.docs/testing.md:1252-1256says "With the current 27 targets that gives the 17 deep targets ~24 min each versus ~17 min under an even split." Measured against the tree by running the script itself: 40 targets, 15 fast at 300s, 25 deep at 900s, versus 675s under an even split. Every figure in that sentence is wrong.docs/testing.mdcarries seven mangled phrases from a botched find/replace — "the PLAN-s worknapshot", "the PLAN-c workheck-repair", "the PLAN-q workcow2-write-infrastructure" — at lines 265, 984, 994, 1013, 1154, 1165 and 1175. Introduced byf415ba9("Move AGENTS/ARCHITECTURE detail into docs/."), which was scrubbing plan references and truncated the surrounding words.
8. A new target has six registration points, two of them
hand-maintained lists that nothing checks. Registering
fuzz_vhd_parent means touching all of:
src/fuzz/fuzz_targets/fuzz_vhd_parent.rs— the target itself.src/fuzz/Cargo.toml— a[[bin]]stanza..github/workflows/coverage-fuzz.yml:196-236— the hardcodedTARGETS=(...)array used whenever no explicit target list is given, which is every nightly and every post-merge run. A target missing from this list is never fuzzed in CI at all, and nothing compares it to the tree.tools/ci/fuzz-tier.sh:34—FAST_TIER, whose comment asks the reader to "keep this list in sync withsrc/fuzz/Cargo.toml" with nothing enforcing it. Omission is safe here — an unlisted target defaults to the deep tier — so this one is a cost question, not a correctness one.scripts/extract-fuzz-corpus.py:27-38—FORMAT_TO_TARGETS, with no fallback: an unlisted target getsfuzz_format_detect's seeds and nothing else.docs/testing.md:1100-1104— the count sentence and the table.
Checked rather than assumed: the workflow's array and the tree agree exactly today, 40 for 40. So this is a trap for the next person, not an existing defect, and decision 9 says what to do about it.
The target count itself is not drifted: src/fuzz/fuzz_targets/,
the [[bin]] count in src/fuzz/Cargo.toml and the table in
docs/testing.md all say 40. Nothing in the master plan's phase 9 row
or the index.md row was factually wrong, so no correction at source
is needed this phase — which is a first for this plan.
Decisions¶
1. One new VHD target, not one per function. fuzz_vhd_parent
drives the whole read path from a single dynamic-header buffer:
VhdParentInfo::parse, then the locator table it returns, then
preferred_locator, then decode_name. Splitting it per function
would multiply corpus and nightly budget for surfaces that share their
entire input. The existing targets follow the same shape — one target
per entry point, not per function.
2. Buffer-based, not CallTable. These parsers do no I/O by
construction; the crate's module docs open with a section titled
"Parent locators do no I/O" (src/crates/vhd/src/lib.rs:7). A
CallTable target would add a mock I/O layer that the code under test
never calls, costing fuzzer throughput for nothing. VhdImageBounds
is a plain descriptor and is synthesised from the fuzz input instead.
3. The target asserts invariants, not merely the absence of a
panic. This is the decision most worth arguing with, so here is the
reasoning. These parsers are safe no_std Rust reading constant
offsets inside lengths already checked, so a panic-only target would
very likely find nothing and would then be indistinguishable from a
target that reaches nothing at all — which is exactly the unearned
green phase 8 was about. The interesting failures here are policy
failures, and they are assertable:
preferred_locatornever returns an entry whosedefectisSome(_). That is the whole point oflocator_defect, and the crate's own tests assert it only for hand-built cases (:3322 preferred_locator_skips_malformed_entries).- A duplicate platform code whose values disagree resolves to
ambiguous, never to a winner (
:3406). decode_namereturnsSome(n)withn <= dst.len(), and writes nothing beyondn.- Parsing is deterministic: the same bytes parsed twice give the same answer, which catches any accidental dependence on uninitialised buffer contents.
- No returned offset or length escapes the
VhdImageBoundsit was parsed against — the propertylocator_defectexists to enforce (:707), stated once at the boundary instead of per rule.
A crash is still a finding; these assertions are what make a non-crash informative.
4. The new target goes in the fast tier. It is buffer-based with a
small fixed input shape and will saturate quickly, like
fuzz_create_emitters which it most resembles. Measured cost, by
running tools/ci/fuzz-tier.sh plan 27000 300 over the real target
list: adding it to the fast tier moves the deep targets from 900s to
888s each (−1.3%); adding it to the deep tier would move them to 865s
(−3.9%). Either is negligible, and the fast tier is the honest
classification.
5. Whether VHDX gets its own target is decided by measurement, not
now. Step 9a produces a coverage report for fuzz_vhdx_metadata
against the phase 2 fixtures. If parse_parent_locator shows a
non-zero hit count, VHDX is already covered and step 9c is a corpus
note rather than a new target. If it shows zero, step 9c adds
fuzz_vhdx_parent in the same shape as fuzz_vhd_parent. Guessing
either way would be guessing about libFuzzer's ability to synthesise a
valid region table, which is not a thing to have an opinion about.
6. A parser defect found here is fixed only if phases 3 to 7 wrote it. Anything older is filed and referenced, on the same rule phase 8 used. The difference from phase 8 is that fuzzing can produce a genuine memory-safety or panic finding, and a panic in a guest binary parser is not a "file it and move on" class of bug — so the rule has one exception: a reachable panic or an out-of-bounds read is fixed here whatever wrote it, and the fix gets its own commit.
7. The one docstring this phase invalidates gets updated; the rest
of #592 does not.
src/crates/vhd/src/lib.rs:1016-1017 says the function is "the entry
point phase 9 points a fuzz target at". Once the target exists that
sentence is both a plan reference in landed code and stale, so it
becomes a reference to fuzz_vhd_parent by name. Every other plan
reference in these crates — and there are many, including several
pointing at PLAN-differencing-phase-01-pin.md — is left to #592.
8. The three stale figures are fixed here. They are in
docs/testing.md and coverage-fuzz.yml, both of which this phase
edits to register a new target, and leaving a wrong "27 targets"
sentence immediately above a row this phase adds would be worse than
the scope cost of fixing it. The N_TARGETS=32 fix derives the count
rather than restating it, so it cannot drift again.
9. Add a CI guard that the two hand-maintained lists match the
tree. Survey finding 8 describes exactly the failure phase 8 spent
three rounds refusing: a green run that proves nothing, because a
target absent from coverage-fuzz.yml's array is never executed and
no check would say so. A new fuzz target that CI silently never runs
is worse than no target, because the row in docs/testing.md claims
coverage that does not exist. The guard is a short script under
tools/ci/ — the repo convention forbids more than about five lines
inline in a workflow step — asserting that the [[bin]] names in
src/fuzz/Cargo.toml, the workflow's TARGETS array, and the files
in src/fuzz/fuzz_targets/ are the same set. FAST_TIER is checked
as a subset, not for equality, because omission there is a deliberate
default rather than an error.
This is scope growth beyond "fuzz the locator parsers" and it is taken deliberately: this phase is the one that adds the first new target since those lists drifted apart in the workflow's duration cap, and a guard written now costs one step, where discovering the omission later costs a nightly run that measured nothing.
10. Step 9d builds a reshaping seed extractor, not a bigger cap.
Survey finding 4's correction leaves the six adversarial fixtures
unreachable by any amount of routing. Three fixes were available:
raise MAX_SEED_SIZE, add a HEADER_ONLY_TARGETS truncation, or
emit a seed in the shape the target actually consumes. The third is
right and the other two are not.
Raising the cap fixes these six and leaves the other 37 dropped
fixtures for someone else to rediscover; it is also a global change
made for a local reason. A truncation is closer but still wrong: the
target treats data[..1024] as the dynamic header, and a real VHD
puts the cxsparse cookie at data_offset, not at zero, so a
header-prefix copy of a whole image is not a valid input to this
target at all — it would sit in the corpus looking like coverage and
contributing none. (The 9b brief asserted the opposite, that the
locator data would be cut off by truncation; that was wrong in its
own way — data_offset is 512 in all six and the platform data lies
in 1536..5632, so any truncation at or above 8 KiB keeps the whole
structure. Both statements were reasoning about the file rather than
about the target's input shape.)
So 9d follows extract_snapshot_parse_seed, which sets exactly this
precedent for qcow2 snapshot tables: read the footer's data_offset,
emit the 1024-byte dynamic header followed by the platform-data
region. A few KB rather than 4 MiB, in the target's own input shape,
and independent of the cap. The wider defect is filed as
#593 and is not
fixed here.
Step plan¶
| Step | Effort | Model | Isolation | Brief for sub-agent |
|---|---|---|---|---|
| 9a | medium | sonnet | none | Measure what the existing targets reach. Inside the instar-build devcontainer (make instar-devcontainer if the image is missing — note it is pruned daily by docker-image-prune.timer, so expect to rebuild), seed the corpus with python3 scripts/extract-fuzz-corpus.py --testdata ../instar-testdata, then run cargo fuzz coverage fuzz_vhdx_metadata and cargo fuzz coverage fuzz_vhd_bat from src/fuzz after a 300s run of each. Report the hit counts for vhdx::parse_parent_locator (src/crates/vhdx/src/lib.rs:1015), vhd::VhdParentInfo::parse (src/crates/vhd/src/lib.rs:1018) and vhd::VhdParentLocatorTable::parse (:836). Expect zero for both vhd functions — neither target calls them, this is the control that proves the measurement works. Change no tracked file; this step produces a number, not a diff. This is the back-brief gate: stop and report before 9b. |
| 9b | high | opus | worktree | Add fuzz_vhd_parent. New file src/fuzz/fuzz_targets/fuzz_vhd_parent.rs plus a [[bin]] stanza in src/fuzz/Cargo.toml (copy the shape of fuzz_vhd_footer, which is buffer-based and takes no CallTable). Require data.len() >= 1024; treat data[..1024] as a dynamic header and synthesise a VhdImageBounds from the tail so bounds vary with the corpus. Call vhd::VhdParentInfo::parse(header, &bounds); on Some, walk info.locators.entries, call info.locators.preferred_locator(None) and again with a VhdImageWindow built from the input, and call info.decode_name(&mut dst) into a 768-byte buffer (the worst-case UTF-8 expansion of the 512-byte UTF-16BE field, per :981). Assert the five invariants in decision 3 of this plan; a parse returning None is a valid outcome, not a failure. Register it in all four of the places survey finding 8 lists: the TARGETS=(...) array in .github/workflows/coverage-fuzz.yml:196-236 (without this the target is never fuzzed in CI at all), FAST_TIER in tools/ci/fuzz-tier.sh:34, and both 'vpc' and 'vhd' in FORMAT_TO_TARGETS (scripts/extract-fuzz-corpus.py:36-37) with no entry in HEADER_ONLY_TARGETS — the dynamic header sits at data_offset, typically 512, so a truncation policy that keeps only a header prefix would cut it off. Constraints: the fuzz crate is not no_std but the crates under test are; do not add dependencies. |
| 9c | medium | sonnet | none | VHDX, conditional on 9a. If 9a measured a non-zero hit count for parse_parent_locator, add no target: instead add a HEADER_ONLY_TARGETS comment recording the measured figure so the next reader does not re-ask, and say so in the commit. If it measured zero, add fuzz_vhdx_parent in the same shape as 9b driving vhdx::parse_parent_locator(item) directly on a fuzz-derived item slice, asserting that a returned parent_linkage() is within the item and that is_vhdx_locator_type() agrees with the locator type GUID; register it in Cargo.toml, the workflow's TARGETS array, FAST_TIER, and FORMAT_TO_TARGETS['vhdx'] -- all four, per survey finding 8. |
| 9d | medium | sonnet | none | Add a minimal differencing seed. create_minimal_seeds in scripts/extract-fuzz-corpus.py:335 builds a disk_type=2 (fixed) VHD footer at :416-423; there is no differencing seed, so a run with no testdata starts cold on exactly the structure this phase fuzzes. Add one: a 512-byte footer with disk_type=4, a 1024-byte cxsparse dynamic header with a non-zero parent unique id at +40 and a UTF-16BE parent name at +64, and one populated W2ru locator entry at +576 pointing at in-bounds data. Write it to the fuzz_vhd_parent corpus dir (and fuzz_vhdx_parent if 9c created one). Follow the existing single-quote / 120-column Python style in that file. |
| 9e | high | opus | worktree | Run it, and prove it can fail. Run fuzz_vhd_parent for at least 600s against the seeded corpus inside the devcontainer, and report the coverage hit counts for the six functions in survey finding 2 — all must be non-zero, which is the criterion that separates this from a target that runs and reaches nothing. Then plant a policy defect and confirm the target catches it within 60s: remove the defect.is_none() condition from preferred_locator's entry filter (src/crates/vhd/src/lib.rs:885), run, observe the assertion fire, restore from a copy — not with git checkout, which discards uncommitted work in the directory. Paste both the coverage figures and the planted-defect crash into the commit message. Triage anything the run finds under decision 6. Also update the docstring at :1016-1017 per decision 7. |
| 9f | medium | sonnet | none | Guard the registration lists. Write tools/ci/check-fuzz-targets.sh asserting that three sets are equal: the .rs basenames in src/fuzz/fuzz_targets/, the [[bin]] name = values in src/fuzz/Cargo.toml, and the entries of the TARGETS=(...) array in .github/workflows/coverage-fuzz.yml. Check FAST_TIER in tools/ci/fuzz-tier.sh as a subset only -- an unlisted target legitimately defaults to the deep tier. Exit non-zero naming the offending target and which list it is missing from. Wire it into the ci-tooling job of .github/workflows/functional-tests.yml alongside the phase 8 guards ("Test the mutation harness verdicts", "Test the literal replace helper") -- copy their step shape. Falsify it before claiming it works: delete one entry from the workflow array, confirm the script exits non-zero and names that target, then restore from a copy. Follow the shellcheck-clean style of tools/ci/test-replace-once.sh. |
| 9g | medium | sonnet | none | Documentation and the three stale figures. In docs/testing.md: add the new target row(s) to the table at :1104, update the count sentence at :1101 ("40 targets" → the new count), and rewrite the tiering sentence at :1252-1256 with the measured figures — 40 targets, 15 fast at 300s, 25 deep at 900s, 675s under an even split — recomputed for whatever 9b and 9c actually added. Repair the seven mangled phrases at lines 265, 984, 994, 1013, 1154, 1165 and 1175 ("the PLAN-s worknapshot" → "the snapshot work", and the two equivalents), naming the work rather than the plan file so the repair does not reintroduce a plan reference. In .github/workflows/coverage-fuzz.yml:128, replace N_TARGETS=32 with a count derived from src/fuzz/Cargo.toml; keep it under five lines or move it to tools/ci/ per the repo convention. Add a CHANGELOG.md Unreleased entry. |
Risks and mitigations¶
| Risk | Mitigation |
|---|---|
| The target runs, panics never, and reaches nothing — an unearned green of exactly the kind phase 8 existed to refuse. | 9e's done-criterion is a coverage hit count per function, not "the run was clean", plus a planted defect that must be caught. Both are pasted into the commit message. The management session re-runs the planted-defect check rather than trusting the report. |
| The fuzzer finds a real parser bug and the phase grows without limit. | Decision 6 bounds it: only phases 3–7 code is fixed here, except a reachable panic or OOB read, which is fixed whatever wrote it and gets its own commit. Anything else is filed. |
9a cannot build the devcontainer, because docker-image-prune.timer removes instar-build daily and make instar only rebuilds instar-release. |
The 9a brief says so. Run make instar-devcontainer in the foreground, not backgrounded — this host has killed two background builds for low memory. |
| The new fast-tier target displaces deep-target time in the nightly run. | Measured, not estimated: 900s → 888s per deep target, −1.3%. Decision 4 records both the measurement and the command that produced it. |
The VhdImageBounds synthesised in 9b is unrealistic, so locator_defect refuses everything and the interesting rules never execute. |
It is exactly what 9e's per-function hit counts detect: locator_defect at :707 is one of the six functions that must come back non-zero. |
Mangled-text repairs in docs/testing.md reintroduce the plan references f415ba9 was removing. |
9g's brief says to name the work, not the plan file, and the done criteria grep for both the mangled phrases and for PLAN-*.md in the repaired lines. |
Definition of done¶
src/fuzz/fuzz_targets/fuzz_vhd_parent.rsexists, is registered insrc/fuzz/Cargo.toml, andcargo fuzz buildsucceeds in the devcontainer.- A coverage report shows a non-zero hit count for each of
VhdParentInfo::parse,VhdParentLocatorTable::parse,VhdParentLocator::parse,locator_defect,preferred_locatoranddecode_name. The figures are in the commit message. - Removing the
defect.is_none()condition frompreferred_locatormakesfuzz_vhd_parentfail within 60 seconds. Run, not reasoned about; the output is in the commit message. - After
python3 scripts/extract-fuzz-corpus.py --testdata ../instar-testdata, the locator structure of all sixcustom/audit/vhd-diff-locator-*.vhdfixtures is present insrc/fuzz/corpus/fuzz_vhd_parent/— as reshaped seeds, not whole images, which the 4 MiBMAX_SEED_SIZErefuses. Each seed round trips: parsing it withVhdParentInfo::parseyields the same platform codes and paths as parsing the fixture it came from. tools/ci/fuzz-tier.sh is-fast fuzz_vhd_parentexits 0.- The VHDX question is answered with a number: either a measured
non-zero hit count for
parse_parent_locatorrecorded in a comment, or a new target. grep -c '^\[\[bin\]\]' src/fuzz/Cargo.toml, the number stated indocs/testing.md, and the row count of its target table all agree.grep -nE 'PLAN-[a-z] work' docs/testing.mdreturns nothing, and no line repaired in 9g names aPLAN-*.mdfile..github/workflows/coverage-fuzz.ymlcontains no hardcoded target count; the value is derived.tools/ci/check-fuzz-targets.shexits 0 on the tree, and exits non-zero naming the target when one entry is deleted from the workflow'sTARGETSarray. Demonstrated, not claimed.- The new target appears in the workflow's
TARGETSarray, so a nightly run actually executes it. - The tiering paragraph in
docs/testing.mdmatches whattools/ci/fuzz-tier.sh plan 27000 300 <targets>actually prints. src/crates/vhd/src/lib.rsno longer says "phase 9" anywhere.make lint,make test-rust,make check-binary-sizesandpre-commit run --all-filesare clean.
Back brief¶
Before 9b writes a line, report 9a's measurement: the hit counts
for parse_parent_locator, VhdParentInfo::parse and
VhdParentLocatorTable::parse, and which of the two branches of
decision 5 that puts step 9c on. The whole shape of the VHDX half of
this phase rests on that number, and it is cheap to get and expensive
to be wrong about — this plan asserts the path is reachable but
explicitly does not claim it is reached.
Before 9e plants its defect, confirm the restore mechanism is a
file copy. Phase 8 hit this: a directory-wide git checkout to undo a
mutation discards uncommitted work in that directory, and in a
worktree carrying a half-finished fuzz target that is expensive.