Repository navigation
install: Add --defer-var to leave the stateroot /var unseeded - #5
jmarrero-bot wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Changes needed at a20a4617.
-
P1 — composefs can fail its first boot.
crates/lib/src/install/defer_var.rs:27–38labels the empty state directory correctly at install, butcrates/initramfs/src/lib.rs:657exposes it as/varbefore SELinux policy loads. The older dracut95nfs/nfsroot-cleanup.shcanmkdir -p "$NEWROOT/var/lib/nfs/rpc_pipefs"when its initramfs RPC filesystem is mounted, creating an unlabeled/var/lib. The published layout-D AVC then explains the failure to create/var/lib/appdb. This is consistent with the proposed cause, not a bad label on the stateroot root or a libostree relabel problem. Ostree normally defers the/varbind until the host'svar.mount, avoiding this exposure. Fix the boot-time contract: preferably defer composefs's persistent/varmount until policy is loaded, or provide correctly ordered, policy-aware labeling of initramfs-created state before host mounts/services need it. Creating caller mountpoints or a hard-coded/var/libskeleton only masks this instance. Current dracut-ng also guards NFS cleanup on an NFS netroot; check/backport that fix for the tested distro, but retain an enforcing-SELinux regression test for empty composefs/var. -
P2 — the committed integration test misses the failing behavior.
tmt/tests/booted/test-install-to-filesystem-var-mount.sh:198,240–265disables SELinux and never boots the installed target. Add an enforcing boot case with the nested/var/lib/appdbvolume, assert logical-path labels for the state root/marker and boot-created parents, then verify caller data and absent image sentinels through upgrade, switch and rollback on both backends. Use a known image payload for the plain-install assertion: on ostree.ostree-selabeledalone satisfies the current nonempty check. Published PR1 evidence covers ostree switch/reboot and composefs boot in the layout without appdb; it does not demonstrate PR1 rollback or a successful composefs switch/upgrade. PR2/prototype results cannot fill that gap. -
P2 — document the composefs separate-
/varlimitation.docs/src/man/bootc-install-to-filesystem.8.md:19–29suggests filling a separate filesystem, but this head still unconditionally binds the state/varby default (crates/initramfs/src/lib.rs:652–657); an fstab/varentry alone is ineffective. Document that callers currently need the image's setup-root configuration[var] mount = "none"plus their mount configuration, and must populate/label their filesystem. The fstab-aware behavior proposed for PR2 is absent here.
The core marker design looks correct by inspection: it precedes the first deploy; libostree v2026.1 skips seeding any nonempty stateroot var, independently relabels /var when its own stamp is absent, and writes that stamp itself. Bootc's logical /var override labels the state root and marker correctly. Later ostree deploys remain nonempty; composefs upgrade/switch already pass initialize_var=false; rollback selects existing deployments. No reseeding path found. Keeping the marker is appropriate for ostree. The Features: implementation, option/man-page generation, and two-commit organization look reasonable.
Verification: read the full diff, commit messages, AGENTS.md, REVIEW.md, REVIEW_RUST.md and CONTRIBUTING.md; bot-git check passed for both commits. Independently reran cargo fmt --check, shell syntax checking and git diff --check against the reviewed head, all passed. Inspected the linked devspace logs recording 336 + 10 passing tests and both install-only tmt passes. I did not rerun builds or VMs; the known boot failure remains unresolved.
jmarrero-bot
left a comment
There was a problem hiding this comment.
Review guide for head a20a4617f1: where to look closely and what is safe to skim. It is advice from the bot's reviewer and doesn't replace reading the diff; the review app walks it.
Adds install to-filesystem --defer-var: before the first deployment bootc writes an empty .bootc-defer-var into the stateroot var (ostree/deploy/STATEROOT/var or state/os/default/var) and labels it as /var. libostree's prepare_deployment_var() seeds only an empty stateroot var, and bootc's composefs install now checks the marker, so neither backend seeds /var at install or later. Also adds a Features: list to bootc --version. The core change is small and correct on both backends. The risk is in what it promises: on composefs, a separate /var filesystem is still shadowed at boot by setup-root's bind mount of the (now nearly empty) state directory until PR 2's setup-root commit lands, and an unfilled deferred /var on composefs can boot with /var/lib unlabeled when a volume sits below a directory the empty /var lacks. The tmt test runs with --disable-selinux and never boots the result, so labels and boot behavior rest on the VM runs in the gist.
Hotspots
- look-closely · logic —
crates/lib/src/install.rs:1153-1156(6e150f6): On ostree the marker works only because libostree's prepare_deployment_var() stops at any entry in the stateroot var (ostree-sysroot-deploy.c, e6be8dbe, ~3130). It must be written after init_osname and before the first deploy, which it is. libostree still relabels /var and writes .ostree-selabeled afterwards, so the bootc labeling here is redundant but harmless on ostree. - note · logic —
crates/lib/src/install.rs:2347-2354(6e150f6): composefs: the marker goes in before setup_composefs_boot, which is where write_composefs_state() checks it. Upgrades and switches pass initialize_var=false anyway, so on composefs the marker only matters at install. - note · logic —
crates/lib/src/install/defer_var.rs:27-40(6e150f6): On composefs this is the only thing that labels the state var root (var_t). The state var is bind-mounted in the initramfs, so directories created under the empty /var before the policy loads (here /var/lib and /var/lib/nfs, for the rpc_pipefs mount carried across switch-root) come out unlabeled until tmpfiles fixes them. That window is what breaks the gist's /var/lib/appdb case. - look-closely · api —
docs/src/man/bootc-install-to-filesystem.8.md:19-22(6e150f6): Says /var can be filled by the caller 'for example on a separate filesystem'. On composefs without PR 2's setup-root commit, a /var filesystem from fstab is shadowed at boot by the bind mount of the state directory, which with --defer-var is empty. Either say so (with the [var] mount = "none" workaround) or move that commit here. - note · api —
crates/lib/src/cli.rs:1081-1095(6e150f6): New external interface: osbuild and image-builder will parse the Features: list from bootc --version. The first line stays the bare version, and the format is pinned by test_long_version. - note · test-gap —
tmt/tests/booted/test-install-to-filesystem-var-mount.sh:243-266(a20a461): Checks the state directory's content only. It runs with --disable-selinux and doesn't boot, so labels, and the absence of reseeding on later deployments, are covered only by the manual VM runs.
Safe to skim
crates/lib/src/install/baseline.rs: struct field defaulttmt/tests/tests.fmf: duration bumptmt/tests/booted/test-install-to-filesystem-var-mount.sh:145-210: mechanical: existing steps moved into mount_target/run_install functions
jmarrero-bot
left a comment
There was a problem hiding this comment.
Reviewer verdict at a20a461: fix first (docs only, small). The code is correct on both backends.
- ostree: the marker is written after
init_osnameand before the first deploy. libostree'sprepare_deployment_var()stops at any entry (e6be8dbe, ~3130), so nothing is seeded on install, upgrade, switch or rollback. libostree still relabels/varand writes.ostree-selabeledon the first deploy (~822-866), so bootc's own labeling there is redundant but harmless. - composefs: the marker is checked only at install. Updates already pass
initialize_var=false.
Findings:
docs/src/man/bootc-install-to-filesystem.8.md:19-22(and the body): the man page offers "for example on a separate filesystem", but on composefs a/varfstab filesystem is still shadowed by setup-root's bind mount of the state directory, which--defer-varleaves empty. bootc-dev#2599 put the setup-root change "in PR 2 unless PR 1 already needs it", and for composefs it does. Either move 6ab7f0a here, or document[var] mount = "none"for composefs until it lands.- The composefs emergency mode with
--defer-varalone, layout D: the cause is right. I booted layout E on a build of the PR 2 head./var/liband/var/lib/nfshave birth times about 3.6s after boot, before the policy loaded at 4.2s, which is the rpc_pipefs mount carried across switch-root. composefs bind-mounts/varin the initramfs, while ostree mounts it after the policy loads. tmpfiles later relabels/var/libtovar_lib_t, so layout E boots.var-lib-appdb.mountruns before tmpfiles, so layout D fails. This shouldn't block this PR, because--defer-varalone means "the caller fills/var". Fix: document that the caller must fill/varbefore the first boot, and file an upstream issue about composefs writes to/varbefore the policy loads. Don't makemark()create mountpoint parents: that only hides the problem. tmt/.../test-install-to-filesystem-var-mount.sh:243-266runs with--disable-selinuxand doesn't boot, so labels and later deployments rest on the manual VM runs. That's acceptable.
Re-run on a 16-core devspace: fmt, clippy (Makefile flags), doc -D warnings, --no-default-features, update-generated (no diff), and 336 + 10 unit tests all pass. The body's claims match r4-static.txt, the tmt outputs and the od/cd logs. bot-git check: clean.
The SystemNotUEFI failure is a real upstream regression, not test setup. 3b40e99 (bootc-dev#2554) made find_booted_from_esp() fall back only on MissingVar (crates/lib/src/store/mod.rs:514), but classify_bootloader already supports non-EFI composefs (bootc-dev#2375). Treat SystemNotUEFI the same way. composefs switch and rollback do run on UEFI: with OVMF, the PR 2 head passed boot, bootc status, switch to v2 and rollback, with all checks passing under enforcing. One catch: a non-EFI install VM leaves the ESP empty (bootupd installs only the BIOS component), so the harness has to fill the ESP.
Nothing here blocks the e2e run (tracker#55).
Generated-by: https://github.com/jmarrero-bot#llms
a20a461 to
3452994
Compare
|
Review fixes pushed: a20a461 is now 3452994.
Results are in the body, logs in the gist (round 5). |
The deployment backends seed their state directory for /var with the image's /var on installation. When the caller puts /var on a separate filesystem, that copy is stored on the root filesystem and hidden under the mount at boot, and callers that fill /var themselves (e.g. with their own copy onto the volumes) get it twice. With --defer-var, `install to-filesystem` leaves the state directory without the image's /var. Before the first deployment, bootc writes an empty .bootc-defer-var file into it, labeled like /var/.bootc-defer-var, and labels the directory as /var, since nothing else labels a /var that isn't seeded. The marker is the one mechanism for both backends: bootc's composefs deployment skips its copy when it is there, and libostree's prepare_deployment_var() seeds the stateroot var only while it is empty, on every deployment, so on ostree its presence is enough and libostree's own .ostree-selabeled stamp is left to libostree (it relabels /var and writes it on the first deployment, as it does for any install). Upgrades, switches and rollbacks then leave /var alone, as they do for any populated /var; the marker stays as a record that /var was deferred. Like other options that only make sense on a fresh target, it is refused over an existing ostree system and with --replace=alongside, before anything on the target is changed. Image builders need to know whether they can pass it, so `bootc --version` now prints a Features: list after the version, with "defer-var" in it; the first line stays the bare version for existing parsers. The list is the one bootc-dev#2521 proposed for --initialize-var-mounts, which will add its own entry. It describes the binary rather than an image, so it is not part of `container inspect`, and `--version` needs no new query interface. With --defer-var alone, the caller has to fill /var before the first boot. On composefs an empty /var is visible in the initramfs before the SELinux policy loads, so whatever early boot creates there is unlabeled; the man page says so. Generated-by: AI
Check that a plain install seeds the backend's state directory with a file the test image puts in /var, and has no marker. Then install again on fresh filesystems with --defer-var: the state directory only holds the marker (and on ostree libostree's .ostree-selabeled stamp), and the separate /var stays empty. Generated-by: AI
3452994 to
9506ed3
Compare
|
Re-review fixes pushed: 3452994 is now 9506ed3, squashed into the commits they belong to.
Tests on a fresh devspace, with images checked to be this build: static checks, 336 + 15 unit tests, tmt plan-32 on ostree and composefs BLS, and the od, cv and cd VM cases. A new gate case installs composefs from main with a |
On composefs, the initramfs bind-mounts the state directory's var over /var before switch-root. With a separate /var filesystem in fstab, systemd then sees var.mount as already active and never mounts it: the system runs on the state directory on the root filesystem, and the /var volume is shadowed and goes unused. With --defer-var that state directory is empty, so a /var that the caller filled on its own volume is hidden behind an empty one. Nested volumes below /var do mount, on top of the bind mount. ostree's system generator skips its own /var mount when fstab has a /var entry. Do the same in setup-root, but only for a system installed with --defer-var, whose stateroot var holds the .bootc-defer-var marker (reached through the deployment state's relative var symlink): with no explicit [var] or transient setting, a mount entry for /var then means `mount = "none"`, as systemd.volatile=state already does. Other systems keep the bind mount they have always had. Existing composefs systems with a /var fstab entry, and new installs with a /var volume but without --defer-var, run on the state directory's var, and switching them to an fstab volume that may be empty or unlabeled on an update would break their boot. What counts is what systemd would mount: as in libmount, a record needs a source, a mountpoint and a filesystem type, and swap entries don't mount /var. As in ostree's fstab_has_mount_entry() (glibc's getmntent), fields are separated by spaces and tabs, and noauto entries count. (getmntent itself also takes two-field and swap lines, which would leave /var unmounted.) openat2 is retried on EAGAIN, as its man page suggests, so that a concurrent rename doesn't fall back to the empty state var. The fstab is read after /etc is mounted, through the mounted /etc, so it is the file the system will see whatever the [etc] mode: with an overlay /etc, whiteouts, opaque directories and metadata-only copy-ups resolve as overlayfs resolves them. Symlinks, for the fstab or for /etc, resolve within the deployment root (RESOLVE_IN_ROOT), never against the initramfs. This runs on every composefs boot, so it must not fail one: the fstab is read as bytes and decoded lossily, since the paths compared are ASCII, anything but a regular file counts as no fstab, and if it can't be read, setup-root warns and keeps the bind mount. The decision is printed, since the initramfs has no tracing subscriber. The marker name is now defined in the initramfs crate and used by the installer, so the two can't drift apart. Generated-by: AI
9506ed3 to
ff41dcc
Compare
|
Second-review low items pushed: 9506ed3 is now ff41dcc. Only commit 3 changed (
Static checks and unit tests (336 + 16) pass, and all 17 mutations fail a test, including the new |
bootc install to-filesystem --defer-varinstalls without seeding the deployment's state directory for/var(ostree/deploy/STATEROOT/var, orstate/os/default/varon composefs). This is the first of the two PRs proposed in bootc-dev#2599. On its own it is for callers that fill/varthemselves, before the first boot. Combined with--initialize-var-mounts(bootc-dev#2521, which will build on this PR), each file under/varis stored only once.Both backends use the same mechanism. Before the first deployment, bootc writes an empty
.bootc-defer-varmarker into the state directory, and labels the marker and the directory as/var. bootc's composefs deployment skips its copy when the marker is there. libostree'sprepare_deployment_var()only seeds an empty staterootvar, so on ostree the marker alone is enough. This doesn't rely on libostree's.ostree-selabeledstamp, which libostree considers slated for removal: libostree still relabels/varand writes its stamp on the first deployment, as it does for any install. Later upgrades, switches and rollbacks leave/varalone, as they do for any populated/var. The marker stays in place as a record.On composefs, setup-root now leaves
/varto systemd on a system installed with--defer-var(its staterootvarhas the marker) when the fstab that/etcshows has a mount entry for/var, as ostree's generator does. Without this, the initramfs bind-mounts the state directory over/var, and systemd considers an fstab/varvolume already mounted, so a volume the caller filled would be hidden behind the empty state directory. Other systems keep the bind mount. That includes existing composefs systems with a/varfstab entry, and new installs with a/varvolume but without--defer-var, whose volume may be empty or unlabeled. A mount entry is what systemd would mount: as in libmount, source, mountpoint and type, and not swap. As in ostree'sfstab_has_mount_entry()(through glibc's getmntent, which by itself would also count two-field and swap lines), fields are separated by spaces or tabs, andnoautocounts. The fstab is read through the mounted/etc, which also covers an overlay/etc, and symlinks resolve inside the deployment root. It is read as bytes, opening it is retried onEAGAIN, and any error keeps the bind mount, so the fstab can't fail a boot. An explicit[var]setting in setup-root-conf.toml still wins.bootc --versionnow prints aFeatures:list after the version, withdefer-var, so image builders can check for it. The first line is still the bare version. bootc-dev#2521 adds its own entry to the list.With
--defer-varalone,/varmust be filled before the first boot, and the man page says so. On composefs an empty/varis visible in the initramfs before the SELinux policy is loaded, so directories created there then are unlabeled. On CentOS Stream 10 that's/var/liband/var/lib/nfs, from dracut's nfs cleanup hook (dracut 107; dracut-ng guards the hook on an NFS root since bf29f4b79a, in 110). A volume at/var/lib/appdbthen can't get its mountpoint, and the boot goes to emergency mode. This is a composefs issue rather than this PR's (draft upstream issue to follow).Tested at ff41dcc on a 16-core RHEL 10 devspace (jmarrero-devspace-40867227224): static checks and unit tests. Since 9506ed3 the setup-root wiring only moved into
var_config(), and the fstab is opened with anEAGAINretry, so the tmt and VM results below are from 9506ed3 (jmarrero-devspace-40852491972, images built without reusing other branches' build caches). Logs: https://gist.github.com/jmarrero-bot/cb0ed128e38cab8cc220d7b21e63de0b (rounds 6 and 7).cargo fmt --check, clippy (Makefile flags),cargo docwith-D warnings,cargo check --no-default-features(lib),cargo xtask update-generated direct --check, andupdate-generated from-codewith no diff.cargo test -p bootc-lib -p bootc-initramfs-setup: 336 + 16 passed. The tests cover the setup-root decision table andvar_config()with and without the marker, fstab records (comments, noauto, swap, two fields, tabs, Unicode whitespace),0xffand Latin-1 bytes, symlinks resolved in the root, a socket and other non-regular files, an fstab that can't be read (also as root), and the marker reached through the state'svarsymlink.[var]and transient precedence, the marker path, the 0755 mode,var_config()ignoring the decision, treating ENXIO as an error, and the alongside check.cargo check --testspasses at each commit. Not covered by a test: passing the wrong root tovar_config()insetup_root()(the VM cases cover that), and theEAGAINretry, which can't be induced.plan-32-install-to-filesystem-var-mount: a plain install seeds a known file from the test image into the state directory; with--defer-varthe state directory holds only the marker, plus libostree's stamp on ostree. PASSED on ostree/grub and composefs grub ext4 BLS (unsealed).--defer-varwith nested volumes: the state root and the marker arevar_t, and the directories created at boot are labeled as the policy says. Throughbootc switchto v2, reboot,bootc rollbackand reboot, the data on a volume stays, no image content appears, and the marker stays./varvolume and nested volumes,--defer-var, filled by the caller:/varis the volume, and setup-root says so in the journal. The same checks hold through switch and rollback. After a Latin-1 byte is added to fstab and the system reboots,/varis still the volume.--defer-var, with a/varfstab entry, then switched to this build. The state var is bind-mounted before and after the update.--defer-varwith nothing filled and a volume at/var/lib/appdb: emergency mode, as described above.Open questions:
--initialize-var-mounts, which fills the/varvolume itself: it writes a second marker,.bootc-var-mounts, in the staterootvar, and setup-root accepts either. Reusing.bootc-defer-vardoesn't work, because composefs seeding would then skip the state directory that--initialize-var-mountsalone still seeds.--defer-varalso apply toinstall to-disk? It only makes sense when the caller provides/var, so it is limited toto-filesystemfor now./varwas deferred.Generated-by: https://github.com/jmarrero-bot#llms
Review draft in jmarrero-forge, not upstream yet. This section is removed when the PR is opened upstream.
bootc-dev/bootc, basemainPVTI_lADOFBIZ7c4BlZPrzhEPhnoTo review:
/promoteon a line of its own, to open it upstream, ready for review. Either covers only the commits pushed so far.Signed-off-by: Joseph Marrero Corchado <jmarrero@redhat.com>to the commits lacking it (the bot's and yours; anyone else's only if you ask), with you as committer./draftline (in the same comment or before) to open it upstream as a draft (/readyundoes that).Bot run: session e7bb5c51, agents a819c411 +2 · claude-opus-5-5 102.5M in (97% cached) / 249k out · ~$41.99 inference + ~$4.99 compute (est., list prices) · 2h 5m wall · 31.7 core-h, Actions run 40714907801
Bot run: session e7bb5c51, agents a819c411 +2 · claude-opus-5-5 223.6M in (95% cached) / 378k out · ~$108.13 inference + ~$11.52 compute (est., list prices) · 17h 57m wall · 73.1 core-h, Actions runs 40832841988, 40714907801
Bot run: session e7bb5c51, agents a6341f71 +1 · claude-opus-5-5 30.0M in (97% cached) / 137k out · ~$12.66 inference + ~$2.58 compute (est., list prices) · 15h 54m wall · 16.4 core-h, Actions runs 40848700131, 40739864348