Phase 13: the push audit¶
Phase 13 of PLAN-bat-population.md, the last
row of its Execution table. Read the plan-push-audit-phase shared
block in the master plan before anything else: it defines what this
phase is for and why its scope cannot be derived after the fact. Then
read PUSH-AUDIT.md, which is the runbook this phase executes.
This phase audits the accumulated work of phases 1 to 12 and changes no code. Its steps are readers. The management session writes the record, which is the commit.
Planning effort: medium. The judgement in a push audit is in scoping it and in triaging what comes back, not in inventing a method -- the runbook and the scope tooling both exist, and a previous push-audit phase in this repository has already been through the traps. What this plan mostly has to get right is the commit list, and that it is honest about the parts it cannot cover.
Review effort: high, concentrated in one place. An audit that reports nothing is indistinguishable from an audit that ran over nothing, and this phase has a live instance of exactly that failure (see the survey). The review that matters is of the resolved scope, not of the findings.
Scope¶
In: the union of the first-parent diffs of the twelve merge commits that
put phases 1 to 12 on develop, which resolves to 78 files; and the
three Python scripts phase 10 landed in instar-testdata.
Out: fixing what the audit finds. Findings land as their own pull
request after this one merges, per the shared block and the
implement-phase skill. Also out: the 17,271 generated files in
instar-testdata (decision 3).
The commit list, which is the whole of this phase's scope and the one thing a reader should check first:
fef62417 phase 1 #809
5c5bba75 phase 2 #819 (instar half; see decision 4)
a8d2f4ca phase 3 #824
d140aa04 phase 4 #835
52518787 phase 5 #848
aef757d2 phase 6 #853
03269d7f phase 7 #858
e49b6275 phase 8 #874
3b74ba8d phase 9 #877
e0b0fe13 phase 10 #881 (instar half; see decision 2)
41f74d40 phase 11 #884
2da0c22e phase 12 #889
Plus, in instar-testdata, the recovered range 99c8663c34..f73d43670a
(decision 2), of which only scripts/detect-profiles.py,
scripts/generate-baselines.py, scripts/validate-profiles.py,
README.md and Makefile are in scope.
What the survey found¶
The master plan's phase 13 row is one line -- "Push audit:
PUSH-AUDIT.md over every phase above" -- so there was little to be
stale. The survey's work was establishing that the scope is
constructible, and it found one thing wrong and one thing missing.
Every recorded merge commit is real, and the record is complete for instar¶
All twelve are two-parent merge commits, all are ancestors of
origin/develop, and all name a bat-population pull request. The
shared block warns that phases which landed before a plan started
recording them have to be reconstructed from gh pr list and
git rev-list; none of that is needed here. This is a real result and
the reason this phase can be planned at medium effort.
The default audit scope is not empty here -- it is wrong, which is worse¶
PUSH-AUDIT.md says a push-audit phase must pass --commits-file,
because develop...HEAD covers only what the audit branch adds and an
empty range reads as a clean audit. On this machine it is worse than
empty. The local develop branch is stale at e0b0fe13, four merges
behind origin/develop, so the implicit scope resolves to 47 files
-- drawn from the renovate bumps, a nightly-toolchain bump and the
assurance pull request that landed between phases, and containing almost
none of phases 1 to 11.
An empty scope invites suspicion. A plausible-looking 47-file scope of unrelated work does not. Measured:
audit scope: 47 files (develop...HEAD) <- wrong
audit scope: 78 files (--commits-file phase-merges.txt) <- correct
Phase 10's Merged cell is missing its instar-testdata half¶
The shared block requires a phase that landed in another repository to
record <repo> <sha> (#pr). Phase 2 did: its cell reads
instar-testdata d2f9467719 (direct to main); instar 5c5bba75 (#819).
Phase 10 did not -- its cell names only e0b0fe13 (#881), although four
of its steps (10a to 10d) landed work in instar-testdata: the aimable
baseline generator, this plan's create cases, the derived-profile
validator and the per-image fixture baselines.
The range is recoverable, which is the good case the shared block hopes
for rather than the "no range is recoverable" case. Six contiguous
commits on 2026-11-07 in that repository, 99c8663c34..f73d43670a,
sitting cleanly after the 2026-11-06 nightly fuzz-corpus commits and
before the next day's. They match steps 10a to 10d one for one. This
phase records that range in phase 10's Merged cell as a correction at
source.
instar-testdata has no PUSH-AUDIT.md, so there is no audit to cite¶
The shared block says a phase landing in another repository is audited
against that repository's default branch as part of the pull request
that lands it, and that this plan's push-audit phase cites that audit
rather than re-running it. Neither half of that is available here.
instar-testdata has no PUSH-AUDIT.md -- its audit/ directory holds
run-qemu-img-audit.py, a qemu-img capability audit, which is a
different thing entirely -- and that repository takes direct commits to
main rather than merge requests, so there was never a pull request to
audit within. The shared block's instruction for a repository with no
runbook is to say so and say what was done instead, which this phase
does, and decision 3 sets what is done instead.
The instrument already works, and its limits are known¶
The scope mechanism this phase depends on was built by a previous
push-audit phase in this repository, PLAN-differencing-phase-18-push-audit.md,
whose own step 1 was to give the wave scripts an explicit scope.
tools/audit/audit-scope.sh, wave1.sh and wave2-mechanical.sh all
exist, with tests beside them. audit-scope.sh is sourced rather than
executed -- running it directly prints nothing and exits quietly, which
cost this survey a wrong answer before the file's own header corrected
it, and which the step briefs therefore state outright.
That phase also recorded what its audit could not reach, and this phase
inherits the same limits: wave1.sh has no integration-test step, so the
only Python executed is whatever the test-review step runs itself; and
the mutation harnesses are not run, because a full run needs docker,
testdata and /dev/kvm together.
Scope shape¶
The 78 files, by area, so the judgement steps can be briefed concretely:
| Area | Files |
|---|---|
docs/ |
30 |
tests/ |
17 |
src/crates/ |
8 |
src/operations/ |
5 |
src/fuzz/ |
5 |
tools/ |
4 |
scripts/ |
2 |
src/vmm/, src/shared/ |
1 each |
ARCHITECTURE.md, AGENTS.md, CHANGELOG.md |
1 each |
.github/, .gitignore |
1 each |
Corrected at source¶
Phase 12's row is set to Complete with 2da0c22e (#889), and phase
10's Merged cell gains its instar-testdata range, in this phase's
first commit.
Decisions¶
-
The audit runs with
--commits-file, never the default scope, and the resolved file count is reported before any check runs. This isPUSH-AUDIT.md's own instruction, and the survey found the default resolves to 47 files of unrelated work on this machine rather than to nothing. Every step that reads files states the count it saw, so a step that silently audited the wrong set is visible in its own report. -
Phase 10's
instar-testdatarange is recorded in the master plan as99c8663c34..f73d43670a, and the recovery is described as a recovery. The shared block requires reconstructed ranges to be labelled as reconstructed; this one is clean enough to state as fact, but how it was derived -- six contiguous commits between two nightly fuzz-corpus runs, matching steps 10a to 10d -- goes in the plan so the next reader can falsify it rather than trust it. -
The audit reads
instar-testdata's three scripts and runs its validator; it does not read the 17,271 generated files. That range changes 17,276 files, of which 17,271 are generatedexpected-outputsdata and five are code or documentation. Reading generated data is not an audit, and 17,000 files would swamp the phase and the agent's context both.
This is the decision most likely to be argued with, and the argument
against it is good: the derived profile directories in that
repository have been silently wrong twice while the raw baselines were
fine and the suite stayed green, so "the data is generated" is
precisely not a reason to trust it. The answer is that phase 10 added
scripts/validate-profiles.py for exactly that reason, and the
useful audit is of the validator and of whether it actually runs --
not of its inputs one file at a time. So the step reads the three
scripts as code and runs the validator, and reports its exit status
and what it covers. If the validator turns out not to cover the
generated set, that is a finding, and a better one than any number of
hand-read baselines.
-
Phase 2's
instar-testdatahalf is cited, not re-audited. Its cell already recordsd2f9467719, and that work is five fixtures plus a generator, superseded in place by phase 10's changes to the same scripts, which decision 3 does cover. The phase says so rather than implying coverage it does not have. -
Findings are recorded and fixed in this phase. This reverses what this plan originally decided, on Mikal's instruction at the back brief. The shared block and the
implement-phaseskill both say a push audit records its findings and a follow-up pull request resolves them, and the reason is that an audit-plus-fixes diff stops being reviewable as an audit. That cost was put to him and he chose to accept it, so the fixes land here and there is no follow-up pull request to wait for.
Two consequences follow and are handled rather than left implicit.
This phase therefore does change code, so the definition-of-done
item asserting it changed none is replaced by one bounding what it
may change. And this phase's own pull request is what marks the plan
Complete; its Merged cell stays empty, because a phase cannot
know its own merge commit while it is being written, and whoever
next touches the plan fills it in. Two of the three push-audit
phases already in this repository carry a filled cell and one is
empty; the filled ones were filled afterwards, which is the same
thing.
The audit steps themselves are still readers. Nothing in 2a to 2e may change a file: findings come back as reports, the management session triages them, and the fixes are made afterwards as their own commits so that the audit and its remedies stay separable in the history even though they share a pull request.
- The four judgement steps run in parallel. They are read-only, so they cannot collide in the worktree, and none consumes another's output. This is the one place a push-audit phase is cheaper than an implementation phase.
Step plan¶
Step 1 runs first and alone: the later steps consume its resolved scope and its mechanical report. Steps 2a to 2e then run together. Step 3 is the management session.
| Step | Effort | Model | Isolation | Brief for sub-agent |
|---|---|---|---|---|
| 1 | medium | sonnet | none | Materialise the audit scope and run the mechanical waves. Write the twelve merge commits from this plan's Scope section, one per line, to audit-commits.txt in the session scratch directory (NOT in the worktree). Then run tools/audit/wave1.sh --commits-file <that file> and, if wave 1 passes, tools/audit/wave2-mechanical.sh --commits-file <that file>. Note that tools/audit/audit-scope.sh is sourced by those scripts, not executed -- do not try to run it directly, it prints nothing and exits quietly. Report the resolved file count each script prints; it must be 78, and if it is not, stop and say so rather than continuing, because a different count means the scope is wrong. For the test totals, read the Ran: and Failed: lines rather than trusting the exit code, which is unreliable in this repository. Save both scripts' full output to the scratch directory and give the paths. Change no file in the worktree. |
| 2a | high | opus | none | Wave 2a, code quality, per PUSH-AUDIT.md's 2a section, over the Rust in the audit scope: 8 files under src/crates/, 5 under src/operations/, 5 under src/fuzz/, and one each in src/vmm/ and src/shared/. Resolve the scope with --commits-file as step 1 did and state the count you saw. The thing to look for across twelve phases is what the phases did to each other rather than what each did alone: a helper duplicated once two phases had both landed, a constant defined twice, an invariant one phase established and a later one quietly weakened. The VHDX layout work was deliberately consolidated into one shared function during the plan, so check it really is one. Report findings with file and symbol, severity, and the evidence; report "nothing" explicitly if that is the answer. Change no file. |
| 2b | high | opus | none | Wave 2b, tests, per PUSH-AUDIT.md's 2b section, over the 17 files under tests/ in the audit scope, the crate test modules inside the scope diff, and src/fuzz/. Resolve the scope with --commits-file and state the count. Five fuzz targets and a differential-fuzzer arm were added across the plan; confirm each is registered everywhere it must be (the .rs file, src/fuzz/Cargo.toml, coverage-fuzz.yml's TARGETS) and that tools/ci/check-fuzz-targets.sh passes. The specific risk this plan carries is a guard that cannot fail: phases 10 to 12 added classification tests and structural comparisons whose oracles are themselves in the diff. Pick the three you trust least and say for each whether a regression would actually fail it. You may run tests; you may not change any file. Note wave1.sh has no integration-test step, so anything you do not run here is unrun. |
| 2c | high | sonnet | none | Wave 2c, documentation, per PUSH-AUDIT.md's 2c section, over the 30 docs/ files in the audit scope plus ARCHITECTURE.md, AGENTS.md and CHANGELOG.md. Resolve the scope with --commits-file and state the count. Phase 12 swept the cross-cutting pages six commits ago, so the question is not whether they were updated but whether twelve phases left any fact stated two different ways -- the VHDX default block size, which subformats each command writes, and the preallocation behaviour are the three that moved most and are where to look first. Also check no file under docs/ outside docs/plans/ cites a source line number or a plan phase, step or decision number; both are pre-commit-enforced, so a hit is a real escape. Change no file. |
| 2d | high | opus | none | Wave 2d, security, per PUSH-AUDIT.md's 2d section, over the parsing and writing paths this plan touched: the VHDX BAT and metadata paths in src/crates/vhdx/, the fixed-layout emitters in src/crates/create/, and the host-side option and size handling in src/vmm/. Resolve the scope with --commits-file and state the count. This plan writes attacker-influenced geometry into on-disk structures and reads it back, so the shapes that matter are: an arithmetic overflow in a BAT offset or entry count, a length used before it is bounds-checked, a cast narrowing a u64 to u32, and a guest-side buffer sized from image-supplied values. The crates are no_std and must be panic-free, so an unchecked arithmetic operation is a defect even where the value cannot currently reach it. Report with file, function and the input that would reach it. Change no file. |
| 2e | medium | sonnet | none | Audit the instar-testdata half. Work in the clone at ../instar-testdata (branch main); do NOT create a worktree there and do NOT commit anything. Scope: the five non-generated files in 99c8663c34..f73d43670a -- scripts/detect-profiles.py, scripts/generate-baselines.py, scripts/validate-profiles.py, README.md and Makefile. Read the three scripts as code (Python conventions: single quotes, 120 columns). Then RUN scripts/validate-profiles.py and report its exit status, what it printed, and -- the question that matters -- which of the 17,271 generated files under expected-outputs/ it actually validates, and which it does not look at. Do not read the generated files individually; there are 17,000 of them and that is not an audit. Note this repository has no PUSH-AUDIT.md, so there is no runbook to follow and no prior audit to cite; say what you checked and what you could not. |
| 3 | high | — | none | Management session. Triage every finding from 1 and 2a-2e into blocking, fixed-in-this-phase, or filed-as-issue (fix-in-follow-up until decision 5's reversal), record the disposition of each in this plan under "Audit findings", write the "Coverage this audit did not have" section, and commit the record. |
Risks and mitigations¶
| Risk | Mitigation |
|---|---|
| A step resolves the default scope and audits 47 files of renovate bumps, reporting a clean result | Every reading step is briefed to print the resolved count and to stop if it is not 78. Step 1 stops the phase rather than continuing. The management session checks the count in each of the six reports before triaging a single finding. |
An agent runs audit-scope.sh directly, gets nothing, and proceeds against an empty set |
Every brief states that the file is sourced, not executed, and that running it prints nothing. This is in the briefs because it happened during the survey. |
| The 17,271 generated testdata files swamp step 2e | The brief bounds it to five files and forbids reading the generated set, and redirects the effort to the validator's coverage, which is the useful question. |
| The audit reports nothing and that is taken as a pass | The phase records a negative result as a negative result, in one sentence per step, and the "Coverage this audit did not have" section is a required part of the record rather than an optional caveat. |
| A judgement agent fixes what it finds | Every brief says to change no file. The steps are readers; step 3 writes. A step that returns a diff has its diff discarded and its finding triaged. |
| Findings grow into a thirteenth implementation phase | Decision 5 as amended at the back brief: findings are recorded here and fixed here, and every fix must trace to a recorded finding or be reverted. That traceability rule is what bounds the phase, in place of the follow-up pull request the original decision relied on. |
Definition of done¶
audit-commits.txtholds exactly the twelve shas in this plan's Scope section, and for each,git rev-list --parents -n1shows two parents andgit merge-base --is-ancestor <sha> origin/developsucceeds.- Both mechanical scripts printed
audit scope: 78 files (--commits-file ...), and the count appears in step 1's report. - Wave 1's result is recorded with its test totals read from the
Ran:andFailed:lines, not from an exit code. - Each of 2a, 2b, 2c, 2d and 2e produced a report that states the resolved file count it saw, and either findings or an explicit statement that it found nothing.
- Every finding is recorded under "Audit findings" with a disposition of blocking, fixed-in-this-phase, or filed-as-issue -- the middle one replacing fix-in-follow-up, which decision 5's reversal removed -- and every one filed as an issue carries its number. A master-plan Future work entry alone does not satisfy this: Future work records what is worth doing, an issue is what somebody is handed, and this plan's own findings include a consequence that was recorded in a plan and never reached either.
- The master plan's phase 10 row names the
instar-testdatarange, and its phase 12 row readsCompletewith2da0c22e. - The plan states in writing that
instar-testdatahas noPUSH-AUDIT.mdand what was done instead. - A "Coverage this audit did not have" section exists and names at least the integration suite and the mutation harnesses.
- Every file this phase changed outside
docs/plans/is traceable to a recorded finding: for each path ingit diff --name-only origin/develop, either it is a plan file, or the "Audit findings" section names the finding it fixes. A fix with no finding behind it is scope creep and is reverted. - Phase 13's row leaves its
Mergedcell empty. A phase cannot know its own merge commit while it is being written, and decision 5's reversal removed the follow-up pull request that would have filled it, so whoever next touches the plan fills it in.
Execution notes¶
Run 2026-11-11 (+1100), in one session, on branch
bat-population-phase-13-push-audit from 2da0c22e.
Back brief answers¶
| Question | Answer |
|---|---|
Keep the generated instar-testdata baselines out of scope? |
Yes, audit the validator instead |
May step 2e run validate-profiles.py in that clone? |
Yes |
| Findings: record here, or fix here? | Fix here -- see below |
| May this session push and open the pull request? | Yes |
The departure, and what it cost¶
The third answer reverses this plan's decision 5, which had findings
recorded here and resolved in a follow-up pull request, as both the
shared block and the implement-phase skill prefer. The reason they
prefer it is that an audit-plus-fixes diff stops being reviewable as an
audit. That was put to Mikal with the recommendation against it and he
chose to accept it, so the reversal is recorded in its own commit
rather than applied quietly, and two consequences were handled rather
than left implicit: the definition-of-done item asserting this phase
changes no code was replaced by one requiring every changed file to
trace to a recorded finding, and this phase's own pull request is what
marks the plan Complete, there being no follow-up to do it.
The audit steps stayed readers throughout. Nothing in 2a to 2e changed a file; the fixes were made afterwards, so the audit and its remedies are separate commits even though they share a pull request.
Step order¶
Step 1 ran alone, because the five judgement steps consume its resolved
scope and its mechanical report. 2a, 2b, 2c, 2d and 2e then ran
concurrently -- they are read-only, so they cannot collide in the one
worktree, which is the single way a push-audit phase is cheaper than an
implementation phase. The two fix agents ran concurrently on disjoint
files and were told not to run pre-commit, which the management
session ran once over the finished tree instead.
What the scope check was worth¶
Every one of the six steps printed audit scope: 78 files and said so
in its report. That mattered: on this machine the default scope is not
the empty range PUSH-AUDIT.md warns about but 47 files of unrelated
work, because the local develop branch is four merges stale. A step
that had silently resolved it would have audited renovate bumps and
reported them clean. One step also noted, correctly, that the line
numbers in the pre-computed added-line extracts are per-commit and
have shifted at HEAD, and re-derived its own from HEAD plus
git blame.
Audit findings¶
Ran 2026-11-11 (+1100). Scope resolved to 78 files from the twelve
merge commits, confirmed in both mechanical scripts' banners and in each
of the five judgement steps' reports. Wave 1 passed: make test-rust
gave 78 cargo suites, 2,800 tests, 0 failures, and
verify-info-output-parity compared 218 images with 0 failures.
The counts below separate what this plan introduced from what it
inherited, because the distinction decides the disposition: a defect in
the plan's own work is a finding and was fixed here, and a defect older
than the plan is an issue to file. git blame against the twelve merges
decided each one.
Fixed in this phase¶
-
A test that could not fail.
test_known_failures_are_in_the_matrixintests/test_vhdx_fixed_fixtures.pypolicedKNOWN_READ_FAILURES, which is empty by design, so its body reduced toassertEqual([], [])and it passed against any matrix -- including one that had stopped running. The known-failure branch of_check_readhad no coverage at all for the same reason. Added byfc5e982ein phase 4's merge, so this plan's own. Both checks are now pure functions,unrun_known_failuresandknown_read_problem, proven against literal stale entries in each of the ways they must catch, in the shapeknown_refusal_problemintests/test_create.pyalready used for an identically empty dict. Six mutants, each killed by exactly the intended test. All six were re-run in the review round, on a copy oftests/with a control run first so that a mutant could not look killed by a broken setup:unrun_known_failuresreturning nothing, matching on the image where it must match on the operation, and defaulting the matrix lookup to the operation it is testing for; andknown_read_problemaccepting a read that now passes, accepting a failure with the wrong message, and rejecting the recorded failure it must accept. -
A duplicated host helper. Phase 8 added
open_fixed_vhdx_convert_output, which re-implemented phase 7'sopen_fixed_vhdx_outputand inlined both offixed_vhdx_open_error's message shapes with aconvert:prefix in place ofcreate:. The plan had already solved this shape:check_vhdx_virtual_size(op, virtual_size)takes the operation prefix and serves five call sites. A second-order cost came with it -- create's separately-testable error builder had no convert equivalent, so the convert wording was untested. Now parameterised byop, with the messages proven unchanged for both.
The fix changed tools/mutate-differencing.sh as well, which is why
that file appears in this phase's diff. Collapsing the two open bodies
into one left the create mutant's search text stale and deleted the
convert mutant's target, so both of the
*-host-vhdx-fixed-efbig-mapping-removed cases were retargeted at
the shared body. They now carry identical
search and replacement text and differ only in the test each names:
one code path can break, and create's and convert's integration tests
must each notice it, because sharing the opener would otherwise have
left convert's filesystem-ceiling refusal resting on create's test.
--check-patterns reported the drift and now reports 153 of 153
landing.
- The resize preallocation modes, stated two ways. This plan added
text to
docs/create.mdanddocs/quirks.mdsayingresize --preallocation falloc|fullallocates the appended region on vmdk, vpc and vhdx. Three older tables said onlyoffwas supported there and thatfallocandfullwere "rejected (planner gap)":docs/resize.md's target-format and preallocation-mode tables, anddocs/format-coverage.md's resize table. The code agrees with the new text -- the three resize crates refuse onlyPreallocation::Metadata, andrun_resize_nonrawcallsapply_preallocationfor both modes -- so the tables were the stale side and are corrected. The stale tables predate the plan; the contradiction does not, because the plan added the claim that exposed them.metadataremains rejected and is left saying so.
What the tables now advertise is barely tested: RESIZE_CASES has no
preallocation case for vmdk, only off for vhdx, and off and full
for vhd, so a host-side gate added later would fail no test and these
tables would go stale again the same way. The review round filed that
as #898 rather than fixing it here: qemu-img refuses these resizes, so
there is no baseline to compare against, and the matrix mirrors
instar-testdata's generator, making it a cross-repository change
rather than a correction.
-
The block-size range without its constraint.
docs/convert.md(twice) anddocs/format-coverage.mdgave the VHDX block size as 1 MiB to 256 MiB without saying it must be a power of two, whichvhdx::layoutrequires and five other pages state. Added by this plan. -
An
unsafe fnwith no# Safetysection.convert_to_vhdx_fixedinsrc/operations/convert/src/main.rs. One was written naming the call-table and buffer invariants it actually relies on, bringing it up to the standardsrc/operations/create/src/main.rsholds, where all 8unsafe fnare documented.
The review of this pull request corrected the reason first recorded
here, which was that create documents all of its own "so the new
function was the outlier". That is true only across files. Within
convert there are 22 unsafe fn and, with this fix, 2 # Safety
sections: the file is the outlier, not the function, and the 20
undocumented ones include convert_to_vhdx, the dynamic sibling
immediately above. Filed as #897 rather than fixed here, because
documenting 20 unsafe fn is its own piece of work and an audit is
not the place to do it. The correction is kept visible because a
misleading rationale in an audit record is what the next audit
trusts.
Filed, not fixed¶
Each of these has a finding behind it, an issue number, and a reason it is not a fix. The issues were filed during this pull request's review round: the five findings here were recorded as master-plan Future work entries first, which this plan's own definition of done does not accept as a disposition, and one of the five is precisely a consequence that was recorded in a plan and never carried into an issue.
-
Nothing enforces the differential fuzzer's structural counters (#892). Phase 11 added
STRUCTURAL_OUTCOMESandKNOWN_DEFECT_SKIPSand reports them; nothing reads them. A nightly in which every structural comparison endedno_pin_for_geometry-- a pin set that stopped matching, which the code's own comment names as the risk -- logs one INFO line and exits 0. Making the counters a threshold anyone fails on is new capability, not a defect fix. -
The structural oracle is wired to
op_convertonly (#893).op_create, whose output is this plan's subject, still compares exit codes andqemu-img info. So the differential fuzzer never checks that an instar-created fixed VHDX has qemu's structure; that rests on the Rust fuzz target, whose oracle is a spec model rather than qemu, and on the Python pin tests. Possibly a deliberate phase 11 decision and in any case a new arm, not a repair. -
instar-testdata's profile validator has no gate (#894). Phase 10 addedscripts/validate-profiles.pybecause that repository's derived profile directories had been silently wrong twice while the raw baselines were fine. It works -- 960 version-to-profile mappings, 163,932 comparisons, 0 mismatches, reaching essentially all of the 17,271 files the plan generated -- but that repository has no CI, no pre-commit and notox, so it runs only when somebody typesmake validate-profiles. Phase 10's plan recorded that the repository had no CI; the consequence that nothing would ever run the validator it was adding was never carried into Future work or an issue. That dropped follow-up is the finding, and recording it is its fix. -
Two
instar-testdatascript defects (#895), both added by phase 10.generate-baselines.pyvalidates--imageagainst the manifest and then silently skips images the per-command loop does not apply to, so--command snapshot-list --image <valid non-qcow2 id>generates nothing and exits 0 -- the exact failure the filter's docstring claims to close. Andvalidate-profiles.pycounts a profile whose name no source produces as hand-maintained, so a stale leftover from a deleted case is absorbed rather than reported. Both are in a repository that takes direct commits tomainrather than pull requests, so they cannot land in this one. -
Advisories from the code-quality and security reads (#896). No named constant for the VHDX BAT entry size, where the crate names its three other table entry sizes and this plan added six more bare
8s; four call-table mirror constants written as literals where a fifth in the same plan usesshared::; andsrc/crates/vhdx/src/lib.rsgrowing 34% to 9,017 lines with a seam visible in its own banner comments.
Negative results worth recording¶
These are the questions the audit asked and answered cleanly. A push audit that records only what it found is half a result.
-
The format crates ship no
.unwrap()or.expect(). The plan added 63 across four crate source files, and every one is inside a#[cfg(test)]region: 42 insrc/crates/vhdx/src/lib.rs, 14 increate, 5 inmeasure, 2 inresize/src/vhdx.rs. The same production-versus-test boundary was re-checked at all twelve merge commits, so no intermediate state shipped one either. These crates are declared panic-free, so this was the plan's sharpest open question. -
No security finding. The new write path is checked end to end:
vhdx::layoutandfill_fixed_batusechecked_*at every step withu32::try_fromfor both narrowings, andfill_fixed_batprovesbat_entries * block_size + fixed_data_startbefore writing a byte. Noas u32on anything derived fromvirtual_sizeorblock_sizeoutside tests. No buffer is sized from an image-supplied value -- the guest BAT slot is a constantMAX_SECTOR_SIZE, and convert's buffers are sized from a host-validated sector size -- so nothing can overrun into the call table. Zero new host-sideunsafe. Of the 44 addedunsafelines, 42 are test code and the two production ones areunsafe fndeclarations, not blocks. -
The VHDX layout really is one function.
vhdx::layoutis the sole producer ofVhdxLayout, and every writer and sizer reads fields off it rather than recomputing: both guest ops, three crates, four host call sites and the fuzz target. No independent region arithmetic survives outside the crate. The one directcalculate_bat_layoutcall outside it is for a sector size the layout does not model and says so. -
Three guards were proven killable by mutation, not read and trusted: the differential fuzzer's classification scan, the VHDX structural comparison, and the two fuzz targets, whose expected values are recomputed from the specification rather than from the code under test.
-
Documentation is internally consistent on the three facts that moved most. The size-scaled block size, the subformat sets, and the
create/measurepreallocation behaviour each now read the same on every page that states them, checked against the code. No file outsidedocs/plans/cites a source line number or a plan phase, step or decision number; line-number citations were added during the plan and removed before it ended.
Coverage this audit did not have¶
- The Python integration suite was not run.
wave1.shhas no integration step -- its 2,800 tests are all cargo -- and this worktree has notests/.venv. That leaves roughly 2,500 added lines of Python test code intest_create.py,test_vhdx_fixed_fixtures.py,test_vhdx_structure.pyandtest_convert.pyunexecuted here. They ran in their own phases and in CI; they did not run in this audit. The in-scope Rust test modules are covered by wave 1.
The exception is this phase's own fixes, which the review round
ran rather than leaving to CI, because an audit whose remedies are the
only unexercised part of the tree is arguing against itself. In a
throwaway virtualenv built from tests/requirements.txt:
TestVhdxFixedFixturePins 9 tests pass in 0.7 s, and the seven
known-failure guards -- the one that could not fail before, plus the
six new ones -- pass in 0.006 s. Neither needs docker, /dev/kvm or
the instar binary, which is why they could run here; the rest of the
module still cannot.
* The full mutation harnesses were not run. A full run needs docker,
testdata and /dev/kvm together. Its pattern check does not:
tools/mutate-differencing.sh --check-patterns reports 153 of 153
landing, re-run in the review round after it had caught this phase's
own refactor drifting both EFBIG cases. The audit's own mutants were
run, on copies, in scratch directories.
* The fuzz targets were not built. src/fuzz is outside the cargo
workspace, so make lint never sees it; check-fuzz-targets.sh
confirms registration, not compilation. coverage-fuzz.yml fires on
pull_request for those paths, so CI covers it.
* instar-testdata's 17,271 generated baselines were not read, by
decision. The validator was run instead and its reach measured.
* The validator proves the derived profiles equal the raw baselines,
not that the raw baselines are right. It cannot detect a wrong raw
baseline, and six output types have no version map and are skipped
entirely -- none of them in this plan's range.
Corrections the audit made to this plan's own text¶
Three claims in this plan or in the briefs written from it were wrong, and each was caught by the step it was given to:
- "Five fuzz targets were added across the plan." One was:
fuzz_vhdx_fixed_bat.rs. The scope table's "5 files undersrc/fuzz/" counts three modified targets andCargo.toml. Confirmed by--diff-filter=Aacross all twelve merges. - "Both documentation rules are pre-commit-enforced." Only
docs-line-referencesis. There is no hook for plan-phase citations, so that rule rests on review, and the plan should not have implied otherwise. - One production
unsafewas attributed tosrc/crates/vhdx/src/lib.rs. It is inside that file's#[cfg(test)] mod tests, so 42 of the 44 addedunsafelines are test code and only two are production.
Back brief¶
Answer before any step runs:
- Is decision 3 right to leave the 17,271 generated files unread? The counter-argument is real: those derived directories have been silently wrong twice before. The plan's answer is to audit the validator rather than its inputs.
- Should step 2e run
validate-profiles.pyat all? It is a local read-only script in another repository's clone, but it is still running something rather than reading it, and that clone is one the fuzz automation commits to daily. - Is a follow-up pull request the right home for the findings, or should trivially safe ones be fixed in this phase's own pull request?
- May this session push and open the pull request when the record is written?
Out of scope¶
- Nothing the audit finds is out of scope to fix, by the back brief's instruction -- but a fix still needs a finding behind it. Something noticed in passing that no audit step reported is filed, not fixed.
- The 17,271 generated
expected-outputsfiles ininstar-testdata(decision 3). - Re-auditing phase 2's
instar-testdatafixtures (decision 4). - Writing a
PUSH-AUDIT.mdforinstar-testdata. The gap is recorded here and is worth an issue, but authoring another repository's audit runbook is not this phase's job. - The master plan's Future work list. The audit may add to it; it does not work through it.