Conversation
This was referenced Sep 20, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First: is
os/yocto/.../dstack-initscript/initactually shipped?Two of the three items below are about a file under
os/yocto/, whichCLAUDE.mdcalls deprecated. That would normally be reason enough to close them. It is not,
and this is the finding that decides whether the rest of the PR exists.
os/mkosi/scripts/make-release-artifacts.sh:92installs that exact file, verbatim,as
/initof the initramfs that goes into the shipped UKI:The surrounding block assembles the initramfs from the mkosi tree (
busyboxat:56,
veritysetupat :68), thenukify build --initrd=.../initramfs.cpio.gzturns it into
dstack-uki.efi. So this script is the early boot of bothbackends and of every image dstack ships today. It is not dead code, and it is
the one piece of the boot path with no console and no recovery.
acceptance.shnow asserts that link, so if mkosi ever stops installing thisfile the tests below stop claiming to cover a shipped artifact.
O-a — the initramfs mounts
/proc,/sys,/run,/devwith no optionsReal. The whole finding rested on a premise that had never been checked:
that systemd does not repair the options afterwards. It was checked.
Booted real systemd on the shipped base (Debian trixie, systemd 257.13-1~deb13u1)
with
/run,/procand/sysmounted bare first, exactly as the initramfsleaves them, then read
/proc/self/mountinfoonce the system reportedrunning:Unchanged.
mount_setup()'smount_one()returns early for a path that isalready a mount point, and the initramfs
mount --moves all four acrossswitch_root— verified thatmount --movepreserves options, so nothing inthe chain restores them.
The concrete consequence, on the same run:
A bare tmpfs takes the kernel default of
1777, not the0755systemd wouldhave used. So every CVM runs with
/runworld-writable andsuid/devhonoured, and
/procand/syswithoutnosuid,nodev,noexec— purely as anaccident of the custom initramfs. A stock Debian boot gets all of these.
This is the cheap-bounded-hardening case rather than a live exploit: the CVM is
single-tenant, so per the project's threat model file modes between workloads
are not a boundary. The argument for fixing it is that the image is less
hardened than the distro default for no reason, and nothing downstream can
correct it.
Failing test first
New
os/tests/test-initramfs-early-mounts.shrebuilds the initramfs layoutmake-release-artifacts.shassembles (same busybox, same applet symlinks, empty/proc,/sys,/run,/dev), chroots into it, runs the realinit, andreads the options back out of the kernel. Against
next:After:
It runs the script rather than grepping it, because the failure that would
actually ship is busybox silently dropping an option it does not parse. All
four option sets were confirmed accepted by busybox 1.37.0 — the version Debian
trixie ships as
busybox-static, which is whatmkosi.confinstalls.Fix
nosuid,nodev,noexecfor/procand/sys;nosuid,nodev,mode=0755for/run;nosuid,strictatime,mode=0755for/dev— the same values systemd'sown
mount_tableuses./devkeepsdevandexecbecause it exists to carrydevice nodes.
Blast radius
Changes the initramfs, therefore the UKI, therefore the measurements — a new OS
image release and a KMS whitelist entry, as with any guest-OS change. Behaviour
risk is a workload that relied on setuid or device nodes on
/run, or on execfrom
/proc//sys; containers get their own/runin their own mountnamespace, so this is the host side of the CVM only. Deliberately not
changed:
size=/nr_inodes=limits on/run. systemd would cap it at 20% ofRAM; adding a cap here is a real behaviour change on small CVMs and is a
separate decision from the option flags.
O-b —
After=chronyd.servicenames a unit that may not existRefuted. The alias genuinely exists in the shipped image. Evidence:
Debian trixie's
chronypackage ships nochronyd.service. It shipschrony.servicewith:[Install]aliases only materialise on enable — correct. But80-dstack.presetalready carriesenable chrony.service, and mkosi runssystemctl --root=/buildroot preset-allover the image tree(
mkosi/__init__.py:3128, called fromrun_presetat :4022). Reproducingexactly that on trixie with this repo's preset files:
The prod profile's
RemoveFiles=does not touch/etc/systemd/system, sothe symlink survives into the released rootfs.
So
After=chronyd.serviceresolves, andchronyddoes run. No rename.Renaming to
chrony.servicewould be churn on a measured file presented as afix.
What the investigation did surface is that the ordering is load-bearing on a
line in a different file, with nothing recording the coupling: delete
enable chrony.servicefrom the preset and two things break silently at once— chronyd never starts, and the
After=resolves to nothing.acceptance.shnow pins it, conditional on the
After=still being there.The related
chronyc makestepclaim — confirmed, and fixeddstack-prepare.sh:100waschronyc makestep || log "Warning: ...". Ran itagainst a chronyd with no reachable source:
makesteponly tells chronyd it may step instead of slew. It answers200 OKand exits 0 whether or not a source has ever been reached, so the
|| logbranch is unreachable and the guard guards nothing.
dstack-prepare.shthenproceeds to
dstack-util setup— the RA-TLS handshake to the KMS — on anarbitrary clock, and both sides of that are wall-clock judgements: quote
collateral freshness and certificate validity windows.
Fix: keep
makestep, add a boundedchronyc waitsync 30 0 0 1after it.Measured the bound rather than assuming it —
waitsync 5 0 0 1against anunreachable source returned
exit=1after 4s, so the 30-try form costs ~29sworst case and returns non-zero rather than hanging. The
|| logis kept, so anNTP-less CVM still boots; it just no longer claims a clock it does not have.
Unbounded waiting was rejected: this unit has
FailureAction=reboot.O-c — dm-verity opened with no corruption policy
Real, and a policy call. Please overrule me cheaply if you disagree.
Today
initopens the rootfs with no mode, which the kernel confirms:With no mode, dm-verity returns
EIOto the faulting reader and nothing else.A tampered rootfs block surfaces late, scattered across whichever process
happened to read it, while the CVM keeps serving attested traffic from a rootfs
whose integrity guarantee has already failed.
The two alternatives, and what each gives the host
--panic-on-corruptionpanic()— stops every CPU at once--restart-on-corruptionkernel_restart()— runs reboot notifiers and device shutdown firstEIOto the readerI chose
--panic-on-corruption. Reasoning, in the order I weighted it:but not a new capability. The host already owns the VM lifecycle and can
reset or kill the CVM outright. Halting concedes nothing it does not have.
panic=1is already on the guest kernel command line(
os/image/kernel-cmdline.sh), so both halting modes end in a rebootregardless. The choice is only how the kernel gets there.
panic()is the safer of the two: it stops immediately, wherekernel_restart()first runs reboot notifiers and device shutdown paths —i.e. more kernel code reading the rootfs that is known to be corrupt.
To overrule: change
--panic-on-corruptionto--restart-on-corruptiononthe single
veritysetup openline inos/yocto/layers/meta-dstack/recipes-core/images/dstack-initscript/init. Thetest reads the flag out of that line rather than restating it, so it keeps
passing either way. What the test refuses is the two states that are not a halt:
no policy, and
--ignore-corruption.Failing test first
New
os/tests/test-initramfs-verity-policy.sh. Againstnext:After:
The runtime leg builds a real verity device and reads the mode back out of
dmsetup table, which is what catches a flag the shippedveritysetuprejectsor a kernel whose dm-verity lacks the mode — either of which would leave the
guest with no rootfs and no console to debug from. Confirmed against
cryptsetup-bin 2:2.7.5-2(trixie, the versionmkosi.confinstalls) andCONFIG_DM_VERITY=yon the 6.18.40 guest kernel.It deliberately never corrupts a block: both halting modes take down the
kernel running the test, which in a container is the host.
Blast radius
Same measurement change as O-a. Behaviourally: a CVM that today would limp along
with scattered
EIOnow reboots. That is the intended change. There is noperformance cost — the mode only affects the error path.
Gates
os/mkosi/tests/acceptance.shpasses, including the new assertions.nextfirst.acceptance.shassertions were negative-tested — deletingenable chrony.serviceand deleting thewaitsyncline each make it failwith the intended message.
prek runclean on every touched file (shellcheck, trailing whitespace,EOF, line endings).
they do not become a hard CI dependency. Verified with a docker-free
PATH.What I did not verify
No guest image was built. Everything above was verified against the exact
component versions the image pins — Debian trixie, systemd 257, busybox 1.37.0,
cryptsetup 2.7.5 — but assembled in containers, not in a real CVM. Specifically
not verified end to end:
make os-imagestill builds and boots with these options;panic_on_corruptionfiring (deliberately never triggered);waitsyncpath against a real CVM's NTP configuration.I did not start a multi-hour mkosi build. If you want that gate before merge,
say so and I will run it.
One
/devcaveat the test comments record:devtmpfsis a singleton, so asecond mount inherits the first mount's
mode=and the test cannot assertmode=0755for/dev. In the guest the initramfs is the first mount, so theoption does apply there; only
nosuidis asserted, and that one is per-mountand did fail before the fix.