From 592dc9a2d3fb1640edb2a4298d3c92327ceb6f97 Mon Sep 17 00:00:00 2001 From: bfjelds Date: Mon, 1 Jun 2026 13:18:57 -0700 Subject: [PATCH 01/22] feat: dedicated verity hash partitions, UUID-based boot, and trident integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move dm-verity from inline hash mode (appended to USR partition) to dedicated HASH-A/HASH-B partitions. Switch boot cmdline from partition UUIDs (PARTUUID=) to filesystem/verity UUIDs (UUID=) so the same UKI works for both A and B slots, enabling A/B updates. Changes: - disk_layout_uki.json: Add HASH-A (3) and HASH-B (5) partitions with dps-usr-verity type, renumber OEM→6 and ROOT→7 - disk_util: Add DPS_USR_VERITY_GUIDS, support verity_hash cross-ref to dedicated hash partition, extract verity/fs UUIDs - build_image_util.sh: Pass --verity_uuid and --fs_uuid through the verity and bootloader pipeline - grub.cfg: Use UUID= instead of PARTUUID= with hash-offset - grub_install.sh: Inject @@FSUUID@@ and @@VERITYUUID@@ placeholders - uki_install.sh: Read UUIDs from build artifacts instead of static disk_layout.json values, add --verity_uuid/--fs_uuid flags - package_catalog.yaml: Add trident to systemd package group - build_image_util.sh (rpm): Enable tridentd.socket at build time - rpm_install.sh: Temporarily disable SELinux module removal to unblock trident install Based on changes from user/frhuelsz/ab-update-trident branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- acl/docs/architecture.md | 5 +- build_library/build_image_util.sh | 11 +++ build_library/disk_layout_uki.json | 34 ++++++-- build_library/disk_util | 105 +++++++++++++++++++------ build_library/rpm/build_image_util.sh | 5 ++ build_library/rpm/grub.cfg | 16 ++-- build_library/rpm/grub_install.sh | 22 ++++++ build_library/rpm/package_catalog.yaml | 1 + build_library/rpm/rpm_install.sh | 86 ++++++++++---------- build_library/rpm/uki_install.sh | 52 +++++++----- 10 files changed, 238 insertions(+), 99 deletions(-) diff --git a/acl/docs/architecture.md b/acl/docs/architecture.md index dce81d48c89..083b09d5cb4 100644 --- a/acl/docs/architecture.md +++ b/acl/docs/architecture.md @@ -53,8 +53,9 @@ ACL's primary boot path uses **systemd-boot** with **Unified Kernel Images (UKI) The `/usr` partition (USR-A) is a read-only btrfs filesystem with zstd compression. **dm-verity** provides block-level integrity verification: -- The verity hash tree is appended to the USR partition data. -- At boot, `systemd-veritysetup` activates the verity device using kernel command-line parameters embedded in the UKI: `systemd.verity_usr_data`, `systemd.verity_usr_hash`, and `systemd.verity_usr_options=hash-offset=,panic-on-corruption`. +- The verity hash tree is stored in a dedicated hash partition (HASH-A, immediately following USR-A on disk). +- At boot, `systemd-veritysetup` activates the verity device using kernel command-line parameters embedded in the UKI: `systemd.verity_usr_data=UUID=`, `systemd.verity_usr_hash=UUID=`, and `systemd.verity_usr_options=panic-on-corruption`. +- The UKI uses filesystem/verity UUIDs (not partition UUIDs) so the same UKI works for both A and B slots. - Any corruption of `/usr` causes an immediate kernel panic, preventing the system from running a tampered image. The A/B partition scheme (USR-A / USR-B) enables safe updates: the inactive slot is written, verified, and then atomically switched on reboot. diff --git a/build_library/build_image_util.sh b/build_library/build_image_util.sh index bf7f4dbba1b..3625ab9386a 100755 --- a/build_library/build_image_util.sh +++ b/build_library/build_image_util.sh @@ -886,6 +886,8 @@ EOF "${BUILD_LIBRARY_DIR}/disk_util" --disk_layout="${disk_layout}" --arch="${BOARD}" verity \ --root_hash="${BUILD_DIR}/${image_name%.bin}_verity.txt" \ + --verity_uuid="${BUILD_DIR}/${image_name%.bin}_verity_uuid.txt" \ + --fs_uuid="${BUILD_DIR}/${image_name%.bin}_fs_uuid.txt" \ "${BUILD_DIR}/${image_name}" # Magic alert! Root hash injection works by writing the hash value to a @@ -953,6 +955,15 @@ EOF if [[ -f "${verity_hash_file}" ]]; then bootloader_args+=(--verity_hash="${verity_hash_file}") fi + # Pass UUID files for UUID-based cmdline construction + local verity_uuid_file="${BUILD_DIR}/${image_name%.bin}_verity_uuid.txt" + if [[ -f "${verity_uuid_file}" ]]; then + bootloader_args+=(--verity_uuid="${verity_uuid_file}") + fi + local fs_uuid_file="${BUILD_DIR}/${image_name%.bin}_fs_uuid.txt" + if [[ -f "${fs_uuid_file}" ]]; then + bootloader_args+=(--fs_uuid="${fs_uuid_file}") + fi fi else bootloader_args+=(--noverity) diff --git a/build_library/disk_layout_uki.json b/build_library/disk_layout_uki.json index 5d4a16e79f5..c964f7af8f6 100644 --- a/build_library/disk_layout_uki.json +++ b/build_library/disk_layout_uki.json @@ -23,23 +23,37 @@ "uuid": "7130c94a-213a-4e5a-8e26-6cce9662f132", "type": "flatcar-rootfs", "blocks": "2097152", - "fs_blocks": "260094", + "fs_blocks": "262144", "fs_type": "btrfs", "fs_compression": "zstd", "mount": "/usr", + "verity_hash": "3", "features": [ "prioritize", "verity" ] }, "3": { + "label": "HASH-A", + "uuid": "b736baf1-cdb4-4535-beba-ddaaa30ad7b7", + "type": "dps-usr-verity", + "blocks": "20480" + }, + "4": { "label": "USR-B", "uuid": "e03dd35c-7c2d-4a47-b3fe-27f15780a57c", "type": "flatcar-rootfs", "blocks": "2097152", - "fs_blocks": "262144" + "fs_blocks": "262144", + "verity_hash": "5" }, - "4": { + "5": { + "label": "HASH-B", + "uuid": "35bdf78b-c453-4661-98e6-f834f534ef5b", + "type": "dps-usr-verity", + "blocks": "20480" + }, + "6": { "label": "OEM", "fs_label": "OEM", "type": "data", @@ -48,7 +62,7 @@ "fs_compression": "zlib", "mount": "/oem" }, - "5": { + "7": { "label": "ROOT", "fs_label": "ROOT", "type": "dps-root", @@ -58,21 +72,21 @@ } }, "vm": { - "5": { + "7": { "label": "ROOT", "fs_label": "ROOT", "blocks": "12943360" } }, "azure": { - "5": { + "7": { "label": "ROOT", "fs_label": "ROOT", "blocks": "58875904" } }, "vagrant": { - "5": { + "7": { "label": "ROOT", "fs_label": "ROOT", "blocks": "33845248" @@ -92,6 +106,12 @@ "type": "blank" }, "5": { + "type": "blank" + }, + "6": { + "type": "blank" + }, + "7": { "label": "ROOT", "fs_label": "ROOT", "type": "0fc63daf-8483-4772-8e79-3d69d8477de4", diff --git a/build_library/disk_util b/build_library/disk_util index 9a37343cb10..2a0c3018ae0 100755 --- a/build_library/disk_util +++ b/build_library/disk_util @@ -34,6 +34,11 @@ DPS_ROOT_GUIDS = { 'aarch64': 'B921B045-1DF0-41C3-AF44-4C6F280D3FAE', } +DPS_USR_VERITY_GUIDS = { + 'x86_64': '77FF5F63-E7B6-4633-ACF4-1565B864C0E6', + 'aarch64': '6E11A4E7-FBCA-4DED-B9E9-E1A512BB664E', +} + # Map BOARD names used by the build system to machine architecture values. BOARD_TO_ARCH = { 'amd64-usr': 'x86_64', @@ -44,18 +49,24 @@ def _resolve_dps_types(config, arch): """Replace symbolic DPS partition type names with architecture-specific GUIDs. Currently supported placeholders: - dps-root → DPS root partition GUID for the target architecture + dps-root → DPS root partition GUID for the target architecture + dps-usr-verity → DPS /usr verity hash partition GUID """ + dps_map = { + 'dps-root': DPS_ROOT_GUIDS, + 'dps-usr-verity': DPS_USR_VERITY_GUIDS, + } for layout in config.get('layouts', {}).values(): for part in layout.values(): if not isinstance(part, dict): continue ptype = part.get('type', '') - if ptype == 'dps-root': - guid = DPS_ROOT_GUIDS.get(arch) + guids = dps_map.get(ptype) + if guids is not None: + guid = guids.get(arch) if guid is None: raise InvalidLayout( - 'No DPS root GUID defined for architecture %r' % arch) + 'No %s GUID defined for architecture %r' % (ptype, arch)) part['type'] = guid @@ -73,7 +84,7 @@ def LoadPartitionConfig(options): '_comment', 'type', 'num', 'label', 'blocks', 'block_size', 'fs_blocks', 'fs_block_size', 'fs_type', 'features', 'uuid', 'part_alignment', 'mount', 'binds', 'fs_subvolume', 'fs_bytes_per_inode', 'fs_inode_size', 'fs_label', - 'fs_compression')) + 'fs_compression', 'verity_hash')) integer_layout_keys = set(( 'blocks', 'block_size', 'fs_blocks', 'fs_block_size', 'part_alignment', 'fs_bytes_per_inode', 'fs_inode_size')) @@ -807,6 +818,12 @@ def Tune(options): def Verity(options): """Hash verity protected filesystems. + Only processes partitions that have a filesystem (fs_type) and either a + verity_hash cross-reference to a dedicated hash partition or the legacy + 'verity' feature flag for inline hash mode. At build time only the + active USR slot (the one with 'prioritize') is formatted, so the + inactive slot is skipped naturally. + Args: options: Flags passed to the script """ @@ -815,7 +832,15 @@ def Verity(options): GetPartitionTableFromImage(options, config, partitions) for part_num, part in partitions.items(): - if 'verity' not in part.get('features', []): + hash_part_num = part.get('verity_hash') + has_verity_feature = 'verity' in part.get('features', []) + + if not hash_part_num and not has_verity_feature: + continue + + # Skip partitions without a filesystem — at build time only the active + # slot (USR-A with 'prioritize') is formatted; USR-B is empty. + if not part.get('fs_type'): continue if not part['image_compat']: @@ -826,22 +851,56 @@ def Verity(options): elif part.get('fs_type', None) == 'btrfs': ReadWriteSubvol(options, part, disable_rw=True) - with PartitionLoop(options, part) as loop_dev: - verityout = SudoOutput(['veritysetup', 'format', '--hash=sha256', - '--data-block-size', part['fs_block_size'], - '--hash-block-size', part['fs_block_size'], - '--data-blocks', part['fs_blocks'], - '--hash-offset', part['fs_bytes'], - loop_dev, loop_dev]).decode('utf8') - print(verityout.strip()) - m = re.search(r'Root hash:\s+([a-f0-9]{64})$', verityout, re.IGNORECASE|re.MULTILINE) - if not m: - raise Exception("Failed to parse verity output!") - - if options.root_hash != None: - with open(options.root_hash, "w") as hash_file: - hash_file.write(m.group(1)) - hash_file.write("\n") + if hash_part_num: + # Separate hash partition mode: write hash tree to a dedicated partition. + hash_part = partitions[hash_part_num] + if not hash_part['image_compat']: + raise InvalidLayout("Hash partition is incompatible with existing image") + + with PartitionLoop(options, part) as data_loop, \ + PartitionLoop(options, hash_part) as hash_loop: + verityout = SudoOutput(['veritysetup', 'format', '--hash=sha256', + '--data-block-size', part['fs_block_size'], + '--hash-block-size', part['fs_block_size'], + '--data-blocks', part['fs_blocks'], + data_loop, hash_loop]).decode('utf8') + print(verityout.strip()) + + # Read the btrfs FS UUID from the data partition for cmdline use. + if options.fs_uuid: + fs_uuid_out = SudoOutput( + ['blkid', '-s', 'UUID', '-o', 'value', data_loop]).decode('utf8') + with open(options.fs_uuid, 'w') as f: + f.write(fs_uuid_out.strip() + '\n') + + else: + # Legacy inline mode: hash tree appended within the same partition. + with PartitionLoop(options, part) as loop_dev: + verityout = SudoOutput(['veritysetup', 'format', '--hash=sha256', + '--data-block-size', part['fs_block_size'], + '--hash-block-size', part['fs_block_size'], + '--data-blocks', part['fs_blocks'], + '--hash-offset', part['fs_bytes'], + loop_dev, loop_dev]).decode('utf8') + print(verityout.strip()) + + m = re.search(r'Root hash:\s+([a-f0-9]{64})$', verityout, re.IGNORECASE|re.MULTILINE) + if not m: + raise Exception("Failed to parse verity root hash!") + + if options.root_hash != None: + with open(options.root_hash, "w") as hash_file: + hash_file.write(m.group(1)) + hash_file.write("\n") + + # Extract the verity superblock UUID from veritysetup output. + if options.verity_uuid: + m_uuid = re.search(r'UUID:\s+([a-f0-9-]{36})$', verityout, re.IGNORECASE|re.MULTILINE) + if not m_uuid: + raise Exception("Failed to parse verity UUID!") + with open(options.verity_uuid, 'w') as f: + f.write(m_uuid.group(1)) + f.write("\n") def Extract(options): @@ -1118,6 +1177,8 @@ def main(argv): a = actions.add_parser('verity', help='compute verity hashes') a.add_argument('disk_image', help='path to disk image file') a.add_argument('--root_hash', help='name of file to contain root hash') + a.add_argument('--verity_uuid', help='name of file to contain verity superblock UUID') + a.add_argument('--fs_uuid', help='name of file to contain data partition filesystem UUID') a.set_defaults(func=Verity) a = actions.add_parser('extract', help='extract a single partition') diff --git a/build_library/rpm/build_image_util.sh b/build_library/rpm/build_image_util.sh index 4aadbcc7bbd..0b5646d58f1 100644 --- a/build_library/rpm/build_image_util.sh +++ b/build_library/rpm/build_image_util.sh @@ -592,6 +592,11 @@ EOF sudo sed -i 's|/var/run/|/run/|g' "${root_fs_dir}/usr/lib/systemd/system/rpcbind.socket" fi + # Enable tridentd.socket - listens for trident API requests + info "RPM mode: Enabling tridentd.socket" + sudo mkdir -p "${root_fs_dir}/usr/lib/systemd/system/sockets.target.wants" + sudo ln -sf ../tridentd.socket "${root_fs_dir}/usr/lib/systemd/system/sockets.target.wants/tridentd.socket" + # Create /var/lib/nfs directories needed by rpc-statd and NFS server via tmpfiles # The nfs-utils RPM only creates v4recovery; sm and sm.bak are missing from the package # /var is stateful so we use tmpfiles.d to create these at boot, not mkdir at build time diff --git a/build_library/rpm/grub.cfg b/build_library/rpm/grub.cfg index 377c3915d61..41e0f2ad38e 100644 --- a/build_library/rpm/grub.cfg +++ b/build_library/rpm/grub.cfg @@ -116,17 +116,17 @@ set azl_kernel="@@KERNEL@@" # systemd-veritysetup-generator reads usrhash= to create /dev/mapper/usr set verity_usrhash="@@USRHASH@@" -# Azure Linux GRUB doesn't have gptprio.next command -# Boot directly from USR-A partition. A/B updates would require custom handling. -# USR-A partition UUID from disk_layout.json +# Filesystem UUID (btrfs) and verity superblock UUID, injected at build time. +# These are A/B agnostic: the update agent swaps which partition holds the +# active UUID, and systemd resolves UUID= to the correct device. +set usr_fs_uuid="@@FSUUID@@" +set verity_uuid="@@VERITYUUID@@" + +# Partition UUIDs for non-verity recovery menu entries only. set usr_a_uuid="7130c94a-213a-4e5a-8e26-6cce9662f132" set usr_b_uuid="e03dd35c-7c2d-4a47-b3fe-27f15780a57c" -# Verity hash offset: fs_blocks * fs_block_size = 260094 * 4096 = 1065345024 -# The hash tree is stored at the end of the USR partition after the filesystem data -set verity_hash_offset="1065345024" - -set verity_cmdline="@@MOUNTUSR@@ usrhash=$verity_usrhash systemd.verity_usr_data=PARTUUID=$usr_a_uuid systemd.verity_usr_hash=PARTUUID=$usr_a_uuid systemd.verity_usr_options=hash-offset=$verity_hash_offset,panic-on-corruption" +set verity_cmdline="@@MOUNTUSR@@ usrhash=$verity_usrhash systemd.verity_usr_data=UUID=$usr_fs_uuid systemd.verity_usr_hash=UUID=$verity_uuid systemd.verity_usr_options=panic-on-corruption" menuentry "Azure Container Linux default" --id=flatcar --unrestricted { linux$suf $azl_kernel $verity_cmdline $linux_cmdline diff --git a/build_library/rpm/grub_install.sh b/build_library/rpm/grub_install.sh index 563ebe6a46f..234629517be 100644 --- a/build_library/rpm/grub_install.sh +++ b/build_library/rpm/grub_install.sh @@ -125,9 +125,31 @@ grub_provision_rpm() { warn "RPM mode: Verity enabled but no hash file provided" sed_cmds+=(-e 's/@@USRHASH@@//') fi + + # Inject filesystem and verity UUIDs for A/B agnostic boot + if [[ -n "${FLAGS_fs_uuid}" && -f "${FLAGS_fs_uuid}" ]]; then + local grub_fs_uuid + grub_fs_uuid=$(cat "${FLAGS_fs_uuid}") + info "RPM mode: Injecting FS UUID ${grub_fs_uuid}" + sed_cmds+=(-e "s/@@FSUUID@@/${grub_fs_uuid}/") + else + warn "RPM mode: No FS UUID file provided" + sed_cmds+=(-e 's/@@FSUUID@@//') + fi + if [[ -n "${FLAGS_verity_uuid}" && -f "${FLAGS_verity_uuid}" ]]; then + local grub_verity_uuid + grub_verity_uuid=$(cat "${FLAGS_verity_uuid}") + info "RPM mode: Injecting verity UUID ${grub_verity_uuid}" + sed_cmds+=(-e "s/@@VERITYUUID@@/${grub_verity_uuid}/") + else + warn "RPM mode: No verity UUID file provided" + sed_cmds+=(-e 's/@@VERITYUUID@@//') + fi else sed_cmds+=(-e 's/@@MOUNTUSR@@/mount.usr/') sed_cmds+=(-e 's/@@USRHASH@@//') + sed_cmds+=(-e 's/@@FSUUID@@//') + sed_cmds+=(-e 's/@@VERITYUUID@@//') fi if [[ -n "${kernel_name}" ]]; then sed_cmds+=(-e "s|@@KERNEL@@|/flatcar/${kernel_name}|") diff --git a/build_library/rpm/package_catalog.yaml b/build_library/rpm/package_catalog.yaml index 9bddf920d7e..dcc93e3ad99 100644 --- a/build_library/rpm/package_catalog.yaml +++ b/build_library/rpm/package_catalog.yaml @@ -31,6 +31,7 @@ packages: - policycoreutils - ca-certificates - irqbalance + - trident sys-libs/systemd-libs: systemd-libs sys-apps/systemd-networkd: systemd-networkd net-misc/systemd-networkd: systemd-networkd diff --git a/build_library/rpm/rpm_install.sh b/build_library/rpm/rpm_install.sh index 2805920bdb2..03c4103a415 100644 --- a/build_library/rpm/rpm_install.sh +++ b/build_library/rpm/rpm_install.sh @@ -947,48 +947,50 @@ EOF sudo chroot "${root_fs_dir}" semodule -X 200 -i "${policy_hotfix}" sudo rm -f "${root_fs_dir}/${policy_hotfix}" - # Remove unnecessary SELinux policy modules (minimize the policy) - info "RPM mode: Minimizing SELinux policy". - sudo chroot "${root_fs_dir}" semodule -X 100 -r \ - abrt accountsd acct acpi afs aide aisexec alsa amanda amavis amtu anaconda \ - apache apcupsd apt aptcacher arpwatch asterisk auditadm automount avahi \ - awstats backup bacula bind bird bitlbee blueman bluetooth boinc brctl \ - bugzilla cachefilesd calamaris canna cdrecord certbot certmaster certmonger \ - certwatch cfengine cgmanager cgroup chkrootkit chromium chronyd clamav \ - cloudinit cobbler cockpit collectd colord comsat condor consolesetup \ - container_compat corosync couchdb courier cpucontrol cpufreqselector crio \ - cryfs ctdb cups cvs cyphesis cyrus daemontools dante dbadm dbskk ddclient \ - devicekit dhcp dictd dirmngr distcc djbdns dkim dmidecode dnsmasq docker \ - dovecot dphysswapfile dpkg drbd eg25manager entropyd evolution exim fail2ban \ - fakehwclock fapolicyd fcoe fetchmail finger firewalld firstboot fprintd ftp \ - games gatekeeper gdomap geoclue git gitosis glance glusterfs gnome gnomeclock \ - gpg gpm gpsd gssproxy guest hadoop haproxy hddtemp hostapd hwloc hypervkvp \ - i18n_input icecast ifplugd iiosensorproxy inetd inn iodine ipsec irc ircd \ - irqbalance iscsi isns jabber java kdump kerberos kerneloops keystone kismet \ - knot ksmtuned kubernetes l2tp ldap libmtp lightsquid likewise lircd livecd \ - lldpad loadkeys logadm logrotate logwatch lowmemorymonitor lpd lsm mailman \ - man2html mandb matrixd mcelog mediawiki memcached memlockd milter minidlna \ - minissdpd modemmanager mojomojo mon mongodb monit mono monop mozilla mpd \ - mplayer mrtg munin mysql nagios ncftool nessus netlabel networkmanager nis \ - node_exporter nsd nslcd ntop ntp numad nut nx obex obfs4proxy oddjob oident \ - openarc openca openct openhpi openoffice opensm openvpn openvswitch pacemaker pads \ - passenger pcscd pegasus perdition pingd pkcs pki plymouthd podman portmap \ - portreserve portslave postfix postfixpolicyd postgresql postgrey \ - powerprofiles ppp prelink prelude privoxy procmail psad publicfile \ - pulseaudio puppet pwauth pxe pyzor qemu qmail qpid quantum quota rabbitmq \ - radius radvd rasdaemon razor rdisc realmd redis remotelogin resmgr rhsmcertd \ - rkhunter rlogin rngd rootlesskit rpc rpcbind rpm rshd rssh rtkit rwho samba \ - samhain sanlock sasl sblim screen secadm sendmail sensord setroubleshoot \ - seunshare shibboleth shorewall shutdown sigrok slocate slpd slrnpull smartmon \ - smokeping smstools snmp snort sosreport soundserver spamassassin squid stubby \ - stunnel sudo svnserve switcheroo sxid sympa syncthing sysstat systemtap \ - tboot tcpd tcsd telepathy telnet tftp tgtd thunderbird thunderbolt timidity \ - tmpreaper tomcat tor tpm2 transproxy tripwire tuned tvtime tzdata ucspitcp \ - ulogd uml updfstab uptime usbguard usbmodules usbmuxd userhelper usernetctl \ - uucp uuidd uwimap varnishd vbetool vdagent vhostmd virt vlock vmware vnstatd \ - vpn watchdog wdmd webadm webalizer wine wireguard wireshark wm xen xfs \ - xguest xscreensaver xserver zabbix zarafa zebra zfs zosremote \ - > /dev/null + # TODO: Re-enable + # TEMPORARILY DISABLED TO UNBLOCK TRIDENT INSTALL + # # Remove unnecessary SELinux policy modules (minimize the policy) + # info "RPM mode: Minimizing SELinux policy." + # sudo chroot "${root_fs_dir}" semodule -X 100 -r \ + # abrt accountsd acct acpi afs aide aisexec alsa amanda amavis amtu anaconda \ + # apache apcupsd apt aptcacher arpwatch asterisk auditadm automount avahi \ + # awstats backup bacula bind bird bitlbee blueman bluetooth boinc brctl \ + # bugzilla cachefilesd calamaris canna cdrecord certbot certmaster certmonger \ + # certwatch cfengine cgmanager cgroup chkrootkit chromium chronyd clamav \ + # cloudinit cobbler cockpit collectd colord comsat condor consolesetup \ + # container_compat corosync couchdb courier cpucontrol cpufreqselector crio \ + # cryfs ctdb cups cvs cyphesis cyrus daemontools dante dbadm dbskk ddclient \ + # devicekit dhcp dictd dirmngr distcc djbdns dkim dmidecode dnsmasq docker \ + # dovecot dphysswapfile dpkg drbd eg25manager entropyd evolution exim fail2ban \ + # fakehwclock fapolicyd fcoe fetchmail finger firewalld firstboot fprintd ftp \ + # games gatekeeper gdomap geoclue git gitosis glance glusterfs gnome gnomeclock \ + # gpg gpm gpsd gssproxy guest hadoop haproxy hddtemp hostapd hwloc hypervkvp \ + # i18n_input icecast ifplugd iiosensorproxy inetd inn iodine ipsec irc ircd \ + # irqbalance iscsi isns jabber java kdump kerberos kerneloops keystone kismet \ + # knot ksmtuned kubernetes l2tp ldap libmtp lightsquid likewise lircd livecd \ + # lldpad loadkeys logadm logrotate logwatch lowmemorymonitor lpd lsm mailman \ + # man2html mandb matrixd mcelog mediawiki memcached memlockd milter minidlna \ + # minissdpd modemmanager mojomojo mon mongodb monit mono monop mozilla mpd \ + # mplayer mrtg munin mysql nagios ncftool nessus netlabel networkmanager nis \ + # node_exporter nsd nslcd ntop ntp numad nut nx obex obfs4proxy oddjob oident \ + # openarc openca openct openhpi openoffice opensm openvpn openvswitch pacemaker pads \ + # passenger pcscd pegasus perdition pingd pkcs pki plymouthd podman portmap \ + # portreserve portslave postfix postfixpolicyd postgresql postgrey \ + # powerprofiles ppp prelink prelude privoxy procmail psad publicfile \ + # pulseaudio puppet pwauth pxe pyzor qemu qmail qpid quantum quota rabbitmq \ + # radius radvd rasdaemon razor rdisc realmd redis remotelogin resmgr rhsmcertd \ + # rkhunter rlogin rngd rootlesskit rpc rpcbind rpm rshd rssh rtkit rwho samba \ + # samhain sanlock sasl sblim screen secadm sendmail sensord setroubleshoot \ + # seunshare shibboleth shorewall shutdown sigrok slocate slpd slrnpull smartmon \ + # smokeping smstools snmp snort sosreport soundserver spamassassin squid stubby \ + # stunnel sudo svnserve switcheroo sxid sympa syncthing sysstat systemtap \ + # tboot tcpd tcsd telepathy telnet tftp tgtd thunderbird thunderbolt timidity \ + # tmpreaper tomcat tor tpm2 transproxy tripwire tuned tvtime tzdata ucspitcp \ + # ulogd uml updfstab uptime usbguard usbmodules usbmuxd userhelper usernetctl \ + # uucp uuidd uwimap varnishd vbetool vdagent vhostmd virt vlock vmware vnstatd \ + # vpn watchdog wdmd webadm webalizer wine wireguard wireshark wm xen xfs \ + # xguest xscreensaver xserver zabbix zarafa zebra zfs zosremote \ + # > /dev/null info "RPM mode: Adding SELinux policy compatibility fixes" # Add policy name compatibility symlink. The Gentoo MCS policy is diff --git a/build_library/rpm/uki_install.sh b/build_library/rpm/uki_install.sh index a4ea402fa86..2e9741c3af6 100755 --- a/build_library/rpm/uki_install.sh +++ b/build_library/rpm/uki_install.sh @@ -19,6 +19,10 @@ DEFINE_boolean verity ${FLAGS_FALSE} \ "Indicates that boot commands should enable dm-verity." DEFINE_string verity_hash "" \ "Path to the file containing the dm-verity root hash for /usr." +DEFINE_string verity_uuid "" \ + "Path to the file containing the dm-verity superblock UUID." +DEFINE_string fs_uuid "" \ + "Path to the file containing the btrfs filesystem UUID for /usr." # Parse flags FLAGS "$@" || exit 1 @@ -134,22 +138,20 @@ OSREL fi info "UKI/RPM: Using ukify from $(command -v ukify)" - # Read partition UUID and compute verity hash offset from the - # canonical UKI disk layout so the values never drift from the source. - local disk_layout_file="${BUILD_LIBRARY_DIR}/disk_layout_uki.json" - if [[ ! -f "${disk_layout_file}" ]]; then - die "UKI/RPM: disk_layout_uki.json not found at ${disk_layout_file}" + # Read filesystem and verity UUIDs from build artifact files. + # These are random per-image, generated by mkfs.btrfs and veritysetup + # format respectively, then extracted by disk_util verity. + local usr_fs_uuid="" + if [[ -n "${FLAGS_fs_uuid}" && -f "${FLAGS_fs_uuid}" ]]; then + usr_fs_uuid=$(cat "${FLAGS_fs_uuid}") + info "UKI/RPM: USR FS UUID = ${usr_fs_uuid}" fi - local usr_a_uuid - usr_a_uuid=$(jq -r '.layouts.base["2"].uuid' "${disk_layout_file}") - - local verity_hash_offset - verity_hash_offset=$(jq -r \ - '(.layouts.base["2"].fs_blocks | tonumber) as $b | (.metadata.fs_block_size | tonumber) as $s | ($b * $s)' \ - "${disk_layout_file}") - - info "UKI/RPM: USR-A uuid=${usr_a_uuid} verity hash-offset=${verity_hash_offset}" + local verity_superblock_uuid="" + if [[ -n "${FLAGS_verity_uuid}" && -f "${FLAGS_verity_uuid}" ]]; then + verity_superblock_uuid=$(cat "${FLAGS_verity_uuid}") + info "UKI/RPM: Verity superblock UUID = ${verity_superblock_uuid}" + fi local cmdline="" if [[ ${FLAGS_verity} -eq ${FLAGS_TRUE} ]]; then @@ -160,13 +162,27 @@ OSREL else die "UKI/RPM: Verity enabled but no hash file at ${FLAGS_verity_hash}" fi + if [[ -z "${usr_fs_uuid}" ]]; then + die "UKI/RPM: Verity enabled but no FS UUID available" + fi + if [[ -z "${verity_superblock_uuid}" ]]; then + die "UKI/RPM: Verity enabled but no verity UUID available" + fi cmdline="mount.usr=/dev/mapper/usr mount.usrflags=ro" - cmdline+=" systemd.verity_usr_data=PARTUUID=${usr_a_uuid}" - cmdline+=" systemd.verity_usr_hash=PARTUUID=${usr_a_uuid}" - cmdline+=" systemd.verity_usr_options=hash-offset=${verity_hash_offset},panic-on-corruption" + cmdline+=" systemd.verity_usr_data=UUID=${usr_fs_uuid}" + cmdline+=" systemd.verity_usr_hash=UUID=${verity_superblock_uuid}" + cmdline+=" systemd.verity_usr_options=panic-on-corruption" cmdline+=" usrhash=${usr_hash}" else - cmdline="mount.usr=PARTUUID=${usr_a_uuid} mount.usrflags=ro" + if [[ -n "${usr_fs_uuid}" ]]; then + cmdline="mount.usr=UUID=${usr_fs_uuid} mount.usrflags=ro" + else + # Fallback: read partition UUID from disk_layout.json + local disk_layout_file="${BUILD_LIBRARY_DIR}/disk_layout.json" + local usr_a_uuid + usr_a_uuid=$(jq -r '.layouts.base["3"].uuid' "${disk_layout_file}") + cmdline="mount.usr=PARTUUID=${usr_a_uuid} mount.usrflags=ro" + fi fi # Common base args — platform-agnostic, same for all image types. cmdline+=" root=LABEL=ROOT rootflags=rw" From e3532d61e7e6d3e44df8ca4eb94a47c8b6b568e7 Mon Sep 17 00:00:00 2001 From: bfjelds Date: Mon, 1 Jun 2026 14:03:52 -0700 Subject: [PATCH 02/22] feat: move verity slot identity into PARTUUID-based UKI addons Move systemd.verity_usr_data, systemd.verity_usr_hash, and usrhash from the main UKI cmdline into per-slot UKI addons. This enables trident to switch A/B slots by replacing the active addon. The main UKI cmdline now contains only slot-independent args: mount.usrflags=ro root=LABEL=ROOT rootflags=rw consoleblank=0 Each verity addon contains the slot-specific args: mount.usr=/dev/mapper/usr systemd.verity_usr_data=PARTUUID= systemd.verity_usr_hash=PARTUUID= systemd.verity_usr_options=panic-on-corruption usrhash= Build artifacts: EFI/Linux/.extra.d/verity.addon.efi (active, slot A) acl/uki-addons/verity-a.addon.efi (template) acl/uki-addons/verity-b.addon.efi (template) Partition UUIDs are looked up by label (USR-A, HASH-A, etc.) from disk_layout_uki.json for robustness against layout renumbering. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build_library/rpm/uki_install.sh | 198 ++++++++++++++++++++++--------- 1 file changed, 141 insertions(+), 57 deletions(-) diff --git a/build_library/rpm/uki_install.sh b/build_library/rpm/uki_install.sh index 2e9741c3af6..0bd10e9cedd 100755 --- a/build_library/rpm/uki_install.sh +++ b/build_library/rpm/uki_install.sh @@ -138,72 +138,41 @@ OSREL fi info "UKI/RPM: Using ukify from $(command -v ukify)" - # Read filesystem and verity UUIDs from build artifact files. - # These are random per-image, generated by mkfs.btrfs and veritysetup - # format respectively, then extracted by disk_util verity. - local usr_fs_uuid="" - if [[ -n "${FLAGS_fs_uuid}" && -f "${FLAGS_fs_uuid}" ]]; then - usr_fs_uuid=$(cat "${FLAGS_fs_uuid}") - info "UKI/RPM: USR FS UUID = ${usr_fs_uuid}" - fi - - local verity_superblock_uuid="" - if [[ -n "${FLAGS_verity_uuid}" && -f "${FLAGS_verity_uuid}" ]]; then - verity_superblock_uuid=$(cat "${FLAGS_verity_uuid}") - info "UKI/RPM: Verity superblock UUID = ${verity_superblock_uuid}" - fi + # NOTE: Filesystem and verity UUIDs (FLAGS_fs_uuid, FLAGS_verity_uuid) + # are not used by the UKI cmdline — slot identity is delivered via + # verity addons using PARTUUID. The UUID flags are retained because + # they are still passed through to grub_install.sh for grub.cfg. local cmdline="" if [[ ${FLAGS_verity} -eq ${FLAGS_TRUE} ]]; then - local usr_hash="" - if [[ -n "${FLAGS_verity_hash}" && -f "${FLAGS_verity_hash}" ]]; then - usr_hash=$(cat "${FLAGS_verity_hash}") - info "UKI/RPM: Verity hash = ${usr_hash}" - else - die "UKI/RPM: Verity enabled but no hash file at ${FLAGS_verity_hash}" - fi - if [[ -z "${usr_fs_uuid}" ]]; then - die "UKI/RPM: Verity enabled but no FS UUID available" - fi - if [[ -z "${verity_superblock_uuid}" ]]; then - die "UKI/RPM: Verity enabled but no verity UUID available" - fi - cmdline="mount.usr=/dev/mapper/usr mount.usrflags=ro" - cmdline+=" systemd.verity_usr_data=UUID=${usr_fs_uuid}" - cmdline+=" systemd.verity_usr_hash=UUID=${verity_superblock_uuid}" - cmdline+=" systemd.verity_usr_options=panic-on-corruption" - cmdline+=" usrhash=${usr_hash}" + # Verity slot-specific args (data partition, hash partition, root hash) + # are delivered via a UKI addon so that trident can swap A/B slots + # by replacing the active addon. The main UKI only carries + # slot-independent args. + cmdline="mount.usrflags=ro" else - if [[ -n "${usr_fs_uuid}" ]]; then - cmdline="mount.usr=UUID=${usr_fs_uuid} mount.usrflags=ro" - else - # Fallback: read partition UUID from disk_layout.json - local disk_layout_file="${BUILD_LIBRARY_DIR}/disk_layout.json" - local usr_a_uuid - usr_a_uuid=$(jq -r '.layouts.base["3"].uuid' "${disk_layout_file}") - cmdline="mount.usr=PARTUUID=${usr_a_uuid} mount.usrflags=ro" - fi + cmdline="mount.usrflags=ro" fi # Common base args — platform-agnostic, same for all image types. cmdline+=" root=LABEL=ROOT rootflags=rw" cmdline+=" consoleblank=0" - # NOTE: crashkernel=256M is delivered via a UKI addon (kdump.addon.efi) - # rather than baked into the main UKI cmdline. This allows disabling - # kdump by removing the addon from the ESP without rebuilding the UKI. - # See _uki_build_kdump_addon() below. - # OEM / platform identification (oem_id, ignition platform, console - # settings, etc.) are NOT baked into the main UKI. They are injected - # via a per-platform UKI addon built during image_to_vm.sh so that - # each VM image format gets the correct OEM-specific cmdline. - # See build_library/rpm/uki_addon.sh for the addon builder. + # NOTE: The main UKI cmdline contains only slot-independent args. + # Slot-specific args are delivered via UKI addons: # - # NOTE: first-boot args (flatcar.first_boot=detected) are deliberately - # NOT baked into the main UKI cmdline because they must only appear on - # the very first boot. Instead, a separate firstboot addon EFI is - # placed in the .extra.d/ directory; ignition-quench.service deletes it - # after Ignition completes, mirroring how GRUB uses the - # /boot/flatcar/first_boot marker file. - # See _uki_build_firstboot_addon() below. + # verity.addon.efi — Active slot's verity partition identity + # (PARTUUID for data + hash) and root hash. + # Trident swaps this to switch A/B slots. + # See _uki_build_verity_addons() below. + # oem.addon.efi — Platform identification (oem_id, console). + # Built per-platform during image_to_vm.sh. + # See build_library/rpm/uki_addon.sh. + # firstboot.addon.efi — Ignition trigger (flatcar.first_boot=detected). + # Deleted by ignition-quench after first boot. + # See _uki_build_firstboot_addon() below. + # kdump.addon.efi — crashkernel=256M (template, not active by default). + # See _uki_build_kdump_addon() below. + # fips.addon.efi — fips=1 (template, not active by default). + # See _uki_build_fips_addon() below. # Write cmdline to a temp file for ukify echo "${cmdline}" > "${uki_temp_dir}/cmdline.txt" @@ -307,6 +276,11 @@ OSREL _uki_build_kdump_addon "${ESP_DIR}" _uki_build_debug_addon "${ESP_DIR}" "${uki_name}" + # Build A/B verity slot addons — slot A is active by default + if [[ ${FLAGS_verity} -eq ${FLAGS_TRUE} ]]; then + _uki_build_verity_addons "${ESP_DIR}" "${uki_name}" + fi + # Clean up rm -rf "${uki_temp_dir}" } @@ -468,6 +442,116 @@ _uki_build_debug_addon() { rm -rf "${debug_temp_dir}" } +# Build verity slot addons for A/B partition switching. +# +# Each addon contains the slot-specific kernel cmdline args that tell systemd +# which partitions to use for dm-verity: +# mount.usr=/dev/mapper/usr +# systemd.verity_usr_data=PARTUUID= +# systemd.verity_usr_hash=PARTUUID= +# systemd.verity_usr_options=panic-on-corruption +# usrhash= +# +# At boot, systemd-stub appends the active addon's cmdline to the main UKI +# cmdline. Trident swaps which addon is in .extra.d/ to switch slots. +_uki_build_verity_addons() { + local esp_dir="$1" + local uki_name="$2" + + info "UKI/RPM: Building verity slot addons" + + # Read the verity root hash — same for both slots at build time + local usr_hash="" + if [[ -n "${FLAGS_verity_hash}" && -f "${FLAGS_verity_hash}" ]]; then + usr_hash=$(cat "${FLAGS_verity_hash}") + info "UKI/RPM: Verity root hash = ${usr_hash}" + else + die "UKI/RPM: Verity enabled but no hash file at ${FLAGS_verity_hash}" + fi + + # Read partition UUIDs from the disk layout + local disk_layout_file="${BUILD_LIBRARY_DIR}/disk_layout_uki.json" + if [[ ! -f "${disk_layout_file}" ]]; then + die "UKI/RPM: disk_layout_uki.json not found at ${disk_layout_file}" + fi + + # Look up partition UUIDs by label for robustness + local usr_a_uuid usr_b_uuid hash_a_uuid hash_b_uuid + usr_a_uuid=$(jq -r '[.layouts.base[] | select(.label == "USR-A")] | .[0].uuid' "${disk_layout_file}") + hash_a_uuid=$(jq -r '[.layouts.base[] | select(.label == "HASH-A")] | .[0].uuid' "${disk_layout_file}") + usr_b_uuid=$(jq -r '[.layouts.base[] | select(.label == "USR-B")] | .[0].uuid' "${disk_layout_file}") + hash_b_uuid=$(jq -r '[.layouts.base[] | select(.label == "HASH-B")] | .[0].uuid' "${disk_layout_file}") + + if [[ -z "${usr_a_uuid}" || "${usr_a_uuid}" == "null" ]]; then + die "UKI/RPM: Could not find USR-A partition UUID in disk layout" + fi + if [[ -z "${hash_a_uuid}" || "${hash_a_uuid}" == "null" ]]; then + die "UKI/RPM: Could not find HASH-A partition UUID in disk layout" + fi + if [[ -z "${usr_b_uuid}" || "${usr_b_uuid}" == "null" ]]; then + die "UKI/RPM: Could not find USR-B partition UUID in disk layout" + fi + if [[ -z "${hash_b_uuid}" || "${hash_b_uuid}" == "null" ]]; then + die "UKI/RPM: Could not find HASH-B partition UUID in disk layout" + fi + + info "UKI/RPM: Slot A: USR=${usr_a_uuid} HASH=${hash_a_uuid}" + info "UKI/RPM: Slot B: USR=${usr_b_uuid} HASH=${hash_b_uuid}" + + local efi_stub="${BOARD_ROOT}/usr/lib/systemd/boot/efi/linux${EFI_ARCH}.efi.stub" + local verity_temp_dir + verity_temp_dir=$(mktemp -d) + + local template_dir="${esp_dir}/acl/uki-addons" + sudo mkdir -p "${template_dir}" + + # Build addon for each slot + local slot slot_label data_uuid hash_uuid cmdline addon_file + for slot in a b; do + if [[ "${slot}" == "a" ]]; then + slot_label="A" + data_uuid="${usr_a_uuid}" + hash_uuid="${hash_a_uuid}" + else + slot_label="B" + data_uuid="${usr_b_uuid}" + hash_uuid="${hash_b_uuid}" + fi + + cmdline="mount.usr=/dev/mapper/usr" + cmdline+=" systemd.verity_usr_data=PARTUUID=${data_uuid}" + cmdline+=" systemd.verity_usr_hash=PARTUUID=${hash_uuid}" + cmdline+=" systemd.verity_usr_options=panic-on-corruption" + cmdline+=" usrhash=${usr_hash}" + + info "UKI/RPM: Slot ${slot_label} cmdline = ${cmdline}" + + echo "${cmdline}" > "${verity_temp_dir}/verity-${slot}-cmdline.txt" + + addon_file="${verity_temp_dir}/verity-${slot}.addon.efi" + sudo ukify build \ + --cmdline=@"${verity_temp_dir}/verity-${slot}-cmdline.txt" \ + --stub="${efi_stub}" \ + --output="${addon_file}" + + if [[ ! -f "${addon_file}" ]]; then + die "UKI/RPM: ukify failed to produce verity-${slot}.addon.efi" + fi + + # Save template + sudo cp "${addon_file}" "${template_dir}/verity-${slot}.addon.efi" + info "UKI/RPM: Saved verity-${slot}.addon.efi → acl/uki-addons/" + done + + # Install slot A as the active verity addon + local addon_dir="${esp_dir}/EFI/Linux/${uki_name}.extra.d" + sudo mkdir -p "${addon_dir}" + sudo cp "${template_dir}/verity-a.addon.efi" "${addon_dir}/verity.addon.efi" + info "UKI/RPM: Activated slot A → EFI/Linux/${uki_name}.extra.d/verity.addon.efi" + + rm -rf "${verity_temp_dir}" +} + info "Installing UKI packages for target ${FLAGS_target}" uki_install_rpm From 60d5a2d3594f250263663b35cf971fc85884dfd2 Mon Sep 17 00:00:00 2001 From: bfjelds Date: Fri, 5 Jun 2026 11:42:24 -0700 Subject: [PATCH 03/22] fix: restore UUID validation, move mount.usr to main UKI, reject non-verity Address deep review findings DR-001, DR-002, DR-003, DR-006: - DR-002: Restore die guards for FLAGS_fs_uuid and FLAGS_verity_uuid. grub_install.sh only warns on missing UUIDs and silently substitutes empty values into grub.cfg. These guards are the only fail-fast gate. - DR-001: Move mount.usr=/dev/mapper/usr from addon cmdline to main UKI. The mapper device name is slot-independent; only the PARTUUID and root hash are slot-specific. - DR-003: Collapse dead if/else that produced identical cmdline in both branches (vestigial from pre-addon refactoring). - DR-006: Replace silent non-verity else branch with die. ACL UKI is always verity-enabled; a non-verity path would produce a broken image. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build_library/rpm/uki_install.sh | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/build_library/rpm/uki_install.sh b/build_library/rpm/uki_install.sh index 0bd10e9cedd..8cd2c562dda 100755 --- a/build_library/rpm/uki_install.sh +++ b/build_library/rpm/uki_install.sh @@ -142,16 +142,27 @@ OSREL # are not used by the UKI cmdline — slot identity is delivered via # verity addons using PARTUUID. The UUID flags are retained because # they are still passed through to grub_install.sh for grub.cfg. + # + # grub_install.sh only warns on missing UUIDs (substitutes empty values + # into grub.cfg), so validate here to fail fast. + if [[ ${FLAGS_verity} -eq ${FLAGS_TRUE} ]]; then + if [[ -z "${FLAGS_fs_uuid}" || ! -f "${FLAGS_fs_uuid}" ]]; then + die "UKI/RPM: FLAGS_fs_uuid not set or file missing (required for grub.cfg)" + fi + if [[ -z "${FLAGS_verity_uuid}" || ! -f "${FLAGS_verity_uuid}" ]]; then + die "UKI/RPM: FLAGS_verity_uuid not set or file missing (required for grub.cfg)" + fi + fi local cmdline="" if [[ ${FLAGS_verity} -eq ${FLAGS_TRUE} ]]; then - # Verity slot-specific args (data partition, hash partition, root hash) - # are delivered via a UKI addon so that trident can swap A/B slots - # by replacing the active addon. The main UKI only carries - # slot-independent args. - cmdline="mount.usrflags=ro" + # mount.usr is in the main UKI (not the addon) because it is + # slot-independent — the mapper device name is the same for both + # slots. The addon supplies only the slot-specific PARTUUID and + # root hash. + cmdline="mount.usr=/dev/mapper/usr mount.usrflags=ro" else - cmdline="mount.usrflags=ro" + die "UKI/RPM: UKI without verity is not supported" fi # Common base args — platform-agnostic, same for all image types. cmdline+=" root=LABEL=ROOT rootflags=rw" @@ -518,8 +529,8 @@ _uki_build_verity_addons() { hash_uuid="${hash_b_uuid}" fi - cmdline="mount.usr=/dev/mapper/usr" - cmdline+=" systemd.verity_usr_data=PARTUUID=${data_uuid}" + # mount.usr is in the main UKI; addon carries only slot-specific args + cmdline="systemd.verity_usr_data=PARTUUID=${data_uuid}" cmdline+=" systemd.verity_usr_hash=PARTUUID=${hash_uuid}" cmdline+=" systemd.verity_usr_options=panic-on-corruption" cmdline+=" usrhash=${usr_hash}" From 15cd89092e2d2a5fee3c912eba8c6795a5cfeb3a Mon Sep 17 00:00:00 2001 From: bfjelds Date: Fri, 5 Jun 2026 11:48:52 -0700 Subject: [PATCH 04/22] fix: harden jq lookups, validate JSON structure, improve temp cleanup Address deep review findings DR-004, DR-005, DR-007, DR-008: - DR-004/005: Replace bare jq lookups with _lookup_partuuid() helper that validates JSON structure (.layouts.base exists), rejects duplicate labels, and uses jq -e to catch null/missing uuid fields. - DR-007: Move mktemp -d after all validation die paths so early failures don't leak temp directories. - DR-008: Add comment clarifying that both slot templates share the same root hash at build time and trident overwrites at update time. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build_library/rpm/uki_install.sh | 52 +++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/build_library/rpm/uki_install.sh b/build_library/rpm/uki_install.sh index 8cd2c562dda..b77da371cd8 100755 --- a/build_library/rpm/uki_install.sh +++ b/build_library/rpm/uki_install.sh @@ -486,30 +486,41 @@ _uki_build_verity_addons() { die "UKI/RPM: disk_layout_uki.json not found at ${disk_layout_file}" fi - # Look up partition UUIDs by label for robustness - local usr_a_uuid usr_b_uuid hash_a_uuid hash_b_uuid - usr_a_uuid=$(jq -r '[.layouts.base[] | select(.label == "USR-A")] | .[0].uuid' "${disk_layout_file}") - hash_a_uuid=$(jq -r '[.layouts.base[] | select(.label == "HASH-A")] | .[0].uuid' "${disk_layout_file}") - usr_b_uuid=$(jq -r '[.layouts.base[] | select(.label == "USR-B")] | .[0].uuid' "${disk_layout_file}") - hash_b_uuid=$(jq -r '[.layouts.base[] | select(.label == "HASH-B")] | .[0].uuid' "${disk_layout_file}") - - if [[ -z "${usr_a_uuid}" || "${usr_a_uuid}" == "null" ]]; then - die "UKI/RPM: Could not find USR-A partition UUID in disk layout" - fi - if [[ -z "${hash_a_uuid}" || "${hash_a_uuid}" == "null" ]]; then - die "UKI/RPM: Could not find HASH-A partition UUID in disk layout" - fi - if [[ -z "${usr_b_uuid}" || "${usr_b_uuid}" == "null" ]]; then - die "UKI/RPM: Could not find USR-B partition UUID in disk layout" - fi - if [[ -z "${hash_b_uuid}" || "${hash_b_uuid}" == "null" ]]; then - die "UKI/RPM: Could not find HASH-B partition UUID in disk layout" + # Validate JSON structure before extracting UUIDs + if ! jq -e '.layouts.base' "${disk_layout_file}" >/dev/null 2>&1; then + die "UKI/RPM: disk_layout_uki.json missing or malformed .layouts.base" fi + # Helper: look up a partition UUID by label, dying on missing or duplicate + _lookup_partuuid() { + local label="$1" + local matches count uuid + matches=$(jq -r "[.layouts.base[] | select(.label == \"${label}\")] | length" "${disk_layout_file}") + if [[ "${matches}" -eq 0 ]]; then + die "UKI/RPM: No partition with label '${label}' in disk layout" + fi + if [[ "${matches}" -ne 1 ]]; then + die "UKI/RPM: Expected exactly 1 '${label}' partition, found ${matches}" + fi + uuid=$(jq -e -r "[.layouts.base[] | select(.label == \"${label}\")] | .[0].uuid" "${disk_layout_file}") + if [[ -z "${uuid}" || "${uuid}" == "null" ]]; then + die "UKI/RPM: Partition '${label}' has no uuid field" + fi + echo "${uuid}" + } + + local usr_a_uuid usr_b_uuid hash_a_uuid hash_b_uuid + usr_a_uuid=$(_lookup_partuuid "USR-A") + hash_a_uuid=$(_lookup_partuuid "HASH-A") + usr_b_uuid=$(_lookup_partuuid "USR-B") + hash_b_uuid=$(_lookup_partuuid "HASH-B") + info "UKI/RPM: Slot A: USR=${usr_a_uuid} HASH=${hash_a_uuid}" info "UKI/RPM: Slot B: USR=${usr_b_uuid} HASH=${hash_b_uuid}" local efi_stub="${BOARD_ROOT}/usr/lib/systemd/boot/efi/linux${EFI_ARCH}.efi.stub" + + # Create temp dir after validation so die paths above don't leak it local verity_temp_dir verity_temp_dir=$(mktemp -d) @@ -560,6 +571,11 @@ _uki_build_verity_addons() { sudo cp "${template_dir}/verity-a.addon.efi" "${addon_dir}/verity.addon.efi" info "UKI/RPM: Activated slot A → EFI/Linux/${uki_name}.extra.d/verity.addon.efi" + # NOTE: Both slot templates are built with the same root hash at image + # build time. Trident overwrites the active addon (and its root hash) + # when switching A/B slots at OS update time. The templates in + # acl/uki-addons/ are starting points, not the final runtime state. + rm -rf "${verity_temp_dir}" } From 531c25a004c3057b58d22aff60ca91cbc7054ebd Mon Sep 17 00:00:00 2001 From: bfjelds Date: Mon, 13 Jul 2026 13:52:56 -0700 Subject: [PATCH 05/22] Re-enable SELinux policy minimization Uncomment the `semodule -X 100 -r` block in rpm_configure_selinux that was temporarily disabled to unblock the Trident install, restoring removal of unnecessary SELinux policy modules. --- build_library/rpm/rpm_install.sh | 86 ++++++++++++++++---------------- 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/build_library/rpm/rpm_install.sh b/build_library/rpm/rpm_install.sh index 03c4103a415..183168f2fd1 100644 --- a/build_library/rpm/rpm_install.sh +++ b/build_library/rpm/rpm_install.sh @@ -947,50 +947,48 @@ EOF sudo chroot "${root_fs_dir}" semodule -X 200 -i "${policy_hotfix}" sudo rm -f "${root_fs_dir}/${policy_hotfix}" - # TODO: Re-enable - # TEMPORARILY DISABLED TO UNBLOCK TRIDENT INSTALL - # # Remove unnecessary SELinux policy modules (minimize the policy) - # info "RPM mode: Minimizing SELinux policy." - # sudo chroot "${root_fs_dir}" semodule -X 100 -r \ - # abrt accountsd acct acpi afs aide aisexec alsa amanda amavis amtu anaconda \ - # apache apcupsd apt aptcacher arpwatch asterisk auditadm automount avahi \ - # awstats backup bacula bind bird bitlbee blueman bluetooth boinc brctl \ - # bugzilla cachefilesd calamaris canna cdrecord certbot certmaster certmonger \ - # certwatch cfengine cgmanager cgroup chkrootkit chromium chronyd clamav \ - # cloudinit cobbler cockpit collectd colord comsat condor consolesetup \ - # container_compat corosync couchdb courier cpucontrol cpufreqselector crio \ - # cryfs ctdb cups cvs cyphesis cyrus daemontools dante dbadm dbskk ddclient \ - # devicekit dhcp dictd dirmngr distcc djbdns dkim dmidecode dnsmasq docker \ - # dovecot dphysswapfile dpkg drbd eg25manager entropyd evolution exim fail2ban \ - # fakehwclock fapolicyd fcoe fetchmail finger firewalld firstboot fprintd ftp \ - # games gatekeeper gdomap geoclue git gitosis glance glusterfs gnome gnomeclock \ - # gpg gpm gpsd gssproxy guest hadoop haproxy hddtemp hostapd hwloc hypervkvp \ - # i18n_input icecast ifplugd iiosensorproxy inetd inn iodine ipsec irc ircd \ - # irqbalance iscsi isns jabber java kdump kerberos kerneloops keystone kismet \ - # knot ksmtuned kubernetes l2tp ldap libmtp lightsquid likewise lircd livecd \ - # lldpad loadkeys logadm logrotate logwatch lowmemorymonitor lpd lsm mailman \ - # man2html mandb matrixd mcelog mediawiki memcached memlockd milter minidlna \ - # minissdpd modemmanager mojomojo mon mongodb monit mono monop mozilla mpd \ - # mplayer mrtg munin mysql nagios ncftool nessus netlabel networkmanager nis \ - # node_exporter nsd nslcd ntop ntp numad nut nx obex obfs4proxy oddjob oident \ - # openarc openca openct openhpi openoffice opensm openvpn openvswitch pacemaker pads \ - # passenger pcscd pegasus perdition pingd pkcs pki plymouthd podman portmap \ - # portreserve portslave postfix postfixpolicyd postgresql postgrey \ - # powerprofiles ppp prelink prelude privoxy procmail psad publicfile \ - # pulseaudio puppet pwauth pxe pyzor qemu qmail qpid quantum quota rabbitmq \ - # radius radvd rasdaemon razor rdisc realmd redis remotelogin resmgr rhsmcertd \ - # rkhunter rlogin rngd rootlesskit rpc rpcbind rpm rshd rssh rtkit rwho samba \ - # samhain sanlock sasl sblim screen secadm sendmail sensord setroubleshoot \ - # seunshare shibboleth shorewall shutdown sigrok slocate slpd slrnpull smartmon \ - # smokeping smstools snmp snort sosreport soundserver spamassassin squid stubby \ - # stunnel sudo svnserve switcheroo sxid sympa syncthing sysstat systemtap \ - # tboot tcpd tcsd telepathy telnet tftp tgtd thunderbird thunderbolt timidity \ - # tmpreaper tomcat tor tpm2 transproxy tripwire tuned tvtime tzdata ucspitcp \ - # ulogd uml updfstab uptime usbguard usbmodules usbmuxd userhelper usernetctl \ - # uucp uuidd uwimap varnishd vbetool vdagent vhostmd virt vlock vmware vnstatd \ - # vpn watchdog wdmd webadm webalizer wine wireguard wireshark wm xen xfs \ - # xguest xscreensaver xserver zabbix zarafa zebra zfs zosremote \ - # > /dev/null + # Remove unnecessary SELinux policy modules (minimize the policy) + info "RPM mode: Minimizing SELinux policy." + sudo chroot "${root_fs_dir}" semodule -X 100 -r \ + abrt accountsd acct acpi afs aide aisexec alsa amanda amavis amtu anaconda \ + apache apcupsd apt aptcacher arpwatch asterisk auditadm automount avahi \ + awstats backup bacula bind bird bitlbee blueman bluetooth boinc brctl \ + bugzilla cachefilesd calamaris canna cdrecord certbot certmaster certmonger \ + certwatch cfengine cgmanager cgroup chkrootkit chromium chronyd clamav \ + cloudinit cobbler cockpit collectd colord comsat condor consolesetup \ + container_compat corosync couchdb courier cpucontrol cpufreqselector crio \ + cryfs ctdb cups cvs cyphesis cyrus daemontools dante dbadm dbskk ddclient \ + devicekit dhcp dictd dirmngr distcc djbdns dkim dmidecode dnsmasq docker \ + dovecot dphysswapfile dpkg drbd eg25manager entropyd evolution exim fail2ban \ + fakehwclock fapolicyd fcoe fetchmail finger firewalld firstboot fprintd ftp \ + games gatekeeper gdomap geoclue git gitosis glance glusterfs gnome gnomeclock \ + gpg gpm gpsd gssproxy guest hadoop haproxy hddtemp hostapd hwloc hypervkvp \ + i18n_input icecast ifplugd iiosensorproxy inetd inn iodine ipsec irc ircd \ + irqbalance iscsi isns jabber java kdump kerberos kerneloops keystone kismet \ + knot ksmtuned kubernetes l2tp ldap libmtp lightsquid likewise lircd livecd \ + lldpad loadkeys logadm logrotate logwatch lowmemorymonitor lpd lsm mailman \ + man2html mandb matrixd mcelog mediawiki memcached memlockd milter minidlna \ + minissdpd modemmanager mojomojo mon mongodb monit mono monop mozilla mpd \ + mplayer mrtg munin mysql nagios ncftool nessus netlabel networkmanager nis \ + node_exporter nsd nslcd ntop ntp numad nut nx obex obfs4proxy oddjob oident \ + openarc openca openct openhpi openoffice opensm openvpn openvswitch pacemaker pads \ + passenger pcscd pegasus perdition pingd pkcs pki plymouthd podman portmap \ + portreserve portslave postfix postfixpolicyd postgresql postgrey \ + powerprofiles ppp prelink prelude privoxy procmail psad publicfile \ + pulseaudio puppet pwauth pxe pyzor qemu qmail qpid quantum quota rabbitmq \ + radius radvd rasdaemon razor rdisc realmd redis remotelogin resmgr rhsmcertd \ + rkhunter rlogin rngd rootlesskit rpc rpcbind rpm rshd rssh rtkit rwho samba \ + samhain sanlock sasl sblim screen secadm sendmail sensord setroubleshoot \ + seunshare shibboleth shorewall shutdown sigrok slocate slpd slrnpull smartmon \ + smokeping smstools snmp snort sosreport soundserver spamassassin squid stubby \ + stunnel sudo svnserve switcheroo sxid sympa syncthing sysstat systemtap \ + tboot tcpd tcsd telepathy telnet tftp tgtd thunderbird thunderbolt timidity \ + tmpreaper tomcat tor tpm2 transproxy tripwire tuned tvtime tzdata ucspitcp \ + ulogd uml updfstab uptime usbguard usbmodules usbmuxd userhelper usernetctl \ + uucp uuidd uwimap varnishd vbetool vdagent vhostmd virt vlock vmware vnstatd \ + vpn watchdog wdmd webadm webalizer wine wireguard wireshark wm xen xfs \ + xguest xscreensaver xserver zabbix zarafa zebra zfs zosremote \ + > /dev/null info "RPM mode: Adding SELinux policy compatibility fixes" # Add policy name compatibility symlink. The Gentoo MCS policy is From eae8f06384e1bc351a359ee0bedd9874f86b66ea Mon Sep 17 00:00:00 2001 From: bfjelds Date: Tue, 14 Jul 2026 12:21:23 -0700 Subject: [PATCH 06/22] fix: address Copilot review feedback on A/B verity boot - grub_install.sh: define --fs_uuid/--verity_uuid so shflags accepts the args build_image_util passes in grub mode (was aborting the build). - rpm/grub_install.sh: fail the build when verity is enabled but a UUID file is missing/empty instead of emitting an invalid empty UUID= cmdline. - disk_util: validate blkid FS UUID output is non-empty before writing. - rpm/uki_install.sh: correct addon header comment (mount.usr lives in the main UKI cmdline, not the per-slot addon). - acl/docs/architecture.md: document the UKI PARTUUID addon boot flow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- acl/docs/architecture.md | 4 ++-- build_library/disk_util | 6 ++++-- build_library/grub_install.sh | 4 ++++ build_library/rpm/grub_install.sh | 8 ++++---- build_library/rpm/uki_install.sh | 3 ++- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/acl/docs/architecture.md b/acl/docs/architecture.md index 083b09d5cb4..24559c50cc4 100644 --- a/acl/docs/architecture.md +++ b/acl/docs/architecture.md @@ -54,8 +54,8 @@ ACL's primary boot path uses **systemd-boot** with **Unified Kernel Images (UKI) The `/usr` partition (USR-A) is a read-only btrfs filesystem with zstd compression. **dm-verity** provides block-level integrity verification: - The verity hash tree is stored in a dedicated hash partition (HASH-A, immediately following USR-A on disk). -- At boot, `systemd-veritysetup` activates the verity device using kernel command-line parameters embedded in the UKI: `systemd.verity_usr_data=UUID=`, `systemd.verity_usr_hash=UUID=`, and `systemd.verity_usr_options=panic-on-corruption`. -- The UKI uses filesystem/verity UUIDs (not partition UUIDs) so the same UKI works for both A and B slots. +- At boot, `systemd-veritysetup` activates the verity device using slot-specific parameters delivered via a per-slot systemd-stub addon: `systemd.verity_usr_data=PARTUUID=`, `systemd.verity_usr_hash=PARTUUID=`, and `systemd.verity_usr_options=panic-on-corruption`. +- The main UKI cmdline stays slot-independent (`mount.usr=/dev/mapper/usr`); Trident switches slots by swapping which addon is active in `.efi.extra.d/`, so the same signed UKI boots either A or B. (The secondary GRUB boot path instead builds the verity cmdline from filesystem/verity UUIDs, `UUID=` / `UUID=`.) - Any corruption of `/usr` causes an immediate kernel panic, preventing the system from running a tampered image. The A/B partition scheme (USR-A / USR-B) enables safe updates: the inactive slot is written, verified, and then atomically switched on reboot. diff --git a/build_library/disk_util b/build_library/disk_util index 2a0c3018ae0..2a1e13fa803 100755 --- a/build_library/disk_util +++ b/build_library/disk_util @@ -869,9 +869,11 @@ def Verity(options): # Read the btrfs FS UUID from the data partition for cmdline use. if options.fs_uuid: fs_uuid_out = SudoOutput( - ['blkid', '-s', 'UUID', '-o', 'value', data_loop]).decode('utf8') + ['blkid', '-s', 'UUID', '-o', 'value', data_loop]).decode('utf8').strip() + if not fs_uuid_out: + raise Exception("Failed to read btrfs FS UUID from %s" % data_loop) with open(options.fs_uuid, 'w') as f: - f.write(fs_uuid_out.strip() + '\n') + f.write(fs_uuid_out + '\n') else: # Legacy inline mode: hash tree appended within the same partition. diff --git a/build_library/grub_install.sh b/build_library/grub_install.sh index b1dc56ab3e5..9a303b2d893 100755 --- a/build_library/grub_install.sh +++ b/build_library/grub_install.sh @@ -28,6 +28,10 @@ DEFINE_string copy_efi_grub "" \ "Copy the EFI GRUB image to the specified path." DEFINE_string copy_shim "" \ "Copy the shim image to the specified path." +DEFINE_string fs_uuid "" \ + "Path to the file containing the btrfs filesystem UUID for /usr." +DEFINE_string verity_uuid "" \ + "Path to the file containing the dm-verity superblock UUID for /usr." # Parse flags FLAGS "$@" || exit 1 diff --git a/build_library/rpm/grub_install.sh b/build_library/rpm/grub_install.sh index 234629517be..ce05f0d2120 100644 --- a/build_library/rpm/grub_install.sh +++ b/build_library/rpm/grub_install.sh @@ -130,20 +130,20 @@ grub_provision_rpm() { if [[ -n "${FLAGS_fs_uuid}" && -f "${FLAGS_fs_uuid}" ]]; then local grub_fs_uuid grub_fs_uuid=$(cat "${FLAGS_fs_uuid}") + [[ -n "${grub_fs_uuid}" ]] || die "RPM mode: FS UUID file '${FLAGS_fs_uuid}' is empty" info "RPM mode: Injecting FS UUID ${grub_fs_uuid}" sed_cmds+=(-e "s/@@FSUUID@@/${grub_fs_uuid}/") else - warn "RPM mode: No FS UUID file provided" - sed_cmds+=(-e 's/@@FSUUID@@//') + die "RPM mode: verity enabled but no FS UUID file provided" fi if [[ -n "${FLAGS_verity_uuid}" && -f "${FLAGS_verity_uuid}" ]]; then local grub_verity_uuid grub_verity_uuid=$(cat "${FLAGS_verity_uuid}") + [[ -n "${grub_verity_uuid}" ]] || die "RPM mode: verity UUID file '${FLAGS_verity_uuid}' is empty" info "RPM mode: Injecting verity UUID ${grub_verity_uuid}" sed_cmds+=(-e "s/@@VERITYUUID@@/${grub_verity_uuid}/") else - warn "RPM mode: No verity UUID file provided" - sed_cmds+=(-e 's/@@VERITYUUID@@//') + die "RPM mode: verity enabled but no verity UUID file provided" fi else sed_cmds+=(-e 's/@@MOUNTUSR@@/mount.usr/') diff --git a/build_library/rpm/uki_install.sh b/build_library/rpm/uki_install.sh index b77da371cd8..3ce1878d6c1 100755 --- a/build_library/rpm/uki_install.sh +++ b/build_library/rpm/uki_install.sh @@ -457,12 +457,13 @@ _uki_build_debug_addon() { # # Each addon contains the slot-specific kernel cmdline args that tell systemd # which partitions to use for dm-verity: -# mount.usr=/dev/mapper/usr # systemd.verity_usr_data=PARTUUID= # systemd.verity_usr_hash=PARTUUID= # systemd.verity_usr_options=panic-on-corruption # usrhash= # +# mount.usr=/dev/mapper/usr is slot-independent and stays in the main UKI +# cmdline (not the addon). # At boot, systemd-stub appends the active addon's cmdline to the main UKI # cmdline. Trident swaps which addon is in .extra.d/ to switch slots. _uki_build_verity_addons() { From eb6df5642eb0fd0d81a3fc8d4a8d33362fc9487c Mon Sep 17 00:00:00 2001 From: bfjelds Date: Tue, 14 Jul 2026 12:46:06 -0700 Subject: [PATCH 07/22] fix: write FS UUID in both verity modes; unify verity paths Copilot re-review follow-ups on PR #28: - disk_util: the --fs_uuid output was only written in separate-hash-partition mode, so legacy inline-verity layouts produced no FS UUID file and now hard- fail in grub.cfg generation. Collapse the two near-identical verity branches into a single contextlib.ExitStack path that reads the FS UUID in both modes, removing the duplicated veritysetup/blkid logic. - rpm/uki_install.sh: correct the stale UUID-validation comment (grub_install.sh now dies rather than warns; uki_install does not pass UUIDs through itself). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build_library/disk_util | 53 ++++++++++++++++---------------- build_library/rpm/uki_install.sh | 14 ++++----- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/build_library/disk_util b/build_library/disk_util index 2a1e13fa803..684c6ae89ad 100755 --- a/build_library/disk_util +++ b/build_library/disk_util @@ -852,39 +852,38 @@ def Verity(options): ReadWriteSubvol(options, part, disable_rw=True) if hash_part_num: - # Separate hash partition mode: write hash tree to a dedicated partition. hash_part = partitions[hash_part_num] if not hash_part['image_compat']: raise InvalidLayout("Hash partition is incompatible with existing image") - - with PartitionLoop(options, part) as data_loop, \ - PartitionLoop(options, hash_part) as hash_loop: - verityout = SudoOutput(['veritysetup', 'format', '--hash=sha256', - '--data-block-size', part['fs_block_size'], - '--hash-block-size', part['fs_block_size'], - '--data-blocks', part['fs_blocks'], - data_loop, hash_loop]).decode('utf8') - print(verityout.strip()) - - # Read the btrfs FS UUID from the data partition for cmdline use. - if options.fs_uuid: - fs_uuid_out = SudoOutput( - ['blkid', '-s', 'UUID', '-o', 'value', data_loop]).decode('utf8').strip() - if not fs_uuid_out: - raise Exception("Failed to read btrfs FS UUID from %s" % data_loop) - with open(options.fs_uuid, 'w') as f: - f.write(fs_uuid_out + '\n') - else: - # Legacy inline mode: hash tree appended within the same partition. - with PartitionLoop(options, part) as loop_dev: - verityout = SudoOutput(['veritysetup', 'format', '--hash=sha256', + hash_part = None + + with contextlib.ExitStack() as loops: + data_loop = loops.enter_context(PartitionLoop(options, part)) + verity_cmd = ['veritysetup', 'format', '--hash=sha256', '--data-block-size', part['fs_block_size'], '--hash-block-size', part['fs_block_size'], - '--data-blocks', part['fs_blocks'], - '--hash-offset', part['fs_bytes'], - loop_dev, loop_dev]).decode('utf8') - print(verityout.strip()) + '--data-blocks', part['fs_blocks']] + if hash_part: + # Separate hash partition mode: write hash tree to a dedicated partition. + hash_loop = loops.enter_context(PartitionLoop(options, hash_part)) + else: + # Legacy inline mode: hash tree appended within the same partition. + hash_loop = data_loop + verity_cmd += ['--hash-offset', part['fs_bytes']] + verity_cmd += [data_loop, hash_loop] + + verityout = SudoOutput(verity_cmd).decode('utf8') + print(verityout.strip()) + + # Read the btrfs FS UUID from the data partition for cmdline use. + if options.fs_uuid: + fs_uuid_out = SudoOutput( + ['blkid', '-s', 'UUID', '-o', 'value', data_loop]).decode('utf8').strip() + if not fs_uuid_out: + raise Exception("Failed to read btrfs FS UUID from %s" % data_loop) + with open(options.fs_uuid, 'w') as f: + f.write(fs_uuid_out + '\n') m = re.search(r'Root hash:\s+([a-f0-9]{64})$', verityout, re.IGNORECASE|re.MULTILINE) if not m: diff --git a/build_library/rpm/uki_install.sh b/build_library/rpm/uki_install.sh index 3ce1878d6c1..9f62c6b3abe 100755 --- a/build_library/rpm/uki_install.sh +++ b/build_library/rpm/uki_install.sh @@ -138,13 +138,13 @@ OSREL fi info "UKI/RPM: Using ukify from $(command -v ukify)" - # NOTE: Filesystem and verity UUIDs (FLAGS_fs_uuid, FLAGS_verity_uuid) - # are not used by the UKI cmdline — slot identity is delivered via - # verity addons using PARTUUID. The UUID flags are retained because - # they are still passed through to grub_install.sh for grub.cfg. - # - # grub_install.sh only warns on missing UUIDs (substitutes empty values - # into grub.cfg), so validate here to fail fast. + # NOTE: The filesystem/verity UUIDs (FLAGS_fs_uuid, FLAGS_verity_uuid) + # are not used by the UKI cmdline — UKI slot identity is delivered via + # verity addons using PARTUUID. They are consumed only by the separate + # grub_install.sh entrypoint (GRUB boot path), which build_image_util + # invokes independently. Validate them here as a build-output sanity + # check so a missing UUID file fails fast during the UKI install rather + # than only surfacing later in a GRUB build. if [[ ${FLAGS_verity} -eq ${FLAGS_TRUE} ]]; then if [[ -z "${FLAGS_fs_uuid}" || ! -f "${FLAGS_fs_uuid}" ]]; then die "UKI/RPM: FLAGS_fs_uuid not set or file missing (required for grub.cfg)" From 40a84b458ee993d21cbe96b14438c31b360a139e Mon Sep 17 00:00:00 2001 From: bfjelds Date: Tue, 14 Jul 2026 13:13:17 -0700 Subject: [PATCH 08/22] fix: raise InvalidLayout for missing verity_hash partition reference When a partition's verity_hash cross-reference points to a partition number that is not present in the loaded layout, disk_util raised a bare KeyError. Validate the reference and raise InvalidLayout with a clear message instead, matching how other malformed-layout cases are reported. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build_library/disk_util | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build_library/disk_util b/build_library/disk_util index 684c6ae89ad..8b3c94dfde1 100755 --- a/build_library/disk_util +++ b/build_library/disk_util @@ -852,6 +852,10 @@ def Verity(options): ReadWriteSubvol(options, part, disable_rw=True) if hash_part_num: + if hash_part_num not in partitions: + raise InvalidLayout( + "verity_hash references partition %r which is not in the layout" + % hash_part_num) hash_part = partitions[hash_part_num] if not hash_part['image_compat']: raise InvalidLayout("Hash partition is incompatible with existing image") From f5d215262dc7fa970885d12009be7ac6aaf8233e Mon Sep 17 00:00:00 2001 From: bfjelds Date: Tue, 14 Jul 2026 13:22:12 -0700 Subject: [PATCH 09/22] fix: drop unused 'count' local in _lookup_partuuid Remove the unused 'count' variable from the local declaration in _lookup_partuuid to clear shellcheck SC2034 noise. No behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build_library/rpm/uki_install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_library/rpm/uki_install.sh b/build_library/rpm/uki_install.sh index 9f62c6b3abe..1c3b31dc2f3 100755 --- a/build_library/rpm/uki_install.sh +++ b/build_library/rpm/uki_install.sh @@ -495,7 +495,7 @@ _uki_build_verity_addons() { # Helper: look up a partition UUID by label, dying on missing or duplicate _lookup_partuuid() { local label="$1" - local matches count uuid + local matches uuid matches=$(jq -r "[.layouts.base[] | select(.label == \"${label}\")] | length" "${disk_layout_file}") if [[ "${matches}" -eq 0 ]]; then die "UKI/RPM: No partition with label '${label}' in disk layout" From 78d965ce058d677c65030b7a860c35e85c700bc7 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Tue, 14 Jul 2026 20:44:12 +0000 Subject: [PATCH 10/22] fix: normalize verity_hash cross-reference to str before lookup partitions is keyed by string partition numbers (JSON object keys), and GetPartitionByNumber() coerces with str(num) for the same reason. Normalize hash_part_num to str right after reading it so a layout that specifies verity_hash as a JSON number (e.g. 3 instead of "3") still resolves against the partitions dict instead of falsely raising InvalidLayout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build_library/disk_util | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build_library/disk_util b/build_library/disk_util index 8b3c94dfde1..ccc0e58bb93 100755 --- a/build_library/disk_util +++ b/build_library/disk_util @@ -833,6 +833,10 @@ def Verity(options): for part_num, part in partitions.items(): hash_part_num = part.get('verity_hash') + if hash_part_num is not None: + # partitions is keyed by string partition numbers (JSON object keys); + # normalize so a numeric verity_hash value still resolves correctly. + hash_part_num = str(hash_part_num) has_verity_feature = 'verity' in part.get('features', []) if not hash_part_num and not has_verity_feature: From b5ce937269cd46e5fd35a0a4651a3b2e01ecc163 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Sun, 19 Jul 2026 16:10:35 +0000 Subject: [PATCH 11/22] Increase UKI ESP size from 128 MiB to 256 MiB The ARM64 UKI is ~82 MiB (vs ~49 MiB on AMD64). A/B updates stage the new UKI on the ESP alongside the running one, requiring room for two. The 128 MiB ESP (262144 blocks) overflows on ARM64 (No space left on device, os error 28, copying UKI to ESP). Double to 256 MiB (524288 blocks) to fit two ~82 MiB UKIs plus bootloaders with headroom. Stays 4096-block aligned. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 72f4b84a-8f5c-4df6-9f8e-fb4850c520e2 --- build_library/disk_layout_uki.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_library/disk_layout_uki.json b/build_library/disk_layout_uki.json index c964f7af8f6..c387a7a6f4c 100644 --- a/build_library/disk_layout_uki.json +++ b/build_library/disk_layout_uki.json @@ -13,7 +13,7 @@ "label": "EFI-SYSTEM", "fs_label": "EFI-SYSTEM", "type": "efi", - "blocks": "262144", + "blocks": "524288", "fs_type": "vfat", "mount": "/boot", "features": [] From 1e1728f30ddb44a8510f9f9d4944a77bf95fbc30 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Mon, 20 Jul 2026 16:48:30 +0000 Subject: [PATCH 12/22] fix: address PR #28 review comments disk_util: normalize root-hash/verity-uuid block indentation and use `is not None` for root_hash. uki_install.sh: validate UUID file content (non-empty), not just existence, so a blank UUID file fails fast. grub_install.sh: hard-fail when verity is enabled but no hash file is provided, matching the sibling FS/verity UUID checks (a warn + empty substitution produces an unbootable grub.cfg). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 72f4b84a-8f5c-4df6-9f8e-fb4850c520e2 --- build_library/disk_util | 16 ++++++++-------- build_library/rpm/grub_install.sh | 3 +-- build_library/rpm/uki_install.sh | 5 +++++ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/build_library/disk_util b/build_library/disk_util index ccc0e58bb93..2bef44ac12e 100755 --- a/build_library/disk_util +++ b/build_library/disk_util @@ -895,21 +895,21 @@ def Verity(options): m = re.search(r'Root hash:\s+([a-f0-9]{64})$', verityout, re.IGNORECASE|re.MULTILINE) if not m: - raise Exception("Failed to parse verity root hash!") + raise Exception("Failed to parse verity root hash!") - if options.root_hash != None: - with open(options.root_hash, "w") as hash_file: - hash_file.write(m.group(1)) - hash_file.write("\n") + if options.root_hash is not None: + with open(options.root_hash, "w") as hash_file: + hash_file.write(m.group(1)) + hash_file.write("\n") # Extract the verity superblock UUID from veritysetup output. if options.verity_uuid: m_uuid = re.search(r'UUID:\s+([a-f0-9-]{36})$', verityout, re.IGNORECASE|re.MULTILINE) if not m_uuid: - raise Exception("Failed to parse verity UUID!") + raise Exception("Failed to parse verity UUID!") with open(options.verity_uuid, 'w') as f: - f.write(m_uuid.group(1)) - f.write("\n") + f.write(m_uuid.group(1)) + f.write("\n") def Extract(options): diff --git a/build_library/rpm/grub_install.sh b/build_library/rpm/grub_install.sh index ce05f0d2120..bae93925709 100644 --- a/build_library/rpm/grub_install.sh +++ b/build_library/rpm/grub_install.sh @@ -122,8 +122,7 @@ grub_provision_rpm() { info "RPM mode: Injecting verity hash ${usr_hash}" sed_cmds+=(-e "s/@@USRHASH@@/${usr_hash}/") else - warn "RPM mode: Verity enabled but no hash file provided" - sed_cmds+=(-e 's/@@USRHASH@@//') + die "RPM mode: Verity enabled but no hash file provided" fi # Inject filesystem and verity UUIDs for A/B agnostic boot diff --git a/build_library/rpm/uki_install.sh b/build_library/rpm/uki_install.sh index 1c3b31dc2f3..de5b165a40a 100755 --- a/build_library/rpm/uki_install.sh +++ b/build_library/rpm/uki_install.sh @@ -146,12 +146,17 @@ OSREL # check so a missing UUID file fails fast during the UKI install rather # than only surfacing later in a GRUB build. if [[ ${FLAGS_verity} -eq ${FLAGS_TRUE} ]]; then + local fs_uuid_content verity_uuid_content if [[ -z "${FLAGS_fs_uuid}" || ! -f "${FLAGS_fs_uuid}" ]]; then die "UKI/RPM: FLAGS_fs_uuid not set or file missing (required for grub.cfg)" fi + fs_uuid_content=$(tr -d '[:space:]' < "${FLAGS_fs_uuid}") + [[ -n "${fs_uuid_content}" ]] || die "UKI/RPM: FS UUID file '${FLAGS_fs_uuid}' is empty" if [[ -z "${FLAGS_verity_uuid}" || ! -f "${FLAGS_verity_uuid}" ]]; then die "UKI/RPM: FLAGS_verity_uuid not set or file missing (required for grub.cfg)" fi + verity_uuid_content=$(tr -d '[:space:]' < "${FLAGS_verity_uuid}") + [[ -n "${verity_uuid_content}" ]] || die "UKI/RPM: verity UUID file '${FLAGS_verity_uuid}' is empty" fi local cmdline="" From 52df44f9612671d98e5cece0f095feb2557df3c0 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Mon, 20 Jul 2026 16:53:51 +0000 Subject: [PATCH 13/22] Revert grub verity-hash die back to pre-existing warn The "verity enabled but no hash file" else-branch (warn + empty @@USRHASH@@ substitution) predates this PR and is a working fallback. Changing it to a hard die altered pre-existing behavior that was not introduced by this change, so restore the original warn. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 72f4b84a-8f5c-4df6-9f8e-fb4850c520e2 --- build_library/rpm/grub_install.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build_library/rpm/grub_install.sh b/build_library/rpm/grub_install.sh index bae93925709..ce05f0d2120 100644 --- a/build_library/rpm/grub_install.sh +++ b/build_library/rpm/grub_install.sh @@ -122,7 +122,8 @@ grub_provision_rpm() { info "RPM mode: Injecting verity hash ${usr_hash}" sed_cmds+=(-e "s/@@USRHASH@@/${usr_hash}/") else - die "RPM mode: Verity enabled but no hash file provided" + warn "RPM mode: Verity enabled but no hash file provided" + sed_cmds+=(-e 's/@@USRHASH@@//') fi # Inject filesystem and verity UUIDs for A/B agnostic boot From 7a834761bd6eede9ed8c45fa307dd5aadb1920ca Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Mon, 20 Jul 2026 17:10:54 +0000 Subject: [PATCH 14/22] fix: drop jq -e in _lookup_partuuid so explicit null check runs uki_install.sh runs under switch_to_strict_mode (set -e + ERR trap). With jq -e, a missing/null .uuid makes jq exit 1, tripping the ERR trap with a generic error before the explicit [[ -z || == null ]] check can emit the clear "Partition has no uuid field" die message. Dropping -e lets jq emit null (exit 0) so the explicit check produces the intended message. Also matches the sibling matches=jq-r lookup which omits -e. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 72f4b84a-8f5c-4df6-9f8e-fb4850c520e2 --- build_library/rpm/uki_install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_library/rpm/uki_install.sh b/build_library/rpm/uki_install.sh index de5b165a40a..8f54561ace8 100755 --- a/build_library/rpm/uki_install.sh +++ b/build_library/rpm/uki_install.sh @@ -508,7 +508,7 @@ _uki_build_verity_addons() { if [[ "${matches}" -ne 1 ]]; then die "UKI/RPM: Expected exactly 1 '${label}' partition, found ${matches}" fi - uuid=$(jq -e -r "[.layouts.base[] | select(.label == \"${label}\")] | .[0].uuid" "${disk_layout_file}") + uuid=$(jq -r "[.layouts.base[] | select(.label == \"${label}\")] | .[0].uuid" "${disk_layout_file}") if [[ -z "${uuid}" || "${uuid}" == "null" ]]; then die "UKI/RPM: Partition '${label}' has no uuid field" fi From 57cb18f5d86e25c4d794eb102663f3e4c4bc4c2a Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Tue, 21 Jul 2026 23:43:20 +0000 Subject: [PATCH 15/22] uki: add arm64 test-image device-timeout addon Install a UKI addon setting systemd.default_device_timeout_sec=120 on the arm64 kola test image only (gated on ARCH=arm64 and INJECT_DOCKER_SYSEXT=true). Under kola's parallel QEMU-TCG emulation on aarch64, CPU contention can keep udev from initialising the ESP/OEM/usr-verity devices within the default initrd device timeout, dropping the VM to an emergency shell. Raising the timeout via a test-image ESP addon fixes this. Verified against a parallel=24 repro: 18/18 boot-to-login vs 18/18 emergency shells at baseline. Scoped to the test image, so production and amd64 boot behaviour is unchanged; the failure is a CI-only emulation artefact that never occurs on real hardware. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 72f4b84a-8f5c-4df6-9f8e-fb4850c520e2 --- build_library/vm_image_util.sh | 84 ++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/build_library/vm_image_util.sh b/build_library/vm_image_util.sh index 3b781c82b89..29ed38e05f1 100644 --- a/build_library/vm_image_util.sh +++ b/build_library/vm_image_util.sh @@ -555,6 +555,7 @@ install_oem_package() { "${oem_files_dir}" if [[ "${BOOTLOADER_MODE}" == "uki" ]]; then install_uki_oem_addon + install_uki_timeout_addon fi return 0 fi @@ -600,6 +601,7 @@ install_oem_package() { # UKI addon (replacing the role of grub.cfg). if [[ "${BOOTLOADER_MODE}" == "uki" ]]; then install_uki_oem_addon + install_uki_timeout_addon fi } @@ -756,6 +758,88 @@ install_uki_oem_addon() { "${oem_files_dir}" } +# Build and install a UKI addon that raises the systemd device-init timeout, +# scoped to the arm64 kola *test* image only (INJECT_DOCKER_SYSEXT=true). +# +# Under kola's parallel QEMU-TCG emulation on aarch64, heavy CPU contention can +# prevent udev from initialising the ESP/OEM/usr-verity devices within the +# default initrd device timeout, dropping the VM to an emergency shell. Baking +# systemd.default_device_timeout_sec=120 into a UKI addon on the test image's +# ESP fixes this without changing production or amd64 boot behaviour. +# +# Deliberately NOT applied to the production VM image (INJECT_DOCKER_SYSEXT=false) +# or to amd64: the failure is a CI-only TCG-emulation artefact that never occurs +# on real hardware. +install_uki_timeout_addon() { + if [[ "${BOOTLOADER_MODE}" != "uki" ]]; then + return 0 + fi + + # Scope: arm64 test image only. + if [[ "${ARCH}" != "arm64" ]] || [[ "${INJECT_DOCKER_SYSEXT:-false}" != "true" ]]; then + return 0 + fi + + local esp_dir="${VM_TMP_ROOT}/boot" + if [[ ! -d "${esp_dir}/EFI/Linux" ]]; then + warn "UKI timeout addon: ESP directory ${esp_dir}/EFI/Linux not found; skipping" + return 0 + fi + + # EFI architecture suffix for the systemd-boot stub. + local efi_arch + case "${ARCH}" in + amd64) efi_arch="x64" ;; + arm64) efi_arch="aa64" ;; + *) warn "UKI timeout addon: unsupported arch ${ARCH}; skipping"; return 0 ;; + esac + + local efi_stub="${VM_TMP_ROOT}/usr/lib/systemd/boot/efi/linux${efi_arch}.efi.stub" + if [[ ! -f "${efi_stub}" ]]; then + warn "UKI timeout addon: EFI stub not found at ${efi_stub}; skipping" + return 0 + fi + + # Detect the main UKI (vmlinuz-.efi) to locate its .extra.d dir. + local uki_name + uki_name=$(find "${esp_dir}/EFI/Linux/" -maxdepth 1 -name 'vmlinuz-*.efi' -printf '%f\n' | sort -V | head -n 1) + if [[ -z "${uki_name}" ]]; then + warn "UKI timeout addon: no UKI (vmlinuz-*.efi) found in ESP; skipping" + return 0 + fi + + if ! command -v ukify &>/dev/null; then + die "UKI timeout addon: ukify not found on PATH" + fi + + info "UKI timeout addon: Building for arm64 test image (${VM_IMG_TYPE})" + + local addon_dir="${esp_dir}/EFI/Linux/${uki_name}.extra.d" + sudo mkdir -p "${addon_dir}" + + local timeout_temp_dir + timeout_temp_dir=$(mktemp -d) + + printf '%s\n' "systemd.default_device_timeout_sec=120" \ + > "${timeout_temp_dir}/timeout-cmdline.txt" + + sudo ukify build \ + --cmdline=@"${timeout_temp_dir}/timeout-cmdline.txt" \ + --stub="${efi_stub}" \ + --output="${timeout_temp_dir}/timeout.addon.efi" + + if [[ ! -f "${timeout_temp_dir}/timeout.addon.efi" ]]; then + rm -rf "${timeout_temp_dir}" + die "UKI timeout addon: ukify failed to produce timeout.addon.efi" + fi + + sudo cp "${timeout_temp_dir}/timeout.addon.efi" "${addon_dir}/timeout.addon.efi" + info "UKI timeout addon: Installed -> EFI/Linux/${uki_name}.extra.d/timeout.addon.efi" + info "UKI timeout addon: cmdline = systemd.default_device_timeout_sec=120" + + rm -rf "${timeout_temp_dir}" +} + # Any other tweaks required? run_fs_hook() { local fs_hook=$(_get_vm_opt FS_HOOK) From 960810d3fd6e3eb6a933b4396bb709dc1a4953f6 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Wed, 22 Jul 2026 18:53:14 +0000 Subject: [PATCH 16/22] Address review: harden UKI timeout addon + tridentd.socket enablement - install_uki_timeout_addon: require exactly one UKI on the ESP (die on multiple) instead of silently picking one via sort -V | head, so the addon can never land in the wrong .efi.extra.d. Mirrors the single-UKI rule in rpm/uki_install.sh. - RPM-mode tridentd.socket enable: hard-fail if tridentd.socket is absent. trident is required for ACL images, so a missing unit is a broken image; fail the build rather than leave a dangling wants-symlink. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 72f4b84a-8f5c-4df6-9f8e-fb4850c520e2 --- build_library/rpm/build_image_util.sh | 9 ++++++++- build_library/vm_image_util.sh | 14 +++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/build_library/rpm/build_image_util.sh b/build_library/rpm/build_image_util.sh index 0b5646d58f1..bb191ce6640 100644 --- a/build_library/rpm/build_image_util.sh +++ b/build_library/rpm/build_image_util.sh @@ -592,7 +592,14 @@ EOF sudo sed -i 's|/var/run/|/run/|g' "${root_fs_dir}/usr/lib/systemd/system/rpcbind.socket" fi - # Enable tridentd.socket - listens for trident API requests + # Enable tridentd.socket - listens for trident API requests. + # trident is a required component of ACL images: a missing unit means the + # trident RPM was not installed, i.e. a broken image. Fail the build now + # rather than leave a dangling wants-symlink and ship an image whose + # provisioning socket never activates. + if [[ ! -f "${root_fs_dir}/usr/lib/systemd/system/tridentd.socket" ]]; then + die "tridentd.socket not found in image - trident RPM missing (trident is required for ACL)" + fi info "RPM mode: Enabling tridentd.socket" sudo mkdir -p "${root_fs_dir}/usr/lib/systemd/system/sockets.target.wants" sudo ln -sf ../tridentd.socket "${root_fs_dir}/usr/lib/systemd/system/sockets.target.wants/tridentd.socket" diff --git a/build_library/vm_image_util.sh b/build_library/vm_image_util.sh index 29ed38e05f1..f6f57d31aa3 100644 --- a/build_library/vm_image_util.sh +++ b/build_library/vm_image_util.sh @@ -801,12 +801,20 @@ install_uki_timeout_addon() { fi # Detect the main UKI (vmlinuz-.efi) to locate its .extra.d dir. - local uki_name - uki_name=$(find "${esp_dir}/EFI/Linux/" -maxdepth 1 -name 'vmlinuz-*.efi' -printf '%f\n' | sort -V | head -n 1) - if [[ -z "${uki_name}" ]]; then + # ACL test images ship exactly one UKI; require that here so the addon is + # never silently installed into the wrong .efi.extra.d (which would + # leave the timeout ineffective and resurrect the emergency-shell flake + # this addon exists to fix). Mirrors rpm/uki_install.sh's single-UKI rule. + local -a uki_matches + mapfile -t uki_matches < <(find "${esp_dir}/EFI/Linux/" -maxdepth 1 -name 'vmlinuz-*.efi' -printf '%f\n' | sort -V) + if [[ "${#uki_matches[@]}" -eq 0 ]]; then warn "UKI timeout addon: no UKI (vmlinuz-*.efi) found in ESP; skipping" return 0 fi + if [[ "${#uki_matches[@]}" -gt 1 ]]; then + die "UKI timeout addon: expected exactly one UKI in ${esp_dir}/EFI/Linux/, found ${#uki_matches[@]}: ${uki_matches[*]}" + fi + local uki_name="${uki_matches[0]}" if ! command -v ukify &>/dev/null; then die "UKI timeout addon: ukify not found on PATH" From e08d31dfea8f7288df70618d76eb06b9790b129e Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Wed, 22 Jul 2026 19:15:52 +0000 Subject: [PATCH 17/22] Address review: fail fast on empty verity_hash in disk_util Verity A present-but-empty verity_hash (e.g. "verity_hash": "") previously fell through to legacy inline-verity mode instead of separate-hash-partition mode, producing an invalid layout that is hard to debug. Strip and raise InvalidLayout when the value is empty, consistent with the other malformed- layout guards in this function. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 72f4b84a-8f5c-4df6-9f8e-fb4850c520e2 --- build_library/disk_util | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/build_library/disk_util b/build_library/disk_util index 2bef44ac12e..4d026bdfaaa 100755 --- a/build_library/disk_util +++ b/build_library/disk_util @@ -836,7 +836,14 @@ def Verity(options): if hash_part_num is not None: # partitions is keyed by string partition numbers (JSON object keys); # normalize so a numeric verity_hash value still resolves correctly. - hash_part_num = str(hash_part_num) + hash_part_num = str(hash_part_num).strip() + # A present-but-empty verity_hash is a malformed layout, not a request + # for legacy inline mode: silently falling back would produce an + # invalid verity layout that is hard to debug. Fail fast instead. + if not hash_part_num: + raise InvalidLayout( + "partition %r has an empty verity_hash; name a hash partition " + "number or omit the key" % part_num) has_verity_feature = 'verity' in part.get('features', []) if not hash_part_num and not has_verity_feature: From e41f2960a22cbbf5d68afb118246f32c2e83f9bd Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Wed, 22 Jul 2026 19:30:47 +0000 Subject: [PATCH 18/22] Address review: guard ukify build under strict mode; genericize FS UUID wording Under switch_to_strict_mode (set -e), a non-zero exit from `sudo ukify build` aborted the script before the following existence check could emit the intended die message or clean up the temp dir. Wrap every ukify invocation (main UKI + firstboot/fips/kdump/debug/verity addons in uki_install.sh and the timeout addon in vm_image_util.sh) in an explicit `if ! ...; then rm -rf ; die; fi`, and clean up the temp dir on the produce-check path too. Failures now surface with a clear message and no leaked temp dirs. Also reword disk_util Verity's fs_uuid comment/exception from "btrfs FS UUID" to "filesystem UUID": the blkid read is filesystem-agnostic. (ACL only verities btrfs today, so this is wording-only.) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 72f4b84a-8f5c-4df6-9f8e-fb4850c520e2 --- build_library/disk_util | 4 +-- build_library/rpm/uki_install.sh | 48 ++++++++++++++++++++++++-------- build_library/vm_image_util.sh | 7 +++-- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/build_library/disk_util b/build_library/disk_util index 4d026bdfaaa..8617e7c3b94 100755 --- a/build_library/disk_util +++ b/build_library/disk_util @@ -891,12 +891,12 @@ def Verity(options): verityout = SudoOutput(verity_cmd).decode('utf8') print(verityout.strip()) - # Read the btrfs FS UUID from the data partition for cmdline use. + # Read the filesystem UUID from the data partition for cmdline use. if options.fs_uuid: fs_uuid_out = SudoOutput( ['blkid', '-s', 'UUID', '-o', 'value', data_loop]).decode('utf8').strip() if not fs_uuid_out: - raise Exception("Failed to read btrfs FS UUID from %s" % data_loop) + raise Exception("Failed to read filesystem UUID from %s" % data_loop) with open(options.fs_uuid, 'w') as f: f.write(fs_uuid_out + '\n') diff --git a/build_library/rpm/uki_install.sh b/build_library/rpm/uki_install.sh index 8f54561ace8..c8a7ec40d75 100755 --- a/build_library/rpm/uki_install.sh +++ b/build_library/rpm/uki_install.sh @@ -226,15 +226,19 @@ OSREL local uki_output="${uki_temp_dir}/${uki_name}" info "UKI/RPM: Building UKI with ukify" - sudo ukify build \ + if ! sudo ukify build \ --stub="${efi_stub}" \ --linux="${kernel}" \ --initrd="${initrd}" \ --cmdline=@"${uki_temp_dir}/cmdline.txt" \ --os-release=@"${osrelease}" \ - --output="${uki_output}" + --output="${uki_output}"; then + rm -rf "${uki_temp_dir}" + die "UKI/RPM: ukify build failed for ${uki_output}" + fi if [[ ! -f "${uki_output}" ]]; then + rm -rf "${uki_temp_dir}" die "UKI/RPM: ukify failed to produce ${uki_output}" fi @@ -321,12 +325,16 @@ _uki_build_firstboot_addon() { # efi_stub existence was already verified by the caller. local efi_stub="${BOARD_ROOT}/usr/lib/systemd/boot/efi/linux${EFI_ARCH}.efi.stub" - sudo ukify build \ + if ! sudo ukify build \ --cmdline=@"${fb_temp_dir}/firstboot-cmdline.txt" \ --stub="${efi_stub}" \ - --output="${fb_temp_dir}/firstboot.addon.efi" + --output="${fb_temp_dir}/firstboot.addon.efi"; then + rm -rf "${fb_temp_dir}" + die "UKI/RPM: ukify build failed for firstboot addon" + fi if [[ ! -f "${fb_temp_dir}/firstboot.addon.efi" ]]; then + rm -rf "${fb_temp_dir}" die "UKI/RPM: ukify failed to produce firstboot.addon.efi" fi @@ -362,12 +370,16 @@ _uki_build_fips_addon() { local efi_stub="${BOARD_ROOT}/usr/lib/systemd/boot/efi/linux${EFI_ARCH}.efi.stub" - sudo ukify build \ + if ! sudo ukify build \ --cmdline=@"${fips_temp_dir}/fips-cmdline.txt" \ --stub="${efi_stub}" \ - --output="${fips_temp_dir}/fips.addon.efi" + --output="${fips_temp_dir}/fips.addon.efi"; then + rm -rf "${fips_temp_dir}" + die "UKI/RPM: ukify build failed for fips addon" + fi if [[ ! -f "${fips_temp_dir}/fips.addon.efi" ]]; then + rm -rf "${fips_temp_dir}" die "UKI/RPM: ukify failed to produce fips.addon.efi" fi @@ -399,12 +411,16 @@ _uki_build_kdump_addon() { local efi_stub="${BOARD_ROOT}/usr/lib/systemd/boot/efi/linux${EFI_ARCH}.efi.stub" - sudo ukify build \ + if ! sudo ukify build \ --cmdline=@"${kdump_temp_dir}/kdump-cmdline.txt" \ --stub="${efi_stub}" \ - --output="${kdump_temp_dir}/kdump.addon.efi" + --output="${kdump_temp_dir}/kdump.addon.efi"; then + rm -rf "${kdump_temp_dir}" + die "UKI/RPM: ukify build failed for kdump addon" + fi if [[ ! -f "${kdump_temp_dir}/kdump.addon.efi" ]]; then + rm -rf "${kdump_temp_dir}" die "UKI/RPM: ukify failed to produce kdump.addon.efi" fi @@ -442,12 +458,16 @@ _uki_build_debug_addon() { local efi_stub="${BOARD_ROOT}/usr/lib/systemd/boot/efi/linux${EFI_ARCH}.efi.stub" - sudo ukify build \ + if ! sudo ukify build \ --cmdline=@"${debug_temp_dir}/debug-cmdline.txt" \ --stub="${efi_stub}" \ - --output="${debug_temp_dir}/debug.addon.efi" + --output="${debug_temp_dir}/debug.addon.efi"; then + rm -rf "${debug_temp_dir}" + die "UKI/RPM: ukify build failed for debug addon" + fi if [[ ! -f "${debug_temp_dir}/debug.addon.efi" ]]; then + rm -rf "${debug_temp_dir}" die "UKI/RPM: ukify failed to produce debug.addon.efi" fi @@ -557,12 +577,16 @@ _uki_build_verity_addons() { echo "${cmdline}" > "${verity_temp_dir}/verity-${slot}-cmdline.txt" addon_file="${verity_temp_dir}/verity-${slot}.addon.efi" - sudo ukify build \ + if ! sudo ukify build \ --cmdline=@"${verity_temp_dir}/verity-${slot}-cmdline.txt" \ --stub="${efi_stub}" \ - --output="${addon_file}" + --output="${addon_file}"; then + rm -rf "${verity_temp_dir}" + die "UKI/RPM: ukify build failed for verity-${slot}.addon.efi" + fi if [[ ! -f "${addon_file}" ]]; then + rm -rf "${verity_temp_dir}" die "UKI/RPM: ukify failed to produce verity-${slot}.addon.efi" fi diff --git a/build_library/vm_image_util.sh b/build_library/vm_image_util.sh index f6f57d31aa3..44747599531 100644 --- a/build_library/vm_image_util.sh +++ b/build_library/vm_image_util.sh @@ -831,10 +831,13 @@ install_uki_timeout_addon() { printf '%s\n' "systemd.default_device_timeout_sec=120" \ > "${timeout_temp_dir}/timeout-cmdline.txt" - sudo ukify build \ + if ! sudo ukify build \ --cmdline=@"${timeout_temp_dir}/timeout-cmdline.txt" \ --stub="${efi_stub}" \ - --output="${timeout_temp_dir}/timeout.addon.efi" + --output="${timeout_temp_dir}/timeout.addon.efi"; then + rm -rf "${timeout_temp_dir}" + die "UKI timeout addon: ukify build failed" + fi if [[ ! -f "${timeout_temp_dir}/timeout.addon.efi" ]]; then rm -rf "${timeout_temp_dir}" From 5107846e2ab0f14587bf9c6b43664241e6d59e06 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Wed, 22 Jul 2026 19:54:13 +0000 Subject: [PATCH 19/22] Address review: use sudo rm -rf for ukify temp-dir cleanup sudo ukify build can leave root-owned artifacts in the mktemp temp dirs, so a plain `rm -rf` could fail and (under strict mode) abort before the intended die. Use `sudo rm -rf` for every temp-dir cleanup in the UKI/addon builders (uki_install.sh main UKI + firstboot/fips/kdump/debug/verity addons, and the timeout addon in vm_image_util.sh), matching the existing sudo rm -rf convention used throughout build_library. Each temp dir is assigned via mktemp -d immediately before use, so the rm target is always a real path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 72f4b84a-8f5c-4df6-9f8e-fb4850c520e2 --- build_library/rpm/uki_install.sh | 36 ++++++++++++++++---------------- build_library/vm_image_util.sh | 6 +++--- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/build_library/rpm/uki_install.sh b/build_library/rpm/uki_install.sh index c8a7ec40d75..24f92f255af 100755 --- a/build_library/rpm/uki_install.sh +++ b/build_library/rpm/uki_install.sh @@ -233,12 +233,12 @@ OSREL --cmdline=@"${uki_temp_dir}/cmdline.txt" \ --os-release=@"${osrelease}" \ --output="${uki_output}"; then - rm -rf "${uki_temp_dir}" + sudo rm -rf "${uki_temp_dir}" die "UKI/RPM: ukify build failed for ${uki_output}" fi if [[ ! -f "${uki_output}" ]]; then - rm -rf "${uki_temp_dir}" + sudo rm -rf "${uki_temp_dir}" die "UKI/RPM: ukify failed to produce ${uki_output}" fi @@ -302,7 +302,7 @@ OSREL fi # Clean up - rm -rf "${uki_temp_dir}" + sudo rm -rf "${uki_temp_dir}" } # Build a self-removing firstboot addon that triggers Ignition on first boot. @@ -329,12 +329,12 @@ _uki_build_firstboot_addon() { --cmdline=@"${fb_temp_dir}/firstboot-cmdline.txt" \ --stub="${efi_stub}" \ --output="${fb_temp_dir}/firstboot.addon.efi"; then - rm -rf "${fb_temp_dir}" + sudo rm -rf "${fb_temp_dir}" die "UKI/RPM: ukify build failed for firstboot addon" fi if [[ ! -f "${fb_temp_dir}/firstboot.addon.efi" ]]; then - rm -rf "${fb_temp_dir}" + sudo rm -rf "${fb_temp_dir}" die "UKI/RPM: ukify failed to produce firstboot.addon.efi" fi @@ -349,7 +349,7 @@ _uki_build_firstboot_addon() { sudo cp "${fb_temp_dir}/firstboot.addon.efi" "${template_dir}/firstboot.addon.efi" info "UKI/RPM: Saved firstboot addon template → acl/uki-addons/firstboot.addon.efi" - rm -rf "${fb_temp_dir}" + sudo rm -rf "${fb_temp_dir}" } # Build a reusable FIPS addon template for UKI systems. @@ -374,19 +374,19 @@ _uki_build_fips_addon() { --cmdline=@"${fips_temp_dir}/fips-cmdline.txt" \ --stub="${efi_stub}" \ --output="${fips_temp_dir}/fips.addon.efi"; then - rm -rf "${fips_temp_dir}" + sudo rm -rf "${fips_temp_dir}" die "UKI/RPM: ukify build failed for fips addon" fi if [[ ! -f "${fips_temp_dir}/fips.addon.efi" ]]; then - rm -rf "${fips_temp_dir}" + sudo rm -rf "${fips_temp_dir}" die "UKI/RPM: ukify failed to produce fips.addon.efi" fi sudo cp "${fips_temp_dir}/fips.addon.efi" "${template_dir}/fips.addon.efi" info "UKI/RPM: Saved FIPS addon template -> acl/uki-addons/fips.addon.efi" - rm -rf "${fips_temp_dir}" + sudo rm -rf "${fips_temp_dir}" } # Build a reusable kdump addon template for UKI systems. @@ -415,12 +415,12 @@ _uki_build_kdump_addon() { --cmdline=@"${kdump_temp_dir}/kdump-cmdline.txt" \ --stub="${efi_stub}" \ --output="${kdump_temp_dir}/kdump.addon.efi"; then - rm -rf "${kdump_temp_dir}" + sudo rm -rf "${kdump_temp_dir}" die "UKI/RPM: ukify build failed for kdump addon" fi if [[ ! -f "${kdump_temp_dir}/kdump.addon.efi" ]]; then - rm -rf "${kdump_temp_dir}" + sudo rm -rf "${kdump_temp_dir}" die "UKI/RPM: ukify failed to produce kdump.addon.efi" fi @@ -428,7 +428,7 @@ _uki_build_kdump_addon() { info "UKI/RPM: Saved kdump addon template -> acl/uki-addons/kdump.addon.efi" info "UKI/RPM: To enable kdump, copy to EFI/Linux/vmlinuz-.efi.extra.d/kdump.addon.efi" - rm -rf "${kdump_temp_dir}" + sudo rm -rf "${kdump_temp_dir}" } # Build an optional debug addon that appends extra kernel cmdline args. @@ -462,12 +462,12 @@ _uki_build_debug_addon() { --cmdline=@"${debug_temp_dir}/debug-cmdline.txt" \ --stub="${efi_stub}" \ --output="${debug_temp_dir}/debug.addon.efi"; then - rm -rf "${debug_temp_dir}" + sudo rm -rf "${debug_temp_dir}" die "UKI/RPM: ukify build failed for debug addon" fi if [[ ! -f "${debug_temp_dir}/debug.addon.efi" ]]; then - rm -rf "${debug_temp_dir}" + sudo rm -rf "${debug_temp_dir}" die "UKI/RPM: ukify failed to produce debug.addon.efi" fi @@ -475,7 +475,7 @@ _uki_build_debug_addon() { info "UKI/RPM: Installed debug addon → EFI/Linux/${uki_name}.extra.d/debug.addon.efi" info "UKI/RPM: debug cmdline = ${extra_cmdline}" - rm -rf "${debug_temp_dir}" + sudo rm -rf "${debug_temp_dir}" } # Build verity slot addons for A/B partition switching. @@ -581,12 +581,12 @@ _uki_build_verity_addons() { --cmdline=@"${verity_temp_dir}/verity-${slot}-cmdline.txt" \ --stub="${efi_stub}" \ --output="${addon_file}"; then - rm -rf "${verity_temp_dir}" + sudo rm -rf "${verity_temp_dir}" die "UKI/RPM: ukify build failed for verity-${slot}.addon.efi" fi if [[ ! -f "${addon_file}" ]]; then - rm -rf "${verity_temp_dir}" + sudo rm -rf "${verity_temp_dir}" die "UKI/RPM: ukify failed to produce verity-${slot}.addon.efi" fi @@ -606,7 +606,7 @@ _uki_build_verity_addons() { # when switching A/B slots at OS update time. The templates in # acl/uki-addons/ are starting points, not the final runtime state. - rm -rf "${verity_temp_dir}" + sudo rm -rf "${verity_temp_dir}" } info "Installing UKI packages for target ${FLAGS_target}" diff --git a/build_library/vm_image_util.sh b/build_library/vm_image_util.sh index 44747599531..10ceccdc6ea 100644 --- a/build_library/vm_image_util.sh +++ b/build_library/vm_image_util.sh @@ -835,12 +835,12 @@ install_uki_timeout_addon() { --cmdline=@"${timeout_temp_dir}/timeout-cmdline.txt" \ --stub="${efi_stub}" \ --output="${timeout_temp_dir}/timeout.addon.efi"; then - rm -rf "${timeout_temp_dir}" + sudo rm -rf "${timeout_temp_dir}" die "UKI timeout addon: ukify build failed" fi if [[ ! -f "${timeout_temp_dir}/timeout.addon.efi" ]]; then - rm -rf "${timeout_temp_dir}" + sudo rm -rf "${timeout_temp_dir}" die "UKI timeout addon: ukify failed to produce timeout.addon.efi" fi @@ -848,7 +848,7 @@ install_uki_timeout_addon() { info "UKI timeout addon: Installed -> EFI/Linux/${uki_name}.extra.d/timeout.addon.efi" info "UKI timeout addon: cmdline = systemd.default_device_timeout_sec=120" - rm -rf "${timeout_temp_dir}" + sudo rm -rf "${timeout_temp_dir}" } # Any other tweaks required? From f730598f9718bf9e43533b2e2645d567c1238e75 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Thu, 23 Jul 2026 20:23:33 +0000 Subject: [PATCH 20/22] acl-grub: use dedicated /usr verity hash partitions, consistent with UKI GRUB's disk_layout.json previously used the legacy single-partition inline-hash verity scheme. The UKI-side changes in this branch switched grub.cfg's verity cmdline construction to UUID=-based hash identification (dropping hash-offset=), which only works when the hash tree lives on its own independently-discoverable partition. Mirror disk_layout_uki.json's structure: add dedicated HASH-A/HASH-B partitions (dps-usr-verity type) cross-referenced via verity_hash, so blkid/udev can resolve systemd.verity_usr_hash=UUID= the same way UKI does. ACL-GRUB still cannot do Trident A/B updates (no addon-swap equivalent), but the partitioning/verity mechanics are now consistent between the two boot paths instead of silently broken on GRUB. ESP stays at 128MiB (no AB-staging need on GRUB). BIOS-BOOT partition is kept (shared with Portage/flatcar builds; RPM/GRUB mode already skips installing to it). --- build_library/disk_layout.json | 42 +++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/build_library/disk_layout.json b/build_library/disk_layout.json index e9747044f72..6aa4b69b552 100644 --- a/build_library/disk_layout.json +++ b/build_library/disk_layout.json @@ -32,22 +32,36 @@ "fs_type":"btrfs", "fs_compression":"zstd", "mount":"/usr", + "verity_hash":"4", "features": ["prioritize", "verity"] }, "4":{ + "label":"HASH-A", + "uuid":"1fae528b-6baf-4efc-a226-2a23f9291ce6", + "type":"dps-usr-verity", + "blocks":"20480" + }, + "5":{ "label":"USR-B", "uuid":"e03dd35c-7c2d-4a47-b3fe-27f15780a57c", "type":"flatcar-rootfs", "blocks":"2097152", - "fs_blocks":"262144" + "fs_blocks":"262144", + "verity_hash":"6" }, - "5":{ + "6":{ + "label":"HASH-B", + "uuid":"accb60b1-1d72-4e05-9363-6fd51e2ce788", + "type":"dps-usr-verity", + "blocks":"20480" + }, + "7":{ "label":"ROOT-C", "uuid":"d82521b4-07ac-4f1c-8840-ddefedc332f3", "type":"blank", "blocks":"0" }, - "6":{ + "8":{ "label":"OEM", "fs_label":"OEM", "type":"data", @@ -56,17 +70,17 @@ "fs_compression":"zlib", "mount":"/oem" }, - "7":{ + "9":{ "label":"OEM-CONFIG", "type":"flatcar-reserved", "blocks":"131072" }, - "8":{ + "10":{ "type":"blank", "label":"flatcar-reserved", "blocks":"0" }, - "9":{ + "11":{ "label":"ROOT", "fs_label":"ROOT", "type":"dps-root", @@ -76,28 +90,28 @@ } }, "vm":{ - "9":{ + "11":{ "label":"ROOT", "fs_label":"ROOT", "blocks":"12943360" } }, "azure":{ - "9":{ + "11":{ "label":"ROOT", "fs_label":"ROOT", "blocks":"58875904" } }, "vagrant":{ - "9":{ + "11":{ "label":"ROOT", "fs_label":"ROOT", "blocks":"33845248" } }, "onmetal":{ - "7":{ + "9":{ "label":"config-2", "fs_label":"config-2", "type":"data", @@ -130,6 +144,12 @@ "type":"blank" }, "9":{ + "type":"blank" + }, + "10":{ + "type":"blank" + }, + "11":{ "label":"ROOT", "fs_label":"ROOT", "type":"0fc63daf-8483-4772-8e79-3d69d8477de4", @@ -137,4 +157,4 @@ } } } -} +} From bb733233dd111970a7c6e00901ce125f0a661315 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Fri, 24 Jul 2026 14:51:10 +0000 Subject: [PATCH 21/22] vm_image_util: extend device-init timeout to GRUB arm64 test images too install_uki_timeout_addon already works around a CI-only QEMU-TCG flake on arm64 kola test images: heavy parallel-VM CPU contention can prevent udev from initialising ESP/OEM/usr-verity devices within the default initrd device timeout, dropping the VM to an emergency shell. That fix was UKI-only (built as a signed .efi addon dropped into .efi.extra.d/). GRUB has no addon mechanism, so add install_grub_timeout_override: since grub.cfg is plain text (not signed/embedded like a UKI), patch the already-written grub.cfg copies on the test image's ESP directly via sed, appending systemd.default_device_timeout_sec=120 to the shared linux_cmdline. Same scoping as the UKI addon: arm64 test image only (INJECT_DOCKER_SYSEXT=true), never applied to production images or amd64. --- build_library/vm_image_util.sh | 53 ++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/build_library/vm_image_util.sh b/build_library/vm_image_util.sh index 10ceccdc6ea..97e09e6af84 100644 --- a/build_library/vm_image_util.sh +++ b/build_library/vm_image_util.sh @@ -556,6 +556,8 @@ install_oem_package() { if [[ "${BOOTLOADER_MODE}" == "uki" ]]; then install_uki_oem_addon install_uki_timeout_addon + elif [[ "${BOOTLOADER_MODE}" == "grub" ]]; then + install_grub_timeout_override fi return 0 fi @@ -602,6 +604,8 @@ install_oem_package() { if [[ "${BOOTLOADER_MODE}" == "uki" ]]; then install_uki_oem_addon install_uki_timeout_addon + elif [[ "${BOOTLOADER_MODE}" == "grub" ]]; then + install_grub_timeout_override fi } @@ -851,6 +855,55 @@ install_uki_timeout_addon() { sudo rm -rf "${timeout_temp_dir}" } +# Append systemd.default_device_timeout_sec=120 to grub.cfg's shared cmdline, +# scoped to the arm64 kola *test* image only (INJECT_DOCKER_SYSEXT=true). +# +# GRUB has no addon mechanism like UKI's .extra.d, so unlike +# install_uki_timeout_addon this patches the already-written grub.cfg copies +# on the ESP directly (both are plain text, no rebuild/re-signing needed) - +# same CI-only QEMU-TCG device-init timeout this exists to work around. +# +# Deliberately NOT applied to the production VM image (INJECT_DOCKER_SYSEXT=false) +# or to amd64: the failure is a CI-only TCG-emulation artefact that never occurs +# on real hardware. +install_grub_timeout_override() { + if [[ "${BOOTLOADER_MODE}" != "grub" ]]; then + return 0 + fi + + # Scope: arm64 test image only. + if [[ "${ARCH}" != "arm64" ]] || [[ "${INJECT_DOCKER_SYSEXT:-false}" != "true" ]]; then + return 0 + fi + + local esp_dir="${VM_TMP_ROOT}/boot" + local -a grub_cfgs=( + "${esp_dir}/EFI/boot/grub.cfg" + "${esp_dir}/boot/grub2/grub.cfg" + ) + + local found=0 + for cfg in "${grub_cfgs[@]}"; do + if [[ ! -f "${cfg}" ]]; then + continue + fi + found=1 + if sudo grep -q 'systemd.default_device_timeout_sec=' "${cfg}"; then + info "GRUB timeout override: ${cfg} already patched; skipping" + continue + fi + sudo sed -i 's/^set linux_cmdline="\(.*\)"$/set linux_cmdline="\1 systemd.default_device_timeout_sec=120"/' "${cfg}" + info "GRUB timeout override: patched ${cfg}" + done + + if [[ "${found}" -eq 0 ]]; then + warn "GRUB timeout override: no grub.cfg found under ${esp_dir}; skipping" + return 0 + fi + + info "GRUB timeout override: cmdline += systemd.default_device_timeout_sec=120" +} + # Any other tweaks required? run_fs_hook() { local fs_hook=$(_get_vm_opt FS_HOOK) From 9063477ecfe8313b81f6aed63d383bf87e798f0c Mon Sep 17 00:00:00 2001 From: bfjelds Date: Fri, 24 Jul 2026 17:59:55 +0000 Subject: [PATCH 22/22] grub timeout override: fail if sed substitution does not take effect Addresses Copilot review comment on PR #28: install_grub_timeout_override logged \ patched\ unconditionally even if the sed substitution silently did not match (e.g. linux_cmdline format changed), leaving the timeout override ineffective without any signal. Now verifies the substitution took effect and dies with a clear message if not. --- build_library/vm_image_util.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build_library/vm_image_util.sh b/build_library/vm_image_util.sh index 97e09e6af84..3f56567c946 100644 --- a/build_library/vm_image_util.sh +++ b/build_library/vm_image_util.sh @@ -893,6 +893,9 @@ install_grub_timeout_override() { continue fi sudo sed -i 's/^set linux_cmdline="\(.*\)"$/set linux_cmdline="\1 systemd.default_device_timeout_sec=120"/' "${cfg}" + if ! sudo grep -q 'systemd.default_device_timeout_sec=120' "${cfg}"; then + die "GRUB timeout override: sed substitution did not match in ${cfg}; linux_cmdline format may have changed" + fi info "GRUB timeout override: patched ${cfg}" done