Phase 3: host-only create, no-op preallocation and monolithicFlat¶
Phase 3 of PLAN-bat-population.md. Read its Situation and the phase 3 paragraph of "Sequencing rationale" first. This file assumes them, and corrects that paragraph where the survey found it wrong.
Planning effort: medium, as the master plan sets for this phase. The work is host-only and follows two existing patterns: raw create's open-and-truncate, and convert's monolithicFlat descriptor. The survey's one surprise is a naming divergence in code this phase shares (decision 3).
Decisions already taken by the operator¶
Every open question in the master plan was resolved with its recommendation adopted (Michael Still, 2026-10-21). The two this phase carries out:
- Open question 1: preallocation of vmdk, vpc and vhdx is qemu
parity.
fallocandfullproduce exactly the imageoffwould, andmetadatais refused without promising future support. - Open question 2:
create -f vmdk -o subformat=monolithicFlatis written by the host with no guest. It is a sparse extent plus the descriptor convert already writes, and it refuses a backing file as qemu does.
Neither is revisited here.
Scope¶
In:
createaccepts-o preallocation=falloc|fulland--preallocation falloc|fullfor vmdk, vpc and vhdx. Each produces theoffimage, andmetadatais refused in qemu's terms.measureaccepts the same through-o, and refusesmetadatathrough-oand--preallocationalike. Its answers do not change.- One host function names a monolithicFlat extent the way qemu does.
convertuses it, which fixes #820, andcreateuses it too. create -f vmdk -o subformat=monolithicFlat(and--subformat monolithicFlat) works on the host. It writes a sparse extent under every preallocation mode plus the descriptor, and refuses a backing file,zeroed_grain=onand a grain size.- Tests that use the phase 1 pin as the oracle, and the per-phase
documentation:
docs/create.md,docs/measure.md,docs/convert.md,docs/format-coverage.mdanddocs/quirks.md.
Out:
- VHDX
subformat=fixed. It is refused exactly as today, sosubformat=fixed,preallocation=fallocstill fails on the fixed refusal. Phase 7 lifts it. - Cross-version
CREATE_CASESbaselines for the new cases. They go to phase 10, with the fixed VHDX cases phase 2 moved there (decision 5). - qemu's DDB lines in VMDK descriptors. Neither instar VMDK writer emits them today. They are recorded in Future work (decision 4).
- Any guest, crate or call-table change. Nothing under
src/operations/orsrc/crates/changes, and nor does theCreateConfigorMeasureConfiglayout.
What the survey found¶
Line numbers are against develop at 5c5bba75. The Situation's
line numbers for src/vmm/src/main.rs have drifted by about 30 lines
since it was written. Its claims still hold, so it is left as it is,
and the numbers below are the current ones.
The refusals are where the Situation says¶
- create
-oparser:parse_create_o_options(main.rs:15276). Its("vmdk" | "vpc" | "vhdx", "preallocation")arm (:15487-15505) acceptsoffand refuses the other three modes with "is not yet supported ... (preallocation is implemented for raw and qcow2 only)". The vmdksubformatarm (:15418-15430) already acceptsmonolithicFlat.zeroed_grain,adapter_type,hwversionandtoolsversionare accept-ignored and not stored (:15434-15440). validate_create_args(:19422) refuses monolithicFlat (:19498-19503, "multi-file subformats are not implemented; use instar convert -O vmdk for now"). It also refuses non-offpreallocation for the three formats a second time (:19533-19553). A unit test pins the old message (:20012), and so does an integration test (tests/test_create.py:1600-1606).run_create(:18223) applies-ooverrides, validates, and sends raw torun_create_raw(:19125) and everything else to the guest (run_create_nonraw,:18331).
The guest already ignores preallocation for these formats¶
run_create_nonraw turns args.preallocation into
CREATE_CONFIG_PREALLOC_* flag bits for every format
(:18354-18359). The create guest reads them only for qcow2
(src/operations/create/src/main.rs:399-404 and :1089), and the
host post-pass is qcow2-only (main.rs:18566-18567). Accepting
falloc and full would therefore produce the off image
unchanged. The host still sends PREALLOC_OFF for these formats
(decision 1), so that this stays true however the guest changes.
measure is half there already¶
measure --preallocation is accepted for vpc, vmdk and vhdx, and has
no effect. --preallocation metadata is accepted too; measured with
a build of da9f043f, measure -O vpc --size 64M --preallocation
metadata prints the same answer as off. -o preallocation=... is
refused as an unrecognised key for all three (parse_o_options,
:15018; the catch-all is at :15186). The two spellings disagree.
qemu-img cannot measure any of the three formats, so instar's own
writer is the only oracle.
convert names the flat extent differently from qemu (#820)¶
convert derives the extent as <file_stem>-flat.vmdk
(main.rs:14297-14309). qemu splits the basename at its last . and
writes <prefix>-flat<postfix>: see filename_decompose, and the
"%s-flat%s" format in vmdk_co_do_create (block/vmdk.c:2279-2316
and :2599 in qemu 7.0.0). Measured with qemu-img 10.0.13 and the
da9f043f build:
| Output | qemu-img extent | instar convert extent |
|---|---|---|
out.vmdk |
out-flat.vmdk |
out-flat.vmdk |
out.img |
out-flat.img |
out-flat.vmdk |
out |
out-flat |
out-flat.vmdk |
.hid |
-flat.hid |
.hid-flat.vmdk |
Filed as #820. The image is valid either way, because the descriptor
names the extent that was written. The two differ only when the output
name does not end in .vmdk.
qemu also searches for \ and then : when the name has no /.
Nothing on a Linux host uses that, and on a:b.vmdk it would put the
extent in the working directory while the descriptor resolves it next
to itself, so instar does not copy it (decision 3).
What qemu does around monolithicFlat create¶
These were measured with qemu-img 10.0.13, and the pin covers all 80 builds where it says so.
- The extent is sparse, with
ftruncateto the virtual size underoff,fallocandfull. Its length is the size rounded up to 512: a1000request gives a 1024-byte extent andRW 2 FLAT. The pin'sprealloc-vmdk-monolithicFlatandvmdk-flat-64Mcases cover this. - With
-b, qemu fails: "Flat image can't have backing file" (block/vmdk.c:2429; pinvmdk-flat-backing, all 80 builds). - With
zeroed_grain=on, qemu fails: "Flat image can't enable zeroed grain" (:2434), and leaves no file. - With
preallocation=metadata, qemu fails: "Unsupported preallocation mode: metadata", and leaves a zero-byte descriptor. - The descriptor carries
ddb.virtualHWVersion,ddb.geometry.*andddb.adapterType, and from 6.2.0 alsoddb.toolsVersion. The pin has two groups at exactly that boundary.adapter_typeandhwversionchange those lines. instar'sbuild_flat_descriptor(src/crates/vmdk/src/lib.rs:261) writes none of them and a constantCID=fffffffe. Its sparse writer omits the DDB lines too, so this is not specific to flat output.
The pin is a ready oracle, but not for absolute lengths¶
The phase 1 pin (tests/golden/qemu-create-pins.json) holds seven
preallocation cases and two monolithicFlat cases, one group each
across all 80 builds except vmdk-flat-64M. qemu_pins.run_case
runs <binary> create -f FMT -o ... IMAGE SIZE, which is also
instar's command line, so the same runner can measure instar. Its
file_length fields cannot be compared directly, though. instar's 64M
images are 11264 bytes (vmdk), 4194304 (vhdx; its default block size
is #645) and 67109376 (fixed vpc; qemu rounds to CHS) against qemu's
65536, 8388608 and 67125760. Only dynamic vpc agrees, at 2560. What
transfers is the pin's shape: every mode's rc, same_bytes_as_off
and same_allocation_as_off. Decision 5 builds the tests on that.
Master plan corrections made in this commit¶
- The phase 3 paragraph of "Sequencing rationale" said the phase's tests were cross-version info baselines. It now says the pin is the oracle, that the baselines join phase 10's, and that the phase fixes #820.
- The phase 10 paragraph now lists the monolithicFlat create cases
among the
CREATE_CASESit adds. - Future work gains the VMDK descriptor DDB lines.
- The Execution table and the
docs/plans/index.mdrow link this file.
Decisions¶
-
The host normalises preallocation for vmdk, vpc and vhdx. Once validated,
fallocandfullare recorded as they were typed inargs.preallocation. For these three formats,run_create_nonrawsendsCREATE_CONFIG_PREALLOC_OFF. The guest ignores the bits for these formats today, but the parity claim should not rest on that: a future guest reading the bit for vhdx would otherwise change everyfallocimage silently. No guest change is needed. -
One refusal text for
metadata, shared by create and measure. It reads<op>: preallocation=metadata is not supported for <format>, built by one helper both validators call. It drops "not yet" and "future work", because qemu refuses it at every version and the plan does not intend to add it. The create-oparser andvalidate_create_argsboth use it, as the old text did. For measure, the check sits inrun_measureafter-ohas been applied (:15805-15860), so--preallocationand-ocannot disagree again. Making measure's--preallocation metadataan error for these formats is a deliberate tightening. qemu has no measure answer for them, and measure is defined as a prediction of instar's own writer, which will refuse the mode. -
The extent name follows qemu's rule, in one function, and convert changes with it. Add a host function, for example
vmdk_flat_extent_path(output: &Path) -> (PathBuf, String), returning the extent path and the name the descriptor records. It takes the basename after the last/, splits it at its last., and returns<prefix>-flat<postfix>in the output's directory. This covers a leading dot (.hidgives-flat.hid) and no dot at all (outgivesout-flat). It does not split on\or:(survey). Both convert and create call it.
This changes convert's output for any name not ending in .vmdk.
That is the decision a reviewer is most likely to question,
because it alters an existing command's behaviour inside a phase
about create. Three things justify it:
- The alternative is two naming rules, or giving create the wrong rule on purpose.
- The change only moves instar to the name qemu-img writes.
- The image stays readable, because a descriptor always names its own extent.
The commit and the PR say "Fixes #820", and docs/convert.md
states the rule.
- The descriptor stays as convert writes it, without DDB lines and
with a constant CID. Matching qemu would mean honouring
adapter_type,hwversionandtoolsversion(all accept-ignored), computing qemu's geometry, and picking one side of the 6.2.0toolsVersionboundary. The same work would be owed to the sparse writer. Neitherqemu-img infonor any reader instar or qemu ships uses those lines, so the work buys only byte-level fidelity. It goes to Future work.docs/quirks.mdrecords the omission, and #566 already covers the constant CID.
adapter_type, hwversion and toolsversion stay accept-ignored
for monolithicFlat. Three options are refused rather than ignored:
* -o zeroed_grain=on, as qemu refuses it. The parser stores the
value instead of dropping it.
* A grain size, through --grain-size or -o grain_size. Its
default is the 0 sentinel; anything else has no meaning for a
flat extent.
* -b, with instar's wording of qemu's refusal.
- The tests take the pin as oracle for shape, and instar's own
offimage for lengths. New tests intests/test_create.pyrunqemu_pins.run_case(<instar binary>, case, workdir)on these cases: prealloc-vpc,prealloc-vmdkandprealloc-vhdx;prealloc-vpc-fixed;prealloc-vmdk-monolithicFlat, once step 3c lands;vmdk-flat-64Mandvmdk-flat-backing, after 3c as well.
For each preallocation case and mode, rc, same_bytes_as_off
and same_allocation_as_off must equal the pin's
qemu_pins.expected(case). file_length must equal that of
instar's own off image in the same run, and metadata's
stderr must be decision 2's text. For vmdk-flat-64M:
* extent_length must equal the pin's;
* the normalised descriptor must equal the pin's newest group with
every ddb. line removed (decision 4);
* CID is already normalised by normalise_descriptor.
prealloc-vhdx-fixed goes in a known-refusal table with the
subformat=fixed message. The table must fail with "remove this
entry" once phase 7 lets the case pass, the pattern
tests/test_vhdx_fixed_fixtures.py uses. Live comparisons against
the host qemu-img cover what the pin cannot, and skip when
qemu-img is absent:
* the four extent-name shapes, through both create and convert;
* qemu-img info --output=json of an instar flat image against
qemu's own, ignoring filename, cid, actual-size, and the
filename and actual-size inside children.
The cross-version CREATE_CASES baselines stay in phase 10. Adding
a case to the instar mirror without its generated baselines fails
test_create_cases_match_baselines. Generating them is the
80-version testdata procedure phase 10 owns, and phase 2 already
moved the fixed VHDX cases there for the same reason.
- monolithicFlat create cleans up after itself, and stays sparse. The steps, in order:
- Validate everything first: size, backing, preallocation,
zeroed_grainand grain size. - Open the extent with
create(true).truncate(true)and mode 0644, asrun_create_rawdoes, andset_lenit to the size rounded up to 512. Do not callcreate_raw_finalize: itsfallocandfullreally allocate, and qemu's extent stays sparse under every mode. - Write the descriptor with
build_flat_descriptor, sized from the same rounded value, and sync both files.
On any failure after a file is opened, remove both files. That
includes a descriptor too long for its buffer (the function
returns the length it needed, so check it), and it matches what
every other instar create does on failure. qemu leaves a
zero-byte descriptor on some failures; instar does not copy that.
The success line and JSON report the descriptor as filename and
the descriptor's length as both metadata_bytes_written and
file_size_after, with resolved_unit_size 0.
Steps¶
| Step | Effort | Model | Isolation | Brief for sub-agent |
|---|---|---|---|---|
| 3a | medium | sonnet | none | No-op preallocation for vmdk, vpc and vhdx. First read decisions 1, 2 and 5 of docs/plans/PLAN-bat-population-phase-03-host-create.md, and its survey. In src/vmm/src/main.rs: (1) in parse_create_o_options (:15487-15505), accept falloc and full for vmdk, vpc and vhdx; refuse metadata with decision 2's text, built by a new helper that measure also uses. (2) Do the same in validate_create_args (:19533-19553) and fix its comment. (3) In run_create_nonraw's flag mapping (:18354-18359), send CREATE_CONFIG_PREALLOC_OFF for these three formats. (4) In measure's parse_o_options (:15018), add a preallocation arm for vpc, vmdk and vhdx accepting off, falloc and full; put the metadata refusal in run_measure after -o is applied (:15805-15860), so both spellings refuse it. Update the unit test at :20012, and add unit tests for each format and mode in both parsers and the validator. In tests/test_create.py, replace :1600-1606 with decision 5's preallocation tests for prealloc-vpc, prealloc-vmdk, prealloc-vhdx and prealloc-vpc-fixed. Use helpers.qemu_pins (run_case, expected); do not reimplement the volatile-byte mask. Put prealloc-vhdx-fixed and prealloc-vmdk-monolithicFlat in a known-refusal table, each with the message it fails with today (step 3c removes the second). Add measure tests: for each of the three formats, -o preallocation=falloc and full print the same answer as off, and metadata is refused through -o and through --preallocation. Documentation: docs/create.md (the per-format -o lists, the "Preallocation modes" table, the Future work bullet on vmdk/vpc/vhdx preallocation), docs/measure.md :91 and nearby, and docs/quirks.md's "Preallocation accept set" (:1104-1117). Each must say the modes are accepted and change nothing, as qemu's are, and that resize --preallocation on these formats does allocate (docs/format-coverage.md's resize table). Commit subject: Accept no-op preallocation for vmdk, vpc and vhdx. |
| 3b | medium | sonnet | none | Name flat extents the way qemu does (#820). First read decision 3 and the #820 survey section of docs/plans/PLAN-bat-population-phase-03-host-create.md. In src/vmm/src/main.rs, add the naming function decision 3 describes, near the convert host path, with a doc comment citing block/vmdk.c:2279-2316 and :2599 (qemu 7.0.0, under instar-testdata/qemu-sources/7.0.0/). Replace convert's inline rule (:14297-14309) with it. Unit tests: the survey table's four rows, plus a name inside a directory and a.b.c (giving a.b-flat.c). In tests/test_convert.py, near the existing monolithicFlat tests (:2470-2560), add a test that converts to out.img and out and asserts the extent names and the descriptor's FLAT "..." line. When the host qemu-img is present, it must also assert the same names as qemu-img convert -O vmdk -o subformat=monolithicFlat writes. The existing .vmdk tests must pass unchanged. In docs/convert.md, near :83, state the naming rule. Commit subject: Name flat VMDK extents the way qemu does. with Fixes #820. in the body. |
| 3c | medium | opus | none | Create monolithicFlat VMDK on the host. First read decisions 3 to 6 and the "What qemu does around monolithicFlat create" survey section of docs/plans/PLAN-bat-population-phase-03-host-create.md. In src/vmm/src/main.rs: remove the refusal in validate_create_args (:19498-19503). Refuse -b ("create: vmdk monolithicFlat cannot have a backing file"), zeroed_grain=on (store it in CreateOptionOverrides; it is accept-ignored at :15434-15440 today) and a non-zero grain size for monolithicFlat. Add run_create_vmdk_flat, dispatched from run_create (:18223) beside the raw branch, implementing decision 6 with 3b's naming function and vmdk::build_flat_descriptor. Do not touch create_raw_finalize or the guest. Unit tests for every new refusal. In tests/test_create.py: remove prealloc-vmdk-monolithicFlat from 3a's known-refusal table so it runs under decision 5. Add vmdk-flat-64M and vmdk-flat-backing under decision 5's rules. Add tests that: a 1000-byte request gives a 1024-byte extent; the extent's allocated blocks under full equal those under off (os.stat(...).st_blocks); instar info and instar check read the image; the live qemu-img info comparison passes; create with an output name of out puts the extent where qemu does; and a failure after opening (for example a read-only directory or a pre-existing unwritable extent) leaves neither file. Documentation: docs/create.md (vmdk -o lists, the "Rejected" list at :131-132, the Future work bullet at :392-394, whose stated reason is false), docs/format-coverage.md (:331 and the create roster near :155), and docs/quirks.md (a short section on the descriptor omitting qemu's DDB lines and writing a constant CID, citing #566). Commit subject: Create monolithicFlat VMDK on the host. |
The management session runs the steps in order. 3b and 3c both edit
the convert and create host paths in main.rs, and 3c uses 3b's
function and 3a's known-refusal table. Every brief also carries the
session's standing rules:
* Build only with make instar (Docker); never install a Rust
toolchain on the host.
* Run make test-rust, redirecting cargo's target dir to an owned
directory if pre-commit has left src/target root-owned.
* Run the touched test modules with stestr from tests/, with
INSTAR_BINARY_PATH pointed at the worktree's build.
* Run pre-commit run --all-files after git add -N on new files,
and tools/ci/check-test-partition.sh.
* Keep plan, phase, step and decision references out of code and
docs.
* Keep a mutation script under the step's scratch directory, and run
each mutant under timeout.
* Leave the work uncommitted.
Execution notes¶
- Back brief, 2026-10-23 (AEDT). The operator chose to land the convert rename (#820) in this phase, and allowed the management session to push the branch and open the pull request once the definition of done passes.
- 3a. Two corrections in review. Decision 1 now covers measure as
well as create: the host tells the measure guest "off" for vmdk, vpc
and vhdx too. A stale comment in
validate_create_argswas also fixed. The agent added anfsynctoqemu_pins._prealloc_create. Without it,st_blockswas read before delayed allocation settled, and the vhdx case failed about one run in three, even comparingoffwithoff. It was kept, because the pin sweep benefits from it too. - 3b. The agent found that
convertrounds a raw source's virtual size up to the next 64 KiB for every output format; qemu-img keeps it exactly. The management session reproduced it onda9f043fand on this branch and filed it as #821. It is unrelated to extent naming and out of scope here. 3b's tests use a 1 MiB source so that they do not depend on it, and 3c's create path does not go through convert. - 3c. The agent turned up two defects outside this phase's scope. The management session reproduced both and filed them:
-
822:
instar checkexits 63 on any monolithicFlat VMDK,¶including one qemu-img made, although
docs/check.mdsays it validates them. 3c's test therefore only requires that check treats a created image the way it treats a converted one. -
823: instar's sparse VMDK descriptors name their extent¶
"output.vmdk", whatever the file is called.
The descriptor comparison with the pin removes the ddb. lines and
collapses trailing newlines to one, because qemu's blank line after
#DDB is left at the end once those lines go.
Risks and mitigations¶
- A no-op preallocation hides a guest that starts honouring the
bit. Decision 1 sends
OFF, and the tests compare every mode's bytes and allocation withoff. The management session confirms that a mutant sending the user's mode instead is caught. It is caught only if the guest reacts to the bit, so that mutant is expected to survive, and its survival is the reason for decision 1, not a gap. - The convert rename breaks a caller expecting
-flat.vmdk. Only non-.vmdknames change. The management session grepstests/,scripts/andtools/for-flat.vmdkbuilt from a non-.vmdkoutput before committing 3b. The phase 3 survey found only.vmdkoutputs (tests/test_convert.py:2480-2550). - Cleanup removes a file the user cared about. Both files are opened with truncate, as qemu opens them, so a failure after opening has already destroyed the old contents. Removing them is no worse, and a validation failure removes nothing because validation runs first. The management session reads 3c's diff for any removal reachable before the first open.
- The pin comparison passes vacuously. If
run_caseraised, orexpectedreturned nothing for a case, a loose test could pass. The mutation scripts break the no-op (allocate underfull), the refusal text, the naming rule and the cleanup, and each must be killed.
Definition of done¶
Each item is a command, run from the worktree root, with its expected result.
git diff --name-only origin/develop...HEAD -- src/lists onlysrc/vmm/src/main.rs.- With the worktree's build in a scratch directory,
instar create -f vpc -o preallocation=full a.vhd 64M,instar create -f vmdk --preallocation falloc a.vmdk 64Mandinstar create -f vhdx -o preallocation=full a.vhdx 64Meach exit 0, andinstar create -f vpc -o preallocation=metadata b.vhd 64Mexits 1 printingpreallocation=metadata is not supported for vpc. instar measure -O vhdx --size 64M -o preallocation=fullprints whatinstar measure -O vhdx --size 64Mprints, andinstar measure -O vpc --size 64M --preallocation metadataexits 1.instar create -f vmdk -o subformat=monolithicFlat,preallocation=full out.img 1000exits 0. It leavesout.imgand a 1024-byteout-flat.img, andstat -c %b out-flat.imgprints what it prints for the same command withpreallocation=off.instar convert -O vmdk --subformat monolithicFlat src.raw out.imgwritesout-flat.img.instar create -f vmdk -o subformat=monolithicFlat -b base.vmdk -F vmdk c.vmdk 64Mexits 1 and leaves noc.vmdkorc-flat.vmdk.make test-rustpasses.- The stestr run of
test_create,test_convert,test_measureandtest_qemu_create_pinsreports "Ran: N" equal to the tests in those modules, with 0 failures. The only skips are missing-binary and missing-driver ones. tools/ci/check-test-partition.shexits 0.git diff origin/develop...HEAD -- src tests docs/*.md | grep -nE 'PLAN-|[Pp]hase [0-9]|step 3[a-c]'prints nothing.grep -n 'not yet supported' docs/create.mdfinds no line about vmdk, vpc or vhdx preallocation or monolithicFlat.git add -A && pre-commit run --all-filespasses.
Back brief¶
Before executing any step, back brief the operator on your understanding of this phase and how the intended work aligns with it. Ask then whether the convert rename (decision 3) should land in this phase, or stay as #820 for a pull request of its own. It is the one change here to an existing command's output.