Skip to content

One layout, one name for the image, and a fingerprint that cannot be shifted - #1

Merged
aledbf merged 1 commit into
mainfrom
machine/converge-with-consumers
Sep 8, 2026
Merged

One layout, one name for the image, and a fingerprint that cannot be shifted#1
aledbf merged 1 commit into
mainfrom
machine/converge-with-consumers

Conversation

@aledbf

@aledbf aledbf commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Groundwork so that spinbox and storage can stop keeping their own copy of what this machine is. Three changes, each removing a way for two things to disagree.

One layout

The same four files had three shapes — what the build extracted into _output/, what the tarball carried under usr/share/spin-stack/, and what a consumer rearranged them into — with a translation step between each pair (hack/release's moves, spinbox's place(), internal/paths's discoverQemuPath) and a discovery function guessing which shape it had. Every one of those is a place the answer can be wrong, and the way it goes wrong is a path that exists and holds the previous release's kernel.

The three Dockerfiles now extract the release tree directly, so _output/ and the tarball are the same shape byte for byte, and the new machine.Open reads either:

rel, err := machine.Open("/usr/share/spin-stack")  // says which file is missing, if one is
spec := rel.Spec()                                 // QEMU, Kernel, Firmware
img, err := rel.Rootfs()

It checks rather than trusting — four stats — so a release with a hole in it names the file, instead of surfacing later as a QEMU that exits for want of an option ROM. The TCG binary and the image are optional: a KVM-only host needs neither, and QEMUTCG()/Rootfs() report their own absence to whoever asks.

This also removes COMPAT_KERNEL_NAME, the symlink hatch that existed so a consumer resolving the kernel under another name would not have to change.

base.qcow2rootfs.qcow2

base names the role — a qcow2 backing file — which the directory and the prose already say, and it collides with "base image" in the container sense. rootfs names what the file holds.

The fingerprint cannot be shifted

It hashed key=value\n lines whose values are a CPU model, a device list and a line out of /proc/cpuinfo — none guaranteed free of the separator, so two different machines could produce one byte stream. Now length-prefixed. It also states which three things a restore is allowed to differ in and why each is safe: the vsock context id is not in the migration stream and the guest re-reads it after the transport reset, a disk is cold-plugged and found with a PCI rescan, and what sits behind a NIC is a host file descriptor the guest never sees.

Verified

qemu:build, kernel:build and image:build all write the release tree; a guest booted from an overlay on the renamed image mounts its ext4 root; task release packs the tarball and spin-machine fingerprint --release reads the unpacked tree with no path fixed up on the way. New tests cover Open naming each missing file, the optional parts, and Spec() deciding nothing a release does not know.

This is a breaking release: every existing template stops matching, which is what changing the machine means. Nothing consumes one yet.

🤖 Generated with Claude Code

https://claude.ai/code/session_019GHRRFvbnZi5q3KSTvyW5a

…shifted

Three changes, all of them removing a way for two things to disagree.

**One layout.** The same four files had three shapes: what the build extracted
into _output/, what the tarball carried under usr/share/spin-stack/, and what a
consumer rearranged them into — with a translation step between each pair and a
discovery function guessing which shape it had been handed. The builds now write
the release tree directly, so `_output/` and the tarball are the same shape byte
for byte, and `machine.Open` reads either. It checks rather than trusting: a
release with a hole in it says which file is missing, instead of surfacing three
seconds later as a QEMU that exits for want of an option ROM.

That also removes COMPAT_KERNEL_NAME, the symlink hatch that existed so a
consumer resolving the kernel under another name would not have to change.

**base.qcow2 is rootfs.qcow2.** `base` names the role — a qcow2 backing file —
which the directory and the prose already say, and it collides with "base image"
in the container sense. `rootfs` names what the file holds.

**Fingerprint is length-prefixed.** It hashed `key=value\n` lines whose values
are a CPU model, a device list and a line out of /proc/cpuinfo — none of them
guaranteed free of the separator, so two different machines could produce one
byte stream. It also now states which three things a restore is allowed to
differ in and why each is safe: the vsock context id is not in the migration
stream and the guest re-reads it after the transport reset, a disk is
cold-plugged and found with a PCI rescan, and what sits behind a NIC is a host
file descriptor the guest never sees.

Verified: qemu:build, kernel:build and image:build all write the release tree;
a guest booted from an overlay on the renamed image mounts its ext4 root; `task
release` packs the tarball and `spin-machine fingerprint --release` reads the
unpacked tree with no path fixed up on the way.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019GHRRFvbnZi5q3KSTvyW5a
@aledbf
aledbf merged commit 17d844f into main Sep 8, 2026
4 of 5 checks passed
aledbf added a commit that referenced this pull request Sep 10, 2026
…ves it

The research's ranked candidate for a kernel rebuild was CONFIG_XFRM=n, on the
grounds that xfrm_init runs inside ip_rt_init and register_xfrm_state_bpf does
BTF work Docker does not need. The measurements agree right up to the last step
and then invert the conclusion.

function_graph at depth 1, silent console, dmesg read back from the guest:

    inet_init                  9252 us
    └─ ip_rt_init              9444 us   (97% of it)
       └─ xfrm_init            8971 / 8638 / 9135 us over three boots
          └─ register_xfrm_state_bpf   9045 / 9306 us - essentially all of it

Which reads as a 9 ms saving sitting behind one config symbol. It is not. Filter
on register_btf_kfunc_id_set instead and the boot makes 51 calls to it:

    #1  9301.9 us      #2  0.75 us      #3  1.52 us      ...  #51

The first call pays the one-time parse of the 4.35 MB .BTF section and every
other call is microseconds. XFRM is not expensive; it is merely first in the
queue. CONFIG_XFRM=n would hand the same 9 ms to whichever kfunc set registers
next, and the only symbol that removes it is CONFIG_DEBUG_INFO_BTF=n - which is
the one thing here that is not up for discussion.

Recorded at the BTF assertion in kernel/Dockerfile, where the decision to keep
BTF now carries its price.

That is the third time in this session a per-function number turned out to
belong to something else: acpi_purge_cached_objects at 26x under a wide filter,
ksm_init and kcompactd_init at 3 ms of vCPU descheduling each, and now this.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019GHRRFvbnZi5q3KSTvyW5a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant