Installing Fedora on a computer that hadn't shipped yet
Contents
The oldest boot in this machine's journal is from 5 October 2026. The machine first ran Fedora on 9 October. The twelve boots before that happened on a different computer.
$ journalctl --list-boots --no-pager | head -14
IDX BOOT ID FIRST ENTRY LAST ENTRY
-29 f657a145f7974ea18abc160d1e2be8d6 Mon 2026-10-05 16:58:39 EEST Mon 2026-10-05 16:59:24 EEST
-28 cfcb877ad4144b119517fa1c642346da Mon 2026-10-05 16:59:48 EEST Mon 2026-10-05 17:00:52 EEST
-27 36916dd2ff404daca7a8d530c48cbb5c Mon 2026-10-05 17:01:17 EEST Mon 2026-10-05 17:02:00 EEST
-26 b91bc2749b424de590c0be29c42c55ef Tue 2026-10-06 02:59:57 EEST Tue 2026-10-06 00:06:08 EEST
-25 2f3c0e624c9149dcb68e48f5fb76f584 Tue 2026-10-06 00:16:04 EEST Tue 2026-10-06 00:18:53 EEST
-24 bc4795b434bb42e9b639371238174409 Tue 2026-10-06 09:43:36 EEST Tue 2026-10-06 11:06:32 EEST
-23 9f2f8229067a414080a2b3c7246ecabd Tue 2026-10-06 11:07:03 EEST Tue 2026-10-06 11:08:09 EEST
-22 d756260ad69a48e484907b5f6851e066 Tue 2026-10-06 15:20:22 EEST Tue 2026-10-06 15:22:12 EEST
-21 83d51b23ac24402b84dcac40d041cfc9 Tue 2026-10-06 15:22:36 EEST Tue 2026-10-06 15:23:03 EEST
-20 25132501db5f4ec8a01c52d7e5d8ec88 Tue 2026-10-06 15:58:15 EEST Tue 2026-10-06 16:35:01 EEST
-19 52412be57f7546849748767f08cc6791 Tue 2026-10-06 19:12:31 EEST Tue 2026-10-06 22:31:30 EEST
-18 9101d8c8bc204e8a8ff190db670a7bc8 Tue 2026-10-06 22:31:55 EEST Tue 2026-10-06 22:33:03 EEST
-17 4e6687cdb2084df0885005a1dec46dce Fri 2026-10-09 13:29:55 EEST Fri 2026-10-09 11:06:18 EEST
Boots −29 through −18 all ran on a GEEKOM AE8, booting a USB disk the A9 Mega had never seen: eleven under QEMU, one (−26) on the AE8's own hardware. Boot −17 is the A9 Mega itself, for the first time. By its own clock it starts at 13:29 and ends at 11:06 the same day.
That one row is the whole post. The install was built, boot-tested and debugged before the box first ran Linux. The rest of the bugs came in the box.
Building the install in a container on another machine#
The target: a GEEKOM A9 Mega. Ryzen AI Max+ 395, 128 GB of soldered LPDDR5X-8000, a 2 TB Crucial P310 with Windows on it. The plan: have the operating system finished before the hardware was.
On 5 October the AE8 (Fedora 44, no sudo) ran a --privileged fedora:45 container with the host's /dev bound in. Inside it, dnf --installroot put Fedora 45 KDE onto an ADATA SX8200 Pro 512 GB sitting in a USB enclosure. The initramfs was generic (hostonly=no), because this one disk would meet three storage paths: usb-storage on the AE8, an emulated NVMe controller in QEMU, USB again on the A9.
A normal install runs Anaconda on the target machine. It never meets the next three problems.
The container writes no SELinux labels#
Files laid down by dnf --installroot inside a container carry no SELinux contexts. Boot that disk in enforcing mode and systemd freezes with "Failed to allocate manager object". Touching /.autorelabel doesn't help: the marker is only acted on by a system that finishes booting, and this one doesn't.
The fix is to do what Anaconda does, from inside the chroot, before the disk ever boots. From inner.sh:
# container writes no SELinux labels; unlabeled files freeze systemd under enforcing before
# .autorelabel can run ("Failed to allocate manager object"). Label like Anaconda does.
chroot $T setfiles -F -T 0 -e /proc -e /sys -e /dev -e /boot/efi \
/etc/selinux/targeted/contexts/files/file_contexts / /boot /home
touch $T/.autorelabel # second pass on first boot catches stragglers
kernel-install copies the wrong machine's command line#
The second container trap is quieter. With no /etc/kernel/cmdline in the target, kernel-install fills the new BLS entry from /proc/cmdline, which belongs to the AE8. The disk would have booted with the build host's root UUID. Same file, earlier:
echo "== pre-seed config read by kernel-install / dracut"
mkdir -p $T/etc/kernel $T/etc/dracut.conf.d
# without this kernel-install copies the HOST /proc/cmdline into the BLS entry
echo "root=UUID=$U_ROOT ro rootflags=subvol=root $CMDLINE_EXTRA" > $T/etc/kernel/cmdline
dracut's crypt generator returns from nowhere#
The first VM boot, 5 October, 16:58, still in the A9's journal:
$ journalctl -b -29 -o short-iso --no-pager | grep -E 'crypt-generator'
2026-10-05T16:58:39+03:00 fedora (gene[179]: '/usr/lib/systemd/system-generators/dracut-crypt-generator' failed with exit status 2.
There is no LUKS on this disk. The generator should look for /etc/crypttab, find none, and leave. The dracut-111-4.fc45 package on the box says this, in 70crypt/crypt-generator.sh:
[ -e /etc/crypttab ] || return 0
return outside a function. The script's shebang is #!/usr/bin/sh, which on Fedora is bash in POSIX mode, and there that's fatal. Under plain bash it isn't, which is how it survives a casual test:
+ cat gen-sh.sh
#!/usr/bin/sh
[ -e /etc/no-such-crypttab ] || return 0
echo "still running"
+ ./gen-sh.sh
./gen-sh.sh: line 2: return: can only `return' from a function or sourced script
+ echo rc=2
rc=2
+ ./gen-bash.sh
./gen-bash.sh: line 2: return: can only `return' from a function or sourced script
still running
+ echo rc=0
rc=0
Upstream already knows. dracut-ng fixed it on 8 July 2026 in fbb7270, "fix(crypt): do not call return to exit a systemd generator":
if ! getargbool 1 rd.luks; then
# crypto LUKS detection is disabled
- return 0
+ exit 0
fi
-[ -e /etc/crypttab ] || return 0
+[ -e /etc/crypttab ] || exit 0
(Excerpt: the same hunk also turns a return 1 into exit 1.)
A host-only initramfs on a machine without LUKS never ships that module: its check() returns 255 unless a crypto_LUKS filesystem is present. A generic one with the crypt and systemd modules does, so every boot logs the failure. The bench disk drops the module instead of patching dracut:
omit_dracutmodules+=" crypt systemd-cryptsetup " # no LUKS; F45 dracut-111 crypt-generator.sh bug
The cable negotiated USB 2.0#
Not a software bug, and the one no amount of scripting finds. The non-Thunderbolt cables tested on 6 October linked at USB 2.0: 480 Mb/s, about 41 MB/s. A Thunderbolt 3 cable on the USB4 port gave 10 Gbps and 834 MB/s. driver=usb-storage stayed, and that's expected: a kernel quirk makes the enclosure's bridge (0b05:1a8a) skip UAS.
What QEMU proved before first boot#
The disk booted in QEMU with OVMF and Secure Boot on, presented as an emulated NVMe drive. It reached the Plasma login screen with SELinux enforcing and zero AVC denials. The trust chain was Fedora's own signed shim and kernel, so there was nothing to sign. That's a much shorter story than signing a kernel you built yourself.
On 9 October the disk was plugged into the real box.
What only the hardware could break#
The RTC was on Windows time#
Fifty-nine seconds into the first real boot:
$ journalctl -b -17 -o short-iso --no-pager | grep chronyd | grep -E 'wrong|stepped'
2026-10-09T13:30:52+03:00 a9-mega chronyd[1173]: System clock wrong by -10799.995388 seconds
2026-10-09T10:30:52+03:00 a9-mega chronyd[1173]: System clock was stepped by -10799.995388 seconds
The A9 shipped with Windows, and, per the Arch wiki, "By default, Windows uses localtime". The RTC held UTC+3. Linux read it as UTC and started the day three hours in the future. An installer running on the box would at least have had the chance to see Windows on the internal drive. Fix: hwclock --systohc --utc, once.
The symptom had already shown up three days earlier. Boot −26 in the opening listing has the same inverted pair, and it wasn't QEMU: its kernel logged DMI: GEEKOM AE8/AE8. The test disk had booted the AE8's own hardware once, and that machine's RTC also held local time, and chrony stepped that clock by −10800.27 seconds just after midnight on 6 October. The VMs never showed it. The one bare-metal boot did, and nobody looked.
Plasma put a headless box to sleep#
$ journalctl -b -17 -o short-iso --no-pager | grep -E 'suspend requested|PM: suspend (entry|exit)'
2026-10-09T10:49:39+03:00 a9-mega systemd-logind[1237]: suspend requested from client PID 2769 ('org_kde_powerde') (unit user@1000.service)...
2026-10-09T10:49:40+03:00 a9-mega kernel: PM: suspend entry (s2idle)
2026-10-09T10:49:41+03:00 a9-mega kernel: PM: suspend exit
2026-10-09T10:49:41+03:00 a9-mega kernel: PM: suspend entry (s2idle)
2026-10-09T10:56:36+03:00 a9-mega kernel: PM: suspend exit
1187 seconds into the boot, Plasma's power manager decided the session was idle and asked logind to suspend. The box went into s2idle and took SSH, the display and the KVM with it. Only a press of the power button brought it back.
For a machine whose whole job is to run benchmarks unattended, that's the wrong default by about twenty minutes. The fix masks every way in:
systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target suspend-then-hibernate.target
Later the same day systemctl set-default multi-user.target removed Plasma from boot altogether. The AE8 had its own power-policy surprise, in the other direction: it refused to idle.
Two harness checks the VM always skipped#
The acceptance harness SKIPs its hardware checks under virtualisation. So the first real run was also the first time two lines of it executed.
The CPU check grepped for AI Max+ 395, the way the product page spells it. The silicon spells it differently, per the bench's meta.txt:
cpu=AMD RYZEN AI MAX+ 395 w/ Radeon 8060S
Now it's grep -qi. The other check piped rocminfo into grep -q under pipefail. grep -q exits on the first match, rocminfo keeps writing into a closed pipe, takes SIGPIPE, and the pipeline reports 128 + 13. Reproduced on the A9 on 10 October:
$ set -o pipefail; rocminfo | grep -q gfx1151; echo $?
141
$ set +o pipefail; rocminfo | grep -q gfx1151; echo $?
0
The GPU was there. The check that looks for it failed.
The hostname lived on two networks#
The A9 has wired Ethernet and Wi-Fi, and both came up. Avahi announced a9-mega.local on both, and the name flipped between the two addresses. SSH sessions landed on the Wi-Fi address while model downloads ran over the wire. None of this was testable in QEMU: its user-mode network has no multicast. The fix publishes mDNS on the wired port only (allow-interfaces=enp196s0) and pins that port to a static DHCP lease.
The factory left a note#
The P310 also carried OA3.txt, the OEM activation report, including the Windows product key. The key is also in the firmware's MSDM table. The two were compared on the box and matched, without either ever being printed. So the license lives in firmware, and wiping the drive costs nothing.
Moving the running system onto the internal P310#
The USB disk had done its job. fix-10-migrate-nvme.sh erased the P310, Windows and recovery partition included, and copied the live system across with rsync -aHAXS. A live copy loses files mid-flight, so one exit code is tolerated:
# rsync exit 24 = files vanished during a live copy (logs, sockets): harmless here.
rs() { rsync "$@" || { rc=$?; [ $rc -eq 24 ] || exit $rc; }; }
The new filesystems got new UUIDs, because the USB disk stays attached and two btrfs filesystems with one UUID is a bad afternoon. The script rewrote fstab, /etc/kernel/cmdline, the BLS entries and the ESP's grub stub to match, and added a UEFI entry. The USB disk was left untouched as a bootable fallback.
The same quick bench, before and after (fio, sequential read):
- USB bench disk: 618 MiB/s (quick run, 9 October 11:14)
- Internal P310: 6814 MiB/s, plus 673,585 random-read IOPS (quick run, 9 October 20:44)
The generic initramfs went too. A host-only image for NVMe plus amdgpu is 81 MiB and boots in about 15 seconds; the 228 MiB generic one stays behind as the rescue entry.
Boot −17 is still in the journal, ending before it starts. Every boot after it is in order.
Filed under: linux, fedora, amd, hardware, qemu, dracut
Read next#
- 120 GiB for the GPU, set in the kernel, not the BIOS — part 2: the obvious BIOS memory knob is the wrong one.
Sources#
- dracut-ng commit fbb7270 — "fix(crypt): do not call return to exit a systemd generator", 8 July 2026; the
return 0→exit 0change. - dracut-ng
70crypt/crypt-generator.shon main — upstream now usesexit 0. - Arch wiki: System time — "By default, Windows uses localtime"; the RTC local-time vs UTC background.