Differencing phase 11: host chain discovery and info --chain¶
Prompt¶
Phase 11 of PLAN-differencing.md, the first of
the six composition phases. The master plan describes it as "resolving a
locator path to a real parent, applying the same resolution and depth
rules qcow2 backing chains get, and attaching each chain member as its
own virtio device", ending with "info --chain walking a VHD or VHDX
chain, which is the cheapest possible proof the host half works and
needs no guest change at all" (PLAN-differencing.md:487-493).
The survey below found that two of those three clauses describe code that already exists, so this phase is materially smaller than the master plan's section implies. What is left is one gate, one resolution rule per format, and the output of one command.
Planning effort¶
High. The master plan's per-phase effort list stops at phase 10
(PLAN-differencing.md:603-608) and says nothing about 11. High is the
right level anyway, and not because the diff is large: the phase turns
on a security-boundary question (what a host may resolve out of an
untrusted image) and on an invariant an earlier phase deliberately
established and this phase must not break. Both are decided below
rather than left to the implementer.
Review effort: high, concentrated on one question -- whether any
operation's refusal became contingent on a parent file existing. That
is the failure this phase is exposed to, and it is invisible in a green
test run unless the test was written to look for it. Phase 8 wrote one;
see TestDifferencingParentAbsent (tests/test_differencing.py:609).
Scope¶
In scope.
- The VHD/VHDX gate in
discover_backing_chain(src/vmm/src/main.rs:2701-2707) and the walk policy that replaces it. - Resolving a VHD parent and a VHDX parent to a real file on the host, under the existing allowlist and depth rules.
instar info --chainoutput for a walked chain, including its--output jsonform.docs/chain-discovery.mdanddocs/info.md, which state the one-image behaviour as current truth and carry a reason that is already false.- Rust unit tests and Python integration tests for all of the above.
Out of scope.
- Any guest change. The guest chain machinery is qcow2-specific
(
qcow2::ChainStates,qcow2::init_chain_statesatsrc/crates/qcow2/src/lib.rs:9442); giving VHD and VHDX a compose path is phases 12 and 13. - Lifting any operation's refusal. Every composing operation keeps the gate and keeps refusing, byte for byte. That is phase 14, operation by operation.
- Parent identity verification -- see decision 5.
- The VHD parent locator table as a resolution source -- see decision 3.
- The read-side rows of
docs/format-coverage.mdand the compose sections ofdocs/chain-config.md, which stay with phase 16. - Six defects, none of them this phase's to fix. Two were found by
this survey and filed during it: the silent chain-device truncation
(#601) and the VHD locator-table resolution gap (#602). Two were
already open and are recorded here because composition depends on
them: #565, where
resizegrows a differencing VHDX without its parent, and #566, which makes parent identity vacuous for any chain instar wrote end to end -- see decision 5. Four more came out of the review rounds on the pull request. Round 1: #608, that the walk never checks a resolved parent is the same format as its child and that errors past the differencing link are not fail-soft, and #609, that image-derived path strings reach the terminal unescaped in the human output forms (the JSON forms escape them). Round 2: #611, that resolution probes an absolute reference before the allowlist is consulted, so the stderr reason distinguishes whether an attacker-chosen host path exists -- pre-existing for qcow2, but now reachable from a command that exits 0; and #612, that the file carries two JSON escape helpers of which only one escapes the C0 range. #608's and #611's boundaries are both documented indocs/chain-discovery.mdrather than left implicit. #609 and #611 are each wider than this change: the older stdout printer carries the same strings, andresolve_backing_pathis shared with qcow2 and VMDK.
What the survey found¶
Measured against 036252f (the phase 10 merge) on 2026-09-28, with a
binary built from that tree by make instar. Four of the findings below
are measured rather than read: F1, F4, F9 and the absent-parent baseline.
The commands and their exact output:
$ instar info --chain vhd-diff-child-aligned.vhd
Chain: 1 image(s)
[0] .../vhd-diff-child-aligned.vhd (vpc) -> vhd-diff-parent.vhd
virtual size: 16 MiB (16777216 bytes)
disk size: 4 MiB (4198912 bytes)
cluster size: 2097152 bytes
# rc 0
$ instar info --chain vhdx-diff-child.vhdx
Chain: 1 image(s)
[0] .../vhdx-diff-child.vhdx (vhdx) -> vhdx-diff-parent.vhdx
... # rc 0
The VHDX line is F4 measured: the parent reads vhdx-diff-parent.vhdx,
not .\vhdx-diff-parent.vhdx, so the Windows-path rationale the gate
still gives is already spent. Renaming the parent out of the way changes
neither output nor exit status -- byte-identical, rc 0 -- which is the
absent-parent baseline the definition of done compares against. And
instar info --chain --output json vhdx-diff-child.vhdx prints exactly
the human text above and exits 0, which is F9.
F1. info --chain already exists, and already reports the parent.
run_info handles args.chain at src/vmm/src/main.rs:10269-10282:
it discovers the chain, calls print_backing_chain and returns before
any guest launches. print_backing_chain (:2724) already renders
backing_file_raw as -> parent, so a differencing child today prints
a one-image chain annotated with the parent it did not follow -- measured
above, for both formats. The flag,
the printer and the annotation are all built. The master plan's phase
11 section implies this command is a deliverable of the phase; it is
not. Corrected at source in this commit.
F2. Chain-member device attachment is already format-agnostic.
open_chain_devices (:2831), open_chain_devices_rw (:2923) and
write_chain_device_entries (:2765) iterate chain.images() and
attach each as a virtio device with no format dispatch whatsoever; the
per-device format code is just image.format.to_shared_format_u32(),
and codes 5 (Vhd) and 6 (Vhdx) are already allocated and documented
(docs/chain-config.md:63-64). So the master plan's "attaching each
chain member as its own virtio device" is not phase 11 work either.
Also corrected at source.
F3. What is actually left is a seven-line gate. At
src/vmm/src/main.rs:2701-2707:
Everything above it -- canonicalisation, circular-reference detection,
depth limiting, allowlist validation -- is format-agnostic and needs no
change. The master plan's claim that "phase 11 extends it rather than
writing one" (PLAN-differencing.md:466) is correct and holds.
F4. Half of the gate's stated rationale is now false. Its comment
(:2678-2700) gives two reasons. The first is that a VHDX parent
locator holds a Windows-style relative path (.\parent.vhdx) that does
not resolve on POSIX. That was true when the comment was written --
commit 10ab838d, 2026-09-13 -- and stopped being true eight days
later: 7733730 ("Report a parent path the way the user typed it",
2026-09-21) added vhdx::posix_relative_path
(src/crates/vhdx/src/lib.rs:984), which the info operation applies
before reporting (src/operations/info/src/main.rs:1243-1248). A
relative VHDX locator therefore arrives at the host already POSIX-shaped
and resolvable. The claim survives only for absolute_win32_path and
volume_path, which are reported verbatim -- see decision 4. The same
false reason is copied into docs/chain-discovery.md:200-202.
F5. The gate's second reason is sound, and is the constraint this
phase is built around. From the same comment: "the outcome became
contingent on the parent's presence: the same differencing image gave
the typed refusal when its parent happened to sit beside it and a path
error when it did not. A refusal that depends on a file instar is not
going to read is not a refusal." That is correct, it is load-bearing,
and phase 8 wrote a test for it (TestDifferencingParentAbsent,
tests/test_differencing.py:609). Decision 1 answers it by
construction.
F6. Only one of the ten call sites needs to walk.
discover_backing_chain is called ten times: run_bench (:4614),
run_rebase (:6471, :6483), run_commit (:6829), run_info
(:10274), run_check (:11212), run_compare (:11953, :11959),
execute_convert (:12792) and run_dd (:14218). Nine of them go on
to attach devices and launch a guest. run_info --chain is the only one
that prints and returns. Note in particular that run_check --chain
is not display-only: it opens chain devices at :11336 and writes
the chain config at :11347, so it composes and keeps the gate.
F7. The default allowlist already refuses the adversarial fixtures.
get_backing_allowlist (src/vmm/src/config.rs:239) defaults to
MARKER_IMAGE_DIR -- the image's own directory -- and
get_max_chain_depth (:250) defaults to 16. Every adversarial locator
fixture in instar-testdata/custom/audit/ names something outside that
directory (/etc/passwd, ../../../etc/passwd, a UNC path, a URL), so
the existing validation rejects all six without a line of new security
code. This is why decision 1 is safe, and the definition of done pins it.
F8. VHD's parent reference is the unicode name, not a locator entry.
info's VHD arm reports the parent unicode name field
(src/operations/info/src/main.rs:437-478, via
parse_vhd_parent_name). The locator table is parsed by crates/vhd
(VhdParentLocator at src/crates/vhd/src/lib.rs:552,
VhdParentLocatorTable at :823) but never reaches the host: nothing
in InfoResult carries it. The master plan's phrase "resolving a
locator path to a real parent" is therefore imprecise for VHD --
decision 3.
F9. info --chain silently ignores --output json. InfoArgs
declares --output with a ["human", "json"] value parser
(src/vmm/src/main.rs:3617-3619), but the --chain branch calls
print_backing_chain unconditionally and returns, so
instar info --chain --output json prints human text and exits 0. There
is no JSON chain printer anywhere (grep for one finds nothing), and the
measurement above confirms the flag is silently ignored rather than
rejected. Pre-existing, and qemu-img's
info --backing-chain --output json emits a JSON array. Decision 7
brings it into scope, with the argument against.
F10. Chain devices are silently truncated, and at least two
operations can reach it. write_chain_config caps device_count at
MAX_CHAIN_DEVICES and reports the truncation with debug! only
(src/vmm/src/main.rs:3041-3050), and both write_chain_device_entries
loops break silently (:2776, :2802). The default
max_chain_depth is 16 and MAX_CHAIN_DEVICES is 16, so a 16-image
chain whose top image has a QCOW2 external data file needs 17 slots and
loses its deepest parent -- reading as zeros rather than erroring. Of
the five write_chain_config callers, run_check (:11347) and
run_bench_guest (:4897) have no device-budget guard in their own
bodies, where execute_convert does (:12809). There is also a second
MAX_CHAIN_DEVICES defined locally in the VMM (:331) shadowing
shared::MAX_CHAIN_DEVICES (src/shared/src/lib.rs:4725); both are 16
today. Pre-existing, affects qcow2 and VMDK now, and out of scope.
File it (step 11f, #601) rather than fixing it here.
F11. The tests that pin the current boundary already exist and say
so. test_info_chain_stops_at_a_vhd_parent
(tests/test_differencing.py:586) asserts a one-image chain and its
docstring reads "phase 11 will change it and should have to change this
test to do so". test_info_reports_the_locator_without_following_it
(:830) asserts the same for the six adversarial fixtures, and must
keep passing. docs/info.md:67-68 states the one-image behaviour in
prose.
Nothing else the master plan's phase 11 section claims was found to be
wrong: discover_backing_chain does carry circular-reference detection,
depth limiting and allowlist checking; security.backing_path_allowlist
and security.max_chain_depth are at src/vmm/src/config.rs:65 and
:67 as stated; the VMDK flat-descriptor short-circuit is real
(:2546); and both chain documentation pages exist at the line counts
phase 10 recorded (209 and 210). The only numeric drift is the function's
own line: the master plan says src/vmm/src/main.rs:2501, it is now
:2519. Corrected at source.
Decisions¶
1. A walk policy, not a lifted gate. Exactly one call site walks.
discover_backing_chain gains an explicit policy argument, and VHD and
VHDX parents are walked only under the reporting policy. run_info's
--chain branch passes it. The other nine call sites pass the composing
policy and keep behaviour that is bit-identical to today.
This is the decision that answers F5 by construction rather than by
care. The invariant phase 4 established -- a refusal must not depend on
whether the parent happens to exist -- cannot be broken by an operation
that never resolves the parent in the first place. The alternative
(walk everywhere, and make every composing operation tolerate an
unresolvable parent) puts the invariant at the mercy of ten error paths
staying correct, and phase 14 has to revisit each of them anyway as it
adds composition. Threading a policy is the smaller and more auditable
change, and phase 14's per-operation work becomes "flip this call site's
policy", which is exactly the granularity the master plan asked for
("phase 14 replaces phase 4's refusal op by op",
PLAN-differencing.md:376).
Shape it as a two-variant enum, not a bool: a bool at a call site reads
as discover_backing_chain(path, size, &cfg, true) and tells the reader
nothing. Name the variants for what the caller does with the result, not
for the formats affected, because phase 14 changes which formats are
affected and must not have to rename anything.
2. Under the reporting policy, an unresolvable parent ends the walk
without erroring. Whatever the reason -- the file is absent, the path
is outside the allowlist, the depth limit is reached, the path is a
Windows absolute path -- the walk stops at the last image it did
resolve, the unresolved reference stays in that image's
backing_file_raw so the annotation survives, and discovery returns
Ok. A single line naming the reason goes to stderr.
info reports and refuses nothing; that is decision 4 of phase 4 and
docs/info.md states it. Turning instar info --chain into a command
that exits non-zero because a parent is missing would contradict it, and
would be a worse regression than the gate it replaces. The reason goes
to stderr rather than being swallowed so a user with a broken chain
learns why, and so the integration tests can assert which reason
fired -- see the definition of done.
Note what this does not weaken: nothing opens a rejected path. The allowlist check still runs and still rejects; the only change is that its rejection ends a listing instead of failing a command.
3. VHD resolves from the parent unicode name. The locator table is
not a resolution source in this phase. F8 establishes that the
unicode name is the only parent reference that reaches the host today,
and widening InfoResult to carry eight locator entries is an ABI
change this phase does not otherwise need.
The stronger reason is oracles. libvhdi -- this plan's oracle, chosen in
phase 1 -- resolves VHD parents from the unicode name alone and never
parses the VHD parent locator table; libvhdi_parent_locator* is
reached only from the VHDX metadata path (master plan open question 2,
PLAN-differencing.md:236-242). qemu-img reads no differencing VHD at
all. So a locator-table resolution path would be the one part of this
feature with no reference implementation to disagree with us, in a plan
whose standing warning is that plausible-looking format claims here go
unchecked.
The argument against, which I accept as real: the locator table is
the VHD spec's designated mechanism and W2ru is what Windows actually
writes, so a third-party child with an empty unicode name and a valid
locator will resolve under Hyper-V and not under instar. That is a
genuine gap. It is recorded as future work with an issue (step 11f, #602)
rather than guessed at here, and the fixtures to settle it already exist
in instar-testdata/custom/audit/.
4. A Windows absolute VHDX path ends the walk; it is never
rewritten. relative_path values arrive POSIX-rendered (F4) and
resolve normally. absolute_win32_path (C:\images\parent.vhdx) and
volume_path (\\?\Volume{GUID}\...) are reported verbatim by design
and must stay that way: rewriting C:\images\parent.vhdx into
/images/parent.vhdx would invent a host path out of a drive letter and
hand it to an allowlist check, which is a path-traversal primitive
dressed as a convenience. So they terminate the walk under decision 2,
with a reason that says the path is a Windows absolute path rather than
that the file is missing.
5. No parent identity verification in this phase. VHD records the
parent's footer unique id in the child's dynamic header and VHDX records
the parent's DataWriteGuid in parent_linkage, so a host that walked a
chain could check it found the right parent. Three reasons not to, here:
the identity fields do not reach the host either (same ABI point as
decision 3); the check belongs where composition happens, because that
is where being wrong corrupts data rather than mislabelling a listing;
and #566 makes it vacuous for any chain instar wrote end to end --
every instar VHD carries an all-zero footer id and every instar VHDX
derives its DataWriteGuid from the sequence number, so any instar parent
satisfies any instar child. A verification step that passes for the
single most likely chain in the wild is worse than none, because it
reads as coverage.
Record the dependency loudly: phases 12 and 13 must not rely on parent identity for correctness until #566 is fixed.
6. No new security knobs, no new defaults. The walk inherits
security.backing_path_allowlist and security.max_chain_depth
unchanged (F7). The image-directory default is already the right
behaviour for relative VHD and VHDX parents, which is the overwhelmingly
common shape, and it is what rejects all six adversarial fixtures. A
knob nobody needs is a knob nobody tests.
7. info --chain --output json is fixed here. F9 is pre-existing
and would normally be an issue rather than phase work. It is in scope
because this phase's entire user-visible deliverable is that command's
output, and shipping a walked chain that the documented JSON mode
silently cannot express would make the feature unusable from a script
while appearing to work -- the failure mode is a zero exit and wrong
text, which is the shape this plan exists to eliminate.
This is the decision a reviewer is most likely to push back on, and
the push-back is reasonable: it is scope the master plan did not ask
for, in a phase that is already about something else. Two mitigations.
It is a separate step (11e) with its own commit, so it can be dropped
without touching anything else. And the issue is filed either way, so
dropping it does not lose it. Match qemu-img's --backing-chain
--output json shape -- an array of objects -- rather than inventing one.
Step plan¶
Each step is one commit. 11b depends on 11a; the rest are independent of each other.
| Step | Effort | Model | Isolation | Brief for sub-agent |
|---|---|---|---|---|
| 11a | high | opus | none | Replace the VHD/VHDX gate with a walk policy. Add a two-variant enum (decision 1 -- name the variants for what the caller does with the chain, e.g. one for callers that go on to attach devices and launch a guest, one for callers that only report) and a parameter for it on discover_backing_chain (src/vmm/src/main.rs:2519). At :2701-2707, keep the existing break for the composing variant and fall through to validate_backing_path for the reporting variant. Update all ten call sites listed in F6: only run_info's --chain branch (:10274) passes the reporting variant; the other nine pass composing, including run_check (:11212), which composes despite being a --chain flag -- it opens devices at :11336 and writes the chain config at :11347. Rewrite the gate's comment: its first rationale is false since 7733730 (see F4) and it cites a plan phase, which this repo does not allow in landed code (issue #592, and ~/.claude/CLAUDE.md) -- state the invariant instead of citing where it was decided. Do not implement decision 2's fail-soft behaviour yet; this step only moves the decision point, and validate_backing_path errors are still propagated. The tree must build and the whole Python suite must pass unchanged after this step, because no reachable behaviour has changed yet: the reporting variant's new path is only taken for VHD/VHDX, and run_info still errors on an unresolvable parent exactly as run_check would have. Note that tests/test_differencing.py:586 will now fail for the three resolvable fixtures -- that is expected and 11d updates it; say so in the commit message rather than editing the test here. |
| 11b | high | opus | none | Make the reporting walk fail-soft, per decisions 2 and 4. In discover_backing_chain, under the reporting variant only, convert every ChainError arising from resolving a VHD or VHDX parent into a terminated walk that returns Ok: emit one eprintln! line naming the image, the unresolved reference and the reason, then break. The reasons to distinguish, because 11d asserts on them: file absent, outside the allowlist, depth limit reached, and a Windows absolute path. The last needs detecting on the host -- a value with a drive-letter prefix (X:\) or a \\ prefix is absolute_win32_path or volume_path (F4, src/operations/info/src/main.rs:1232-1249 explains which key produces which convention) and must be reported as such, never rewritten into a POSIX path (decision 4). Composing callers keep propagating every error unchanged -- this is the only thing standing between the tree and a contingent refusal, so keep the two paths textually obvious rather than clever. VHD resolves from the unicode name that info already reports in backing_file; there is no locator-table path to add (decision 3, F8). Nothing about the allowlist or depth defaults changes (decision 6). |
| 11c | medium | sonnet | none | Rust unit tests for 11a and 11b in src/vmm/src/main.rs's existing test module (find it near the existing differencing_refusal_error_* tests at :945). Cover: the policy enum selects the gate or the walk; a Windows-absolute value is classified as such rather than as a missing file; and the reason strings are distinct. Where a test would need a real KVM guest to run execute_info_operation, test the classification helper 11b introduced directly instead of the whole walk -- factor it out as a small pure function taking the reference string if 11b has not already. make test-rust must be clean; note the worktree target-ownership trap in AGENTS.md if cargo complains about src/target. |
| 11d | medium | sonnet | none | Python integration tests in tests/test_differencing.py. Update test_info_chain_stops_at_a_vhd_parent (:586) to assert a two-image chain for the resolvable fixtures, renaming it for what it now asserts, and keep its docstring's habit of saying which phase owns the boundary. The fixtures are in DIFFERENCING_CHAIN_FIXTURES; the resolvable ones sit beside their parents in instar-testdata/custom/format-coverage/ (vhd-diff-child-aligned.vhd -> vhd-diff-parent.vhd, vhdx-diff-child.vhdx -> vhdx-diff-parent.vhdx). Leave test_info_reports_the_locator_without_following_it (:830) asserting one image and add to it an assertion on the stderr reason, so it passes for the allowlist reason rather than passing by accident (F7, and the definition of done). Add: an orphaned child still lists one image and exits 0; and every composing operation is byte-for-byte unchanged -- extend or cite TestDifferencingParentAbsent (:609) rather than writing a new class, since it already encodes the invariant. Run the suite with python3 -m pytest tests/test_differencing.py or the repo's usual runner; the full suite is expected zero-fail on healthy testdata. |
| 11e | medium | sonnet | none | Decision 7: give info --chain a JSON form. InfoArgs.output is declared at src/vmm/src/main.rs:3617-3619; the --chain branch at :10269-10282 ignores it. Add a JSON printer beside print_backing_chain (:2724) and select on args.output. Match qemu-img info --backing-chain --output json: a top-level array, one object per chain member, keys for the resolved filename, format, virtual size, actual size, cluster size where non-zero, and the unresolved backing reference where there is one. Reuse the existing json_escape helper (:17899, tested at :20191) rather than adding a JSON dependency -- the VMM hand-rolls its JSON elsewhere. Add an integration test asserting the output parses with json.loads and has one element per chain member. Keep the human form byte-identical; a test that diffs it against the current output is worth more than one that reads it. |
| 11f | low | haiku | none | Housekeeping. (1) File two issues: the silent chain-device truncation from F10 (#601, name write_chain_config at src/vmm/src/main.rs:3041-3050, the two unguarded callers run_check and run_bench_guest, the duplicate MAX_CHAIN_DEVICES at :331 versus src/shared/src/lib.rs:4725, and that it affects qcow2 and VMDK today); and the VHD locator-table resolution gap from decision 3 (#602). (2) Add a CHANGELOG.md entry for the walked chain and the JSON form. (3) Amend this plan's What the survey found with anything later steps discovered, and record the issue numbers. Do not add plan references to any source file or comment. |
| 11g | medium | sonnet | none | Rewrite docs/chain-discovery.md's backing-file-support table and its Known limitations section: delete the rationale about Windows-path incompatibility that vhdx::posix_relative_path falsified (F4), keeping the one about refusals not becoming contingent on a parent file existing; correct docs/info.md's claim at :67-68 that --chain stops at one image. |
Risks and mitigations¶
| Risk | Mitigation |
|---|---|
| A composing operation's refusal becomes contingent on the parent existing -- the exact defect the gate was added to prevent | Decision 1 makes it structurally impossible: nine of ten call sites never resolve a VHD or VHDX parent. The management session checks the diff for exactly this, and TestDifferencingParentAbsent (tests/test_differencing.py:609) fails if it happens. A green suite is not the evidence; the call-site audit is. |
| The adversarial locator fixtures keep reporting one image for the wrong reason -- e.g. resolution silently failing rather than the allowlist rejecting | 11d asserts the stderr reason, not just the image count. The definition of done states it separately because "the test still passes" is precisely how this one hides. |
| A Windows absolute path gets rewritten into a POSIX path by a well-meaning implementer | Decision 4 states it, 11b's brief repeats it, and 11c tests the classification. It is called out three times on purpose: it is the one change here that would turn a listing command into a file-disclosure primitive. |
run_check --chain is mistaken for a display-only caller because of its flag name |
F6 and 11a's brief both name it explicitly with the two line numbers that prove it composes (:11336, :11347). |
| Scope grows into phases 12-16 because the walk makes composition look close | The scope section names the guest machinery and its file (src/crates/qcow2/src/lib.rs:9442) so its absence is a fact rather than an impression. No step touches src/operations/. |
| The JSON step (11e) drags the phase out | It is last but for housekeeping, independent of every other step, and decision 7 says to drop it if it does. The issue is filed either way. |
Definition of done¶
instar info --chainonvhd-diff-child-aligned.vhdand onvhdx-diff-child.vhdxreports a two-image chain, with the parent resolved to an absolute path. Demonstrated with the command output in the commit message, not asserted.instar info --chainon each of the six fixtures inADVERSARIAL_LOCATOR_FIXTURESreports a one-image chain, exits 0, and writes a stderr reason, and the reason is the correct one for that fixture —vhd-diff-locator-etc-passwdis outside the allowlist,vhd-diff-locator-uncis a Windows absolute path, andurl,overlongandconflictingare each not found. The second half is the part that matters: a one-image chain alone does not distinguish "correctly refused" from "silently failed to resolve".
Corrected after CI: dotdot gives either "not found" or "outside the
allowlist", and which one is a property of the host rather than of
instar. ../../../ resolves relative to the fixture's own directory,
so it reaches a real /etc/passwd only where the testdata tree sits
within three levels of the root, as CI's /testdata/ mount does;
every development clone is deeper and stops at "not found" first,
because resolution canonicalises before the allowlist is consulted.
Both are refusals the walk classified and named, so the test accepts
either for that fixture and only that fixture.
* instar info --chain on a differencing child whose parent has been
moved away reports a one-image chain and exits 0.
* Every one of the nine composing call sites in F6 passes the composing
policy, verified by reading the diff and listed in 11a's commit
message by line number.
* convert, compare, dd, bench, map, measure and check
produce byte-identical output for a differencing source with its
parent present and with its parent absent. This is the phase's
central invariant; state the two captured outputs in the commit
message.
* No value that reached the host from inside an image is rewritten
between path conventions anywhere in the diff. grep the diff for
replace and for '\\' and account for every hit.
* docs/chain-discovery.md no longer carries the
posix_relative_path-falsified reason (F4), its backing-file-support
table (:132-133) reflects what VHD and VHDX now do, and
docs/info.md:67-68 no longer says --chain stops at one image.
* instar info --chain --output json output parses with json.loads
and has one element per chain member -- or 11e was dropped and the
issue exists. Say which.
* No source file or comment added by this phase cites a plan phase,
step or decision number, and the gate comment rewritten by 11a no
longer does either.
* The two issues 11f asks for are filed, with numbers recorded in
this plan: #601 (chain devices past MAX_CHAIN_DEVICES dropped with
only a debug log) and #602 (differencing VHD parent resolution
ignores the locator table). The review rounds on the pull request
added four more -- #608 and #609 in round 1, #611 and #612 in round
2 -- recorded in What the survey found.
* make lint, make test-rust, the full Python suite, and
pre-commit run --all-files are clean.
Back brief¶
Before implementing, restate: which call sites get which policy and why
run_check is not among the reporting ones; what happens to each of the
four unresolvable-parent reasons; and what stops a Windows absolute path
being rewritten.
Gate on 11a. Bring the policy enum's shape -- variant names and the signature change -- back for agreement before editing ten call sites. The edit is mechanical and the naming outlives the phase: phase 14 flips these call sites one at a time and should not have to rename anything to do it.
No gate on 11b through 11f; the decisions above settle them.