From f7a90842e2f62e43140079f729fc9134e3565589 Mon Sep 17 00:00:00 2001 From: bfjelds Date: Mon, 1 Jun 2026 13:18:57 -0700 Subject: [PATCH 01/31] 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 9192bb42f90..b4fb24edb37 100644 --- a/build_library/disk_layout_uki.json +++ b/build_library/disk_layout_uki.json @@ -24,23 +24,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", @@ -49,7 +63,7 @@ "fs_compression": "zlib", "mount": "/oem" }, - "5": { + "7": { "label": "ROOT", "fs_label": "ROOT", "type": "dps-root", @@ -59,21 +73,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" @@ -93,6 +107,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 d857ba2c43b..9eca0bb074d 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 f48bd11be96ba3aa8f14a90f9e5d7fcf86d8b621 Mon Sep 17 00:00:00 2001 From: bfjelds Date: Mon, 1 Jun 2026 14:03:52 -0700 Subject: [PATCH 02/31] 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 3f37071799e5d576256e87e9f71368d0a2c2cf0b Mon Sep 17 00:00:00 2001 From: bfjelds Date: Fri, 5 Jun 2026 11:42:24 -0700 Subject: [PATCH 03/31] 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 38579853148d22cf8ad195215784b5dfea2579ec Mon Sep 17 00:00:00 2001 From: bfjelds Date: Fri, 5 Jun 2026 11:48:52 -0700 Subject: [PATCH 04/31] 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 08a261e058c3c5b4d07c4691a13df3fcfaa830e5 Mon Sep 17 00:00:00 2001 From: bfjelds Date: Mon, 13 Jul 2026 13:52:56 -0700 Subject: [PATCH 05/31] 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 b39600962dc9b61413d3d7a03732e393a0cdc4b4 Mon Sep 17 00:00:00 2001 From: bfjelds Date: Tue, 14 Jul 2026 12:21:23 -0700 Subject: [PATCH 06/31] 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 159dd990fcda5877cda8b0ea74c0779fe2cd506b Mon Sep 17 00:00:00 2001 From: bfjelds Date: Tue, 14 Jul 2026 12:46:06 -0700 Subject: [PATCH 07/31] 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 9e2d158f919abe8f202065b7c532b0a91bedb509 Mon Sep 17 00:00:00 2001 From: bfjelds Date: Tue, 14 Jul 2026 13:13:17 -0700 Subject: [PATCH 08/31] 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 625a18a5e266d7ebf5dd7b524f501f45e39a92a2 Mon Sep 17 00:00:00 2001 From: bfjelds Date: Tue, 14 Jul 2026 13:22:12 -0700 Subject: [PATCH 09/31] 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 566a92f6c908b4634bfd327c25023f5a7fbc8351 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Tue, 14 Jul 2026 20:44:12 +0000 Subject: [PATCH 10/31] 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 6485eb7c93bb3525f892550a3cdd15fb254a19b6 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Sun, 19 Jul 2026 16:10:35 +0000 Subject: [PATCH 11/31] 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 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build_library/disk_layout_uki.json b/build_library/disk_layout_uki.json index b4fb24edb37..44be15d4117 100644 --- a/build_library/disk_layout_uki.json +++ b/build_library/disk_layout_uki.json @@ -10,11 +10,11 @@ "layouts": { "base": { "1": { - "_comment": "128 MiB -> 192 MiB (blocks are 512-byte sectors). UKI boot keeps kernel+initramfs on the ESP, and the acl-t UKI rebuilt by Image Customizer needs more headroom than stock.", + "_comment": "128 MiB -> 256 MiB (blocks are 512-byte sectors). UKI boot keeps kernel+initramfs on the ESP; the acl-t UKI rebuilt by Image Customizer and Trident A/B update addons both need more headroom than stock.", "label": "EFI-SYSTEM", "fs_label": "EFI-SYSTEM", "type": "efi", - "blocks": "393216", + "blocks": "524288", "fs_type": "vfat", "mount": "/boot", "features": [] From 2e17ae6eff077975afd32ea5145d9d0ed22202c7 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Mon, 20 Jul 2026 16:48:30 +0000 Subject: [PATCH 12/31] 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 1f7abaec193d42167984a509fdbf2b7f91f46d0b Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Mon, 20 Jul 2026 16:53:51 +0000 Subject: [PATCH 13/31] 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 5b829029debf1d0bdb8e4670e09c508df5a49ca1 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Mon, 20 Jul 2026 17:10:54 +0000 Subject: [PATCH 14/31] 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 aa12a3ef0781a8ec0873c0b719f9e2265dda7aa7 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Tue, 21 Jul 2026 23:43:20 +0000 Subject: [PATCH 15/31] 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 8b072baaac4c936a07368ec2143bc8c630cc0097 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Wed, 22 Jul 2026 18:53:14 +0000 Subject: [PATCH 16/31] 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 a0fc5534c6a95aef420d820f38e52e1b7bcbb44e Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Wed, 22 Jul 2026 19:15:52 +0000 Subject: [PATCH 17/31] 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 e28a115300e3f7b7e72599d367dbdc67fcd1d5fd Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Wed, 22 Jul 2026 19:30:47 +0000 Subject: [PATCH 18/31] 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 3e502b5706c91815f938b35f5bb29c7b66c79b3b Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Wed, 22 Jul 2026 19:54:13 +0000 Subject: [PATCH 19/31] 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 8cdc0c74ee5f2e4c98373d277249271e66b7fc3e Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Thu, 23 Jul 2026 20:23:33 +0000 Subject: [PATCH 20/31] 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 97fcc37e70544fa28206a2ff49207ce3ef5487c9 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Fri, 24 Jul 2026 14:51:10 +0000 Subject: [PATCH 21/31] 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 1e0b995ff5c27512de2fc604657b10f581a44388 Mon Sep 17 00:00:00 2001 From: bfjelds Date: Fri, 24 Jul 2026 17:59:55 +0000 Subject: [PATCH 22/31] 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 From 1528eb5bc6f14b084798bfeeb3ac9cb5d515d9c4 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Fri, 31 Jul 2026 21:02:52 +0000 Subject: [PATCH 23/31] validate_qemu: avoid pipefail/SIGPIPE race in libvirt network active check virsh net-info default | grep -q could report a false pipeline failure under set -o pipefail (inherited via sourcing from validate_common.sh): grep -q exits as soon as it matches, which can SIGPIPE virsh while it is still writing, making the whole pipeline non-zero even though the pattern matched. Capture output first, then grep the captured string. --- acl/validate/validate_qemu.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/acl/validate/validate_qemu.sh b/acl/validate/validate_qemu.sh index 4b0fdd46e28..7c77e690367 100755 --- a/acl/validate/validate_qemu.sh +++ b/acl/validate/validate_qemu.sh @@ -49,7 +49,12 @@ ensure_libvirt_network() { return 1 fi fi - if ! virsh net-info default 2>/dev/null | grep -q 'Active:.*yes'; then + # Capture output first instead of piping directly into grep: under `set -o pipefail` + # (inherited from validate_common.sh, since this file is sourced not executed), + # grep -q can exit as soon as it matches while virsh is still writing, causing a + # SIGPIPE in virsh and a false pipeline failure even though the pattern matched. + net_info="$(virsh net-info default 2>/dev/null)" + if ! grep -q 'Active:.*yes' <<< "$net_info"; then info "Starting libvirt default network..." if sudo virsh net-start default; then sudo virsh net-autostart default 2>/dev/null || true From abea204943f729d8b59b04cba987d103dca8bd37 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Thu, 6 Aug 2026 21:14:30 +0000 Subject: [PATCH 24/31] rpm mode: disable auditd.service by default trident-selinux pulls in policycoreutils-python-utils (for SELinux module install tooling), which transitively depends on the audit package. Azure Linux audit RPM enables+starts auditd via its %post systemd preset, unlike portage-mode builds which ship a blanket disable-all preset. Remove the enable symlink so RPM-mode images match upstream ACL behavior: auditd installed but disabled by default. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build_library/rpm/build_image_util.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build_library/rpm/build_image_util.sh b/build_library/rpm/build_image_util.sh index bb191ce6640..4ba04f09f7d 100644 --- a/build_library/rpm/build_image_util.sh +++ b/build_library/rpm/build_image_util.sh @@ -938,6 +938,13 @@ _configure_misc_rpm() { sudo cp "${BUILD_LIBRARY_DIR}/rpm/additional_files/audit-rules.service" "${root_fs_dir}/usr/lib/systemd/system/audit-rules.service" fi + # Disable auditd.service if present (it is pulled in transitively by trident RPMs). + if [[ -f "${root_fs_dir}/usr/lib/systemd/system/auditd.service" ]]; then + info "RPM mode: Disabling auditd.service (left disabled by default, matching portage-mode ACL images)" + sudo rm -f "${root_fs_dir}/usr/lib/systemd/system/multi-user.target.wants/auditd.service" + sudo rm -f "${root_fs_dir}/etc/systemd/system/multi-user.target.wants/auditd.service" + fi + # Create tmpfiles.d entry for logrotate state directory. # The Azure Linux 3 logrotate RPM doesn't ship a tmpfiles.d drop-in, # so /var/lib/logrotate is not recreated at boot on ACL's immutable rootfs. From a4ac9852c6aa67e91dfe8aa3b65314f0acbb1d0a Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Thu, 6 Aug 2026 22:53:55 +0000 Subject: [PATCH 25/31] rpm mode: install trident-acl-agent instead of trident ACL images need trident-acl-agent (triggers AB-updates), not the full trident package. Keep the auditd.service disable workaround in build_image_util.sh regardless, since trident-acl-agent still depends on trident, and defense-in-depth is warranted independent of the trident.spec fix (microsoft/trident#734). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build_library/rpm/package_catalog.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_library/rpm/package_catalog.yaml b/build_library/rpm/package_catalog.yaml index 9eca0bb074d..efc73649101 100644 --- a/build_library/rpm/package_catalog.yaml +++ b/build_library/rpm/package_catalog.yaml @@ -31,7 +31,7 @@ packages: - policycoreutils - ca-certificates - irqbalance - - trident + - trident-acl-agent sys-libs/systemd-libs: systemd-libs sys-apps/systemd-networkd: systemd-networkd net-misc/systemd-networkd: systemd-networkd From c59aa9443fa5748e93f45b1f726d35a87cbaeea6 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Mon, 24 Aug 2026 18:00:53 +0000 Subject: [PATCH 26/31] include trident RPM (not trident-acl-agent) initially --- build_library/rpm/package_catalog.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_library/rpm/package_catalog.yaml b/build_library/rpm/package_catalog.yaml index efc73649101..9eca0bb074d 100644 --- a/build_library/rpm/package_catalog.yaml +++ b/build_library/rpm/package_catalog.yaml @@ -31,7 +31,7 @@ packages: - policycoreutils - ca-certificates - irqbalance - - trident-acl-agent + - trident sys-libs/systemd-libs: systemd-libs sys-apps/systemd-networkd: systemd-networkd net-misc/systemd-networkd: systemd-networkd From 41b7f266952a05bf46c92958cb620247ab954349 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Thu, 27 Aug 2026 16:07:39 +0000 Subject: [PATCH 27/31] Address PR #28 review feedback - generate_grub_hashes.py: fix PCR policy allowlist entry for the OEM partition, which moved from gpt6 to gpt8 in this PR's renumbered disk_layout.json. The expected measured GRUB command is the OEM search result substituted into "source (hd0,gptN)/grub.cfg"; it must track the actual partition number or secure/measured boot policy checks will fail against real hardware measurements. - prod_image_util.sh: create_prod_tar() resolve ROOT and USR-A by GPT PARTLABEL via blkid instead of hardcoded partition numbers (p9/p3), which broke when ROOT moved from partition 9 to 11 in this PR. - disk_util / disk_layout.json: add an explicit legacy_verity_offset opt-in flag. When set on a verity_hash-bearing partition, Verity() now writes the hash tree to both the dedicated hash partition and the legacy inline offset within the data partition (same salt, so both copies are byte-identical and share one root hash). This keeps Portage/bootengine boots working via the existing hardcoded inline hash-offset activation, without requiring any bootengine or GRUB cmdline changes, while RPM/UKI-style consumers of the dedicated HASH-A/HASH-B partitions are unaffected. Verified end-to-end with veritysetup that both write targets produce an identical root hash and both independently pass veritysetup open. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build_library/disk_layout.json | 6 ++-- build_library/disk_util | 44 ++++++++++++++++++++++----- build_library/generate_grub_hashes.py | 2 +- build_library/prod_image_util.sh | 31 ++++++++++++++----- 4 files changed, 66 insertions(+), 17 deletions(-) diff --git a/build_library/disk_layout.json b/build_library/disk_layout.json index 6aa4b69b552..01209808a3f 100644 --- a/build_library/disk_layout.json +++ b/build_library/disk_layout.json @@ -33,6 +33,7 @@ "fs_compression":"zstd", "mount":"/usr", "verity_hash":"4", + "legacy_verity_offset":true, "features": ["prioritize", "verity"] }, "4":{ @@ -47,7 +48,8 @@ "type":"flatcar-rootfs", "blocks":"2097152", "fs_blocks":"262144", - "verity_hash":"6" + "verity_hash":"6", + "legacy_verity_offset":true }, "6":{ "label":"HASH-B", @@ -157,4 +159,4 @@ } } } -} +} diff --git a/build_library/disk_util b/build_library/disk_util index 8617e7c3b94..78651983d56 100755 --- a/build_library/disk_util +++ b/build_library/disk_util @@ -84,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', 'verity_hash')) + 'fs_compression', 'verity_hash', 'legacy_verity_offset')) integer_layout_keys = set(( 'blocks', 'block_size', 'fs_blocks', 'fs_block_size', 'part_alignment', 'fs_bytes_per_inode', 'fs_inode_size')) @@ -824,6 +824,12 @@ def Verity(options): active USR slot (the one with 'prioritize') is formatted, so the inactive slot is skipped naturally. + A partition with both a verity_hash cross-reference and + legacy_verity_offset set to true gets the hash tree written to *both* + locations (dedicated partition + inline within the data partition), so + boot paths that only know the legacy inline offset keep working + unmodified alongside consumers that expect a dedicated hash partition. + Args: options: Flags passed to the script """ @@ -875,22 +881,46 @@ def Verity(options): 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']] + base_cmd = ['veritysetup', 'format', '--hash=sha256', + '--data-block-size', part['fs_block_size'], + '--hash-block-size', part['fs_block_size'], + '--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] + base_cmd = base_cmd + ['--hash-offset', part['fs_bytes']] + + # Use an explicit salt (rather than veritysetup's random default) so + # that if we also need to write a second, legacy-inline copy of the + # hash tree below, both writes are byte-identical and produce the + # same root hash. + salt = os.urandom(32).hex() + verity_cmd = base_cmd + ['--salt', salt, data_loop, hash_loop] verityout = SudoOutput(verity_cmd).decode('utf8') print(verityout.strip()) + if hash_part and part.get('legacy_verity_offset'): + # Also write an identical copy of the hash tree inline, appended + # after the filesystem within the data partition itself, for boot + # paths (e.g. Portage/bootengine) that don't yet know how to + # locate a separate hash partition. + legacy_cmd = base_cmd + ['--salt', salt, '--hash-offset', + part['fs_bytes'], data_loop, data_loop] + legacy_out = SudoOutput(legacy_cmd).decode('utf8') + m_legacy = re.search(r'Root hash:\s+([a-f0-9]{64})$', legacy_out, + re.IGNORECASE | re.MULTILINE) + m_primary = re.search(r'Root hash:\s+([a-f0-9]{64})$', verityout, + re.IGNORECASE | re.MULTILINE) + if not m_legacy or not m_primary or \ + m_legacy.group(1) != m_primary.group(1): + raise Exception( + "Legacy inline verity hash does not match dedicated-partition " + "verity hash for partition %r" % part_num) + # Read the filesystem UUID from the data partition for cmdline use. if options.fs_uuid: fs_uuid_out = SudoOutput( diff --git a/build_library/generate_grub_hashes.py b/build_library/generate_grub_hashes.py index bd9ab9e8387..e3bf478fdba 100755 --- a/build_library/generate_grub_hashes.py +++ b/build_library/generate_grub_hashes.py @@ -51,7 +51,7 @@ {"value": 'search --no-floppy --set oem --part-label OEM --hint hd0,gpt1', "description": "Flatcar Grub configuration %s" % version}, {"value": 'set .+', "description": "Flatcar Grub configuration %s" % version}, {"value": 'setparams Flatcar default', "description": "Flatcar Grub configuration %s" % version}, - {"value": 'source (hd0,gpt6)/grub.cfg', "description": "Flatcar Grub configuration %s" % version}] + {"value": 'source (hd0,gpt8)/grub.cfg', "description": "Flatcar Grub configuration %s" % version}] with open(os.path.join(outputdir, "grub_commands.config"), "w") as f: f.write(json.dumps({"8": {"asciivalues": [{"prefix": "grub_cmd", "values": commands}]}})) diff --git a/build_library/prod_image_util.sh b/build_library/prod_image_util.sh index 1fa25905563..22998ff47ea 100755 --- a/build_library/prod_image_util.sh +++ b/build_library/prod_image_util.sh @@ -218,13 +218,30 @@ create_prod_tar() { local container="${BUILD_DIR}/flatcar-container.tar.gz" local lodev="$(sudo losetup --find --show -r -P "${image}")" local lodevbase="$(basename "${lodev}")" - sudo mkdir -p "/mnt/${lodevbase}p9" - sudo mount "${lodev}p9" "/mnt/${lodevbase}p9" - sudo mount "${lodev}p3" "/mnt/${lodevbase}p9/usr" - sudo tar --xattrs -czpf "${container}" -C "/mnt/${lodevbase}p9" . - sudo umount "/mnt/${lodevbase}p9/usr" - sudo umount "/mnt/${lodevbase}p9" - sudo rmdir "/mnt/${lodevbase}p9" + local mountpoint="/mnt/${lodevbase}-root" + + # Resolve ROOT and USR-A by GPT partition label rather than a hardcoded + # partition number, since disk_layout.json layouts can renumber + # partitions (e.g. ROOT moving from p9 to p11). + local partdev rootdev usrdev + for partdev in "${lodev}"p*; do + case "$(sudo blkid -o value -s PARTLABEL "${partdev}" 2>/dev/null)" in + ROOT) rootdev="${partdev}" ;; + USR-A) usrdev="${partdev}" ;; + esac + done + if [[ -z "${rootdev}" || -z "${usrdev}" ]]; then + sudo losetup --detach "${lodev}" + die_notrace "create_prod_tar: could not resolve ROOT/USR-A partitions on ${lodev}" + fi + + sudo mkdir -p "${mountpoint}" + sudo mount "${rootdev}" "${mountpoint}" + sudo mount "${usrdev}" "${mountpoint}/usr" + sudo tar --xattrs -czpf "${container}" -C "${mountpoint}" . + sudo umount "${mountpoint}/usr" + sudo umount "${mountpoint}" + sudo rmdir "${mountpoint}" sudo losetup --detach "${lodev}" } From 1f81d57d9661789516ba0b69ddce9d44309ee28a Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Fri, 28 Aug 2026 16:47:37 +0000 Subject: [PATCH 28/31] rpm mode: uninstall auditd instead of just disabling it Deleting the multi-user.target.wants symlink for auditd.service is not durable: Azure Linux ships a 90-default.preset with an explicit "enable auditd.service" rule, so any later systemctl preset/preset-all re-application (e.g. triggered by downstream image customization installing more packages) recreates the symlink and the service starts on the next boot. Uninstall the audit and python3-audit packages instead, removing the unit file itself so there is nothing left for a future preset pass to enable. audit-libs is left in place since other packages link against libaudit.so.1 directly. rpm -e uses --noscripts, so %preun/%postun (which would run `systemctl disable`) never execute, and the *.wants symlinks aren't part of the package payload in the first place (systemctl/preset creates them, not rpm). Removing the package alone leaves them as dangling symlinks, which trips kola's dead-symlink content check. Explicitly remove both possible .wants symlink locations after the uninstall. This is a temporary workaround for trident-selinux's transitive dependency on audit. Trident's fix (microsoft/trident#734) already removed that dependency, but the fix hasn't been published to PMC yet (PMC still serves trident 0.26.0). Once a trident release containing the fix reaches PMC, this block should be removed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build_library/rpm/build_image_util.sh | 31 ++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/build_library/rpm/build_image_util.sh b/build_library/rpm/build_image_util.sh index 4ba04f09f7d..56e27889c96 100644 --- a/build_library/rpm/build_image_util.sh +++ b/build_library/rpm/build_image_util.sh @@ -938,9 +938,34 @@ _configure_misc_rpm() { sudo cp "${BUILD_LIBRARY_DIR}/rpm/additional_files/audit-rules.service" "${root_fs_dir}/usr/lib/systemd/system/audit-rules.service" fi - # Disable auditd.service if present (it is pulled in transitively by trident RPMs). - if [[ -f "${root_fs_dir}/usr/lib/systemd/system/auditd.service" ]]; then - info "RPM mode: Disabling auditd.service (left disabled by default, matching portage-mode ACL images)" + # Remove auditd (it is pulled in transitively by trident RPMs, and Azure Linux's + # 90-default.preset unconditionally enables auditd.service). Merely deleting the + # multi-user.target.wants symlink is not durable: any later `systemctl preset`/ + # `preset-all` re-application (e.g. triggered by downstream image customization + # installing more packages) recreates it from that preset rule and the service + # starts on the next boot. Uninstalling the package removes the unit file itself, + # so there is nothing left for a future preset pass to enable. + # audit-libs is left in place: it provides libaudit.so.1, which other packages + # (e.g. systemd, sudo, polkit) link against directly. + # + # TEMPORARY: this whole block is a workaround for trident-selinux's transitive + # dependency on audit. Trident's fix (microsoft/trident#734) already removed + # that dependency, but it has not been published to PMC yet (PMC still serves + # trident 0.26.0, which predates the fix). Once a trident release containing + # the fix reaches PMC, delete this block. + local audit_pkgs + audit_pkgs=$(sudo rpm --dbpath="${root_fs_dir}/var/lib/rpm" -qa "audit" "python3-audit" 2>/dev/null | sort -u) + if [[ -n "${audit_pkgs}" ]]; then + info "RPM mode: Removing auditd packages: ${audit_pkgs//$'\n'/ }" + if ! sudo rpm --root="${root_fs_dir}" --dbpath="/var/lib/rpm" -e --nodeps --noscripts ${audit_pkgs}; then + error "RPM mode: Failed to remove auditd packages, aborting build" + exit 1 + fi + # --noscripts skips %preun/%postun, so `systemctl disable` never runs, and + # the *.wants symlinks aren't part of the package payload (systemctl/preset + # creates them, not rpm), so removing the package doesn't remove them either. + # Without this, they're left as dangling symlinks (unit file gone from + # /usr/lib/systemd/system) which trips kola's dead-symlink content check. sudo rm -f "${root_fs_dir}/usr/lib/systemd/system/multi-user.target.wants/auditd.service" sudo rm -f "${root_fs_dir}/etc/systemd/system/multi-user.target.wants/auditd.service" fi From fee4ee4fe84384275f60297e061e5acef0f125cb Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Fri, 28 Aug 2026 18:59:12 +0000 Subject: [PATCH 29/31] prod_image_util.sh: initialize rootdev/usrdev to avoid unbound-var crash create_prod_tar() runs under switch_to_strict_mode (set -u). `local partdev rootdev usrdev` leaves rootdev/usrdev declared-but-unset, and referencing an unset local variable -- even just to test it with `[[ -z "${rootdev}" ]]` -- is itself an unbound-variable error under set -u. If ROOT or USR-A can't be resolved by partition label, the intended missing-partition check crashes on the check itself, before it can reach the informative die_notrace message or detach the loop device. Initialize both to empty strings so the check works as intended. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build_library/prod_image_util.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/build_library/prod_image_util.sh b/build_library/prod_image_util.sh index 22998ff47ea..631ed04934c 100755 --- a/build_library/prod_image_util.sh +++ b/build_library/prod_image_util.sh @@ -223,7 +223,12 @@ create_prod_tar() { # Resolve ROOT and USR-A by GPT partition label rather than a hardcoded # partition number, since disk_layout.json layouts can renumber # partitions (e.g. ROOT moving from p9 to p11). - local partdev rootdev usrdev + # rootdev/usrdev must be initialized: this function runs under + # switch_to_strict_mode (set -u), and referencing a `local`-declared-but- + # unset variable -- even just to test it with `-z` -- is itself an + # unbound-variable error, which would abort before the missing-partition + # check below ever gets to run. + local partdev rootdev="" usrdev="" for partdev in "${lodev}"p*; do case "$(sudo blkid -o value -s PARTLABEL "${partdev}" 2>/dev/null)" in ROOT) rootdev="${partdev}" ;; From c46dc7fa470d15c6916675c6f4771a0e06e18a74 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Fri, 28 Aug 2026 20:16:27 +0000 Subject: [PATCH 30/31] uki_install.sh: rename verity slot addons to slot-*.addon.efi, add acl.slot cmdline Rename the per-slot verity addon templates from verity-a/b.addon.efi to slot-a/b.addon.efi, and install the active slot addon verbatim (no rename) into the .extra.d directory. Add acl.slot= to each slot cmdline so the running kernel can identify its active slot. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build_library/rpm/uki_install.sh | 37 +++++++++++++++++++------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/build_library/rpm/uki_install.sh b/build_library/rpm/uki_install.sh index 24f92f255af..3330ed954cd 100755 --- a/build_library/rpm/uki_install.sh +++ b/build_library/rpm/uki_install.sh @@ -175,10 +175,11 @@ OSREL # NOTE: The main UKI cmdline contains only slot-independent args. # Slot-specific args are delivered via UKI addons: # - # 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. + # slot-a.addon.efi/ — Active slot's verity partition identity + # slot-b.addon.efi (PARTUUID for data + hash), root hash, and + # acl.slot=. Trident swaps this (copying + # the template verbatim, no rename) 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. @@ -486,11 +487,14 @@ _uki_build_debug_addon() { # systemd.verity_usr_hash=PARTUUID= # systemd.verity_usr_options=panic-on-corruption # usrhash= +# acl.slot= # # 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. +# cmdline. Trident swaps which addon is in .extra.d/ to switch slots — it +# copies the slot's template verbatim (slot-a.addon.efi / slot-b.addon.efi, +# no rename) from acl/uki-addons/ into .efi.extra.d/. _uki_build_verity_addons() { local esp_dir="$1" local uki_name="$2" @@ -571,35 +575,38 @@ _uki_build_verity_addons() { cmdline+=" systemd.verity_usr_hash=PARTUUID=${hash_uuid}" cmdline+=" systemd.verity_usr_options=panic-on-corruption" cmdline+=" usrhash=${usr_hash}" + cmdline+=" acl.slot=${slot}" info "UKI/RPM: Slot ${slot_label} cmdline = ${cmdline}" - echo "${cmdline}" > "${verity_temp_dir}/verity-${slot}-cmdline.txt" + echo "${cmdline}" > "${verity_temp_dir}/slot-${slot}-cmdline.txt" - addon_file="${verity_temp_dir}/verity-${slot}.addon.efi" + addon_file="${verity_temp_dir}/slot-${slot}.addon.efi" if ! sudo ukify build \ - --cmdline=@"${verity_temp_dir}/verity-${slot}-cmdline.txt" \ + --cmdline=@"${verity_temp_dir}/slot-${slot}-cmdline.txt" \ --stub="${efi_stub}" \ --output="${addon_file}"; then sudo rm -rf "${verity_temp_dir}" - die "UKI/RPM: ukify build failed for verity-${slot}.addon.efi" + die "UKI/RPM: ukify build failed for slot-${slot}.addon.efi" fi if [[ ! -f "${addon_file}" ]]; then sudo rm -rf "${verity_temp_dir}" - die "UKI/RPM: ukify failed to produce verity-${slot}.addon.efi" + die "UKI/RPM: ukify failed to produce slot-${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/" + sudo cp "${addon_file}" "${template_dir}/slot-${slot}.addon.efi" + info "UKI/RPM: Saved slot-${slot}.addon.efi → acl/uki-addons/" done - # Install slot A as the active verity addon + # Install slot A as the active verity addon — copied verbatim (no + # rename) so the staged filename matches its template, mirroring how + # Trident activates the target slot during an A/B update. 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" + sudo cp "${template_dir}/slot-a.addon.efi" "${addon_dir}/slot-a.addon.efi" + info "UKI/RPM: Activated slot A → EFI/Linux/${uki_name}.extra.d/slot-a.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) From 6a3c132087e079ee4a615b84fb7e52e4bc03004a Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Fri, 28 Aug 2026 20:47:16 +0000 Subject: [PATCH 31/31] uki_install.sh: fix stray trailing slash in doc comment Address Copilot review feedback on PR 64: slot-a.addon.efi/ looked like a directory path due to a stray trailing slash. Replaced with a comma so it reads as two alternative addon filenames. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build_library/rpm/uki_install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build_library/rpm/uki_install.sh b/build_library/rpm/uki_install.sh index 3330ed954cd..bfaf1969a4c 100755 --- a/build_library/rpm/uki_install.sh +++ b/build_library/rpm/uki_install.sh @@ -175,8 +175,8 @@ OSREL # NOTE: The main UKI cmdline contains only slot-independent args. # Slot-specific args are delivered via UKI addons: # - # slot-a.addon.efi/ — Active slot's verity partition identity - # slot-b.addon.efi (PARTUUID for data + hash), root hash, and + # slot-a.addon.efi, — Active slot's verity partition identity + # slot-b.addon.efi (PARTUUID for data + hash), root hash, and # acl.slot=. Trident swaps this (copying # the template verbatim, no rename) to switch # A/B slots. See _uki_build_verity_addons() below.