qcow2 write infrastructure — phase 01: semantics pin¶
Parent: PLAN-qcow2-write-infrastructure.md. Planned at high effort.
Scope¶
Investigation-only phase: no production code changes. Four questions are settled empirically and by code inventory, and the findings are recorded back into the master plan (a new "Findings: phase 1 semantics pin" section under Agent guidance, following the precedent of PLAN-bench.md's OQ4 findings section, with live probe transcripts). Probe scripts live in the session scratchpad, not the tree — the findings write-up must therefore be self-contained (fixture recipes, exact commands, observed output).
The phase closes with master-plan updates: OQ1-OQ3 resolved (or explicitly narrowed), the phase 2 row confirmed live or collapsed into a documentation note, and — if a live corruption defect is confirmed — a GitHub issue drafted for the operator to review before filing.
Facts already established at master-plan time (do not re-derive; verify only if a probe contradicts them):
- commit has no
nb_snapshotsgate and no COPIED/refcount check before overwriting backing clusters (src/operations/commit/src/main.rs, verified by grep; blind overwrite at :772). rebase likewise has no snapshot gate (src/operations/rebase/src/main.rs,src/crates/rebase/src/qcow2.rs, verified by grep). - bench refuses
nb_snapshots > 0(src/operations/bench/src/main.rs:642). - The commit / rebase differential-fuzz oracles are normalised
qemu-img info --output=jsoncomparison, not byte identity:op_commitcompares info JSON on both the resulting overlay and backing (scripts/differential-fuzz.py:2837and the comparison loop near :2960);op_rebasecompares info JSON (:2595).op_benchis sha256 on raw butqemu-img compare+qemu-img checkmetrics on qcow2 (:2429, contract at :2437-2445). - The cross-version baselines for commit and rebase are
info-equivalence, not byte:
instar-testdata/expected-outputs/commit-backing-info-json,commit-overlay-info-json,rebase-info-json. - Pinned static qemu-img builds live at
../instar-testdata/qemu-img-binaries/x86_64/<version>/qemu-imgcovering 6.0.0 through 10.2.0 (79 versions); the system qemu-img is 10.0.8.
Q1 — qemu-img commit into a snapshot-bearing backing¶
What does qemu-img commit do when the backing file has internal
snapshots, and what does instar commit do to the same fixture?
Fixture recipe (one probe script parameterised over the matrix):
qemu-img create -f qcow2 -o cluster_size=<cs> base.qcow2 64M.- Write pattern A into a known region via
qemu-io -c 'write -P 0xaa <off> <len>' base.qcow2. qemu-img snapshot -c snap1 base.qcow2— the A clusters are now snapshot-shared (refcount 2, COPIED clear).- Optionally write pattern B over part of A's region (post- snapshot COW inside base, exercising a mixed refcount state).
qemu-img create -f qcow2 -b base.qcow2 -F qcow2 overlay.qcow2and write pattern C into the overlay: one extent overlapping A's region (forces the commit to write into snapshot-shared clusters), one extent in never-allocated space (forces fresh allocation).- Duplicate the whole directory byte-for-byte for the instar side before any commit runs.
Pre-commit record, per side: sha256 of base; qemu-img map
--output=json base.qcow2; snap1's full content (apply snap1 on
a copy of base, qemu-img convert it to raw, sha256).
Probe, qemu side: qemu-img commit overlay.qcow2. Post-commit
record: exit code; qemu-img check metrics on base; qemu-img
snapshot -l still lists snap1; snap1's content re-extracted the
same way and compared to the pre-commit extraction (the core
question: preserved or corrupted?); active content of base
converted to raw and compared against the expected merge; whether
base's file length grew (COW allocating new clusters) and where
the committed clusters landed (map diff).
Probe, instar side: identical steps with instar commit. If
instar corrupts snap1 (expected from code reading: the blind
overwrite lands in the shared cluster), capture the full
divergence: check metrics (corruptions/leaks), snap1 content
diff, active content diff vs qemu's result.
Matrix: cs ∈ {512, 65536}; with and without step 4; committed
extent overlapping-shared vs fresh-only (the fresh-only row is
the control — both tools should agree there); qemu versions
{6.2.0, 7.2.22, 8.2.10, 9.2.4, 10.2.0, system 10.0.8} from the
pinned-binaries tree (fixture generation and observation with the
same version as the commit under test). instar is the current
develop-built binary throughout (make instar; see AGENTS.md
for build and run conventions).
Also probe the second-order case: commit when the overlay (not the backing) has internal snapshots — qemu-img may refuse; record the message and exit code, and what instar does.
Q2 — rebase safe mode on a snapshot-bearing overlay¶
Same shape as Q1. Fixture: base_old.qcow2 and base_new.qcow2
with differing content in known regions; overlay backed by
base_old; write into the overlay; qemu-img snapshot -c snap1
overlay.qcow2 so the overlay's active L1/L2/data are
snapshot-shared; then qemu-img rebase -b base_new.qcow2 -F qcow2
overlay.qcow2 (safe mode, no -u).
The questions: does qemu refuse, or proceed and COW the shared
L2 tables / data clusters it must rewrite (observable as new
allocations in the map diff and an unchanged snap1 content
extraction)? What does instar rebase do to the identical twin
fixture — refuse, corrupt snap1, or match? Matrix: cluster sizes
{512, 65536}, versions as Q1.
Q3 — parity-oracle inventory and COW placement determinism¶
Two deliverables:
- An oracle inventory table (goes into the master-plan
findings) listing, for each v1 consumer (commit, rebase,
bench
-w), every parity surface with file:line — unit tests, integration tests (tests/test_commit.py,tests/test_rebase.py,tests/test_bench.pyincludingKNOWN_BENCH_DIVERGENCES), cross-version baselines (instar-testdata/expected-outputs/*), differential-fuzz oracle (scripts/differential-fuzz.pyop functions), and any docs/quirks.md entries — each classified as byte-identity / virtual-content (qemu-img compare) / info-equivalence / check-clean. The master plan's OQ3 assumed byte-parity oracles for commit and rebase; initial inventory during planning found info-equivalence instead, which if confirmed widens the layout freedom available to COW and refcount growth. The inventory must be exhaustive so the conclusion is safe to build on. - A COW placement determinism probe: on the Q1 fixture,
run
qemu-img commitN=5 times on byte-identical copies under one version — is the resulting base byte-identical run-to-run? Then across the Q1 version matrix — is placement stable version-to-version? This decides whether byte-parity with qemu COW is even achievable if a future oracle demands it, and therefore how OQ3's recommendation is worded.
The synthesis must also state the migration-proof oracle for phases 4-6: regardless of qemu-parity strictness, the refactor phases prove byte-invisibility as instar-before vs instar-after (same fixture corpus through the pre-migration and post-migration instar binaries, sha256 equality on every output), which is independent of qemu layout freedom. Confirm a fixture corpus for that proof exists or specify what phase 4 must build.
Q4 — memory budget and API-shape survey¶
Code-reading only. Tabulate, for the three guest ops to be
migrated (src/operations/commit/src/main.rs,
src/operations/rebase/src/main.rs scratch map at :57-95,
src/operations/bench/src/main.rs scratch map at :57-165):
- every scratch region (name, base, limit, static asserts),
including staged L2 sets, staged refblocks
(
WRITE_REFBLOCKS_LIMITetc.), bounce buffers and the bump-allocator heap boundary; - current guest binary size vs its cap
(
scripts/check-binary-sizes.sh, and note the .bss-aware size lint history — the amend HEADER_MISMATCH incident — as the constraint class to respect); - whether a shared staging struct sized for the v1 envelope plus
a one-cluster COW bounce buffer (worst case 2 MiB at
MAX_CLUSTER_SIZE) fits every op's existing map without moving regions, or which op needs its map re-laid; - a recommendation for master-plan OQ4 (step-program buffer vs incremental query API) with arithmetic: worst-case steps per cluster write (COW read, zero/copy fill, data write, L2 patch, L1 patch, refcount RMWs, barriers) × a bounded in-flight window → bytes of step buffer, compared against available scratch.
Steps¶
| Step | Effort | Model | Isolation | Brief for sub-agent |
|---|---|---|---|---|
| 1a | high | default (Fable) | none | Q1 probe. Write a parameterised probe script in the session scratchpad implementing the Q1 fixture recipe, record matrix and observation set exactly as specified in Scope §Q1 (six qemu versions from ../instar-testdata/qemu-img-binaries/x86_64/<v>/qemu-img, instar from make instar). Run the full matrix, emit a per-row verdict table (qemu behaviour; instar behaviour; agree/diverge/corrupt), and capture representative raw transcripts for the findings write-up. The core deliverable is the answer to "does instar commit corrupt backing-file snapshots that qemu preserves?" with evidence. Do not modify tree files. |
| 1b | high | default (Fable) | none | Q2 probe. Same discipline as 1a with the Scope §Q2 fixture (snapshot-bearing overlay, safe-mode rebase to a differing backing). Deliverables: per-row verdict table, transcripts, and the answer to whether instar rebase corrupts overlay snapshots or diverges from qemu. Reuse 1a's probe scaffolding where practical. |
| 1c | high | default (Fable) | none | Q3. Produce the exhaustive oracle inventory table per Scope §Q3 (read tests/test_commit.py, tests/test_rebase.py, tests/test_bench.py, scripts/differential-fuzz.py op_commit/:2837 op_rebase/:2595 op_bench/:2429, ../instar-testdata/expected-outputs/{commit-backing,commit-overlay,rebase}-info-json generation in instar-testdata's generate-baselines.py, docs/quirks.md) with file:line and a strictness classification per surface. Then run the COW determinism probe (N=5 same-version repeats + cross-version sweep on 1a's overlapping-shared fixture, sha256 of resulting base). Conclude with a written recommendation resolving master-plan OQ3, and the phases 4-6 migration-proof oracle statement (instar-before vs instar-after byte equality; name the fixture corpus or specify what phase 4 must build). |
| 1d | medium | default (Fable) | none | Q4. Code-reading survey per Scope §Q4: scratch-region tables for the commit/rebase/bench guest ops with file:line, binary sizes vs scripts/check-binary-sizes.sh caps, feasibility verdict for a shared staging struct + 2 MiB COW bounce buffer per op, and the OQ4 API-shape recommendation with the step-buffer arithmetic. No code changes; output is a findings section ready to paste. |
| 1e | medium | default (Fable) | none | Synthesis. Fold 1a-1d outputs into the master plan: add a "Findings: phase 1 semantics pin" section under Agent guidance (tables + representative transcripts, self-contained since probe scripts are ephemeral); mark OQ1-OQ3 resolved with one-line pointers to the findings; update OQ4/OQ5 with 1d's recommendation; set the phase 1 row to Complete and the phase 2 row to live (with a one-line defect summary) or collapsed (with the refusal/match evidence); draft — do not file — a GitHub issue body if a corruption defect is confirmed, for operator review. Update the plans index status column. One commit for the documentation update, following the repo commit-message conventions. |
Steps 1a, 1b and 1d are independent and can run concurrently; 1c's determinism probe depends on 1a's fixture scaffolding (sequence it after 1a or have it rebuild the one fixture it needs); 1e depends on all of them.
Review checklist deltas¶
Standard checklist from PLAN-TEMPLATE.md, minus the build/test gates for 1a-1d (no tree changes to build), plus:
- Confirm no probe artefacts or scripts were committed to the tree; the only tree change from this phase is the 1e documentation commit.
- Cross-check the 1a/1b verdict tables against the raw transcripts before accepting a "corruption confirmed" or "no defect" conclusion — the conclusion gates whether phase 2 exists and whether an issue is filed.
- Verify the master-plan findings section is self-contained (fixture recipes and commands reproducible without the scratchpad).
pre-commit run --all-filespasses on the 1e commit.