Skip to content

initrd/bin/kexec-sign-config.sh: stop re-hashing stale default entry paths on -u update#2150

Merged
tlaurion merged 2 commits into
linuxboot:masterfrom
tlaurion:fix-stale-rehash-kexec-sign-config
Jul 8, 2026
Merged

initrd/bin/kexec-sign-config.sh: stop re-hashing stale default entry paths on -u update#2150
tlaurion merged 2 commits into
linuxboot:masterfrom
tlaurion:fix-stale-rehash-kexec-sign-config

Conversation

@tlaurion

@tlaurion tlaurion commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

[Updated 2026-07-07: this description has been significantly revised since first opened. See commits and comments for full history.]

Remove the -u block that re-hashed file paths extracted from the old kexec_default_hashes.txt. Those paths are from a previously saved default boot entry. When a distro replaces a versioned boot file in-place (e.g. xen-4.19.4.gz to xen-4.19.5.gz), the old path no longer exists on disk and sha256sum fails.

The re-hash was redundant: the find ./ -type f ! -path "./kexec*" -print0 | xargs -0 sha256sum command in the same -u block already generates a complete, fresh hash of all current /boot files. The user re-saves the default boot entry from the boot menu to regenerate kexec_default_hashes.txt with current paths.

Root cause

The rehash also behaved unpredictably across bash builds. Before commit 1f6a975 (2025-02-07), the subshell had no TRACE_FUNC and set -e was not enforced on the pipeline failure: the sha256sum error was silently masked by print_tree returning 0, the subshell exited 0, and a partial hash file was signed. After 1f6a975 placed TRACE_FUNC as the first command inside the subshell, set -e became enforced, making the same pipeline failure fatally abort the subshell before print_tree could mask it.

This is a known bug in bash 5.1 execute_cmd.c: when the first command in a (... ) subshell is a builtin (cd), the executing subshell path pollutes the builtin_ignoring_errexit state, causing exit_immediately_on_error to be set to 0. When the first command is a function call, the execute_shell_function() path uses its own setjmp/longjmp context that does not touch the errexit state. Fixed in bash 5.2 and documented in BashFAQ/105.

Additional fixes

This PR also includes a second commit that hardens every (... ) subshell with a pipeline across the codebase against the same class of bug, using an explicit exit code capture pattern:

pipeline_exit=0
cmd | other_cmd || pipeline_exit=$?
exit $pipeline_exit

Files changed:

  • initrd/bin/oem-factory-reset.sh — hash generation subshell
  • initrd/bin/root-hashes-gui.sh — root hash computation subshell
  • initrd/bin/kexec-insert-key.sh — initramfs cpio archive subshell
  • initrd/bin/inject_firmware.sh — firmware initrd injection subshell
  • initrd/bin/kexec-save-default.sh — default boot hash creation subshell
  • initrd/etc/luks-functions.sh — DRK keyslot detection process substitution

Mitigation on unpatched firmware

mount -o remount,rw /boot
rm /boot/kexec_default_hashes.txt
mount -o remount,ro /boot

Then re-run Update checksums. With the file gone, the -e guard skips the re-hash entirely. Then set a new default from the boot menu to regenerate kexec_default_hashes.txt with current paths.

Copilot AI review requested due to automatic review settings July 6, 2026 00:00
@tlaurion tlaurion added bug feature freeze - 2025-01-30 (originally targeted 2024-11-20) labels Jul 6, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review any files in this pull request.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@tlaurion

tlaurion commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

QubesOS automated testing was testing (results still available as writing):

diff --git a/initrd/bin/kexec-sign-config b/initrd/bin/kexec-sign-config
index 52e6add6d7e..050b5730d52 100755
--- a/initrd/bin/kexec-sign-config
+++ b/initrd/bin/kexec-sign-config
@@ -34,19 +34,34 @@ confirm_gpg_card
 mount -o remount,rw /boot
 
 # update hashes in /boot before signing
-if [ "$update" = "y" ]; then
+       if [ "$update" = "y" ]; then
        (
                cd /boot
+               DEBUG "DEBUGR: find/xargs sha256sum of /boot (excluding ./kexec*) to kexec_hashes.txt"
                find ./ -type f ! -path './kexec*' -print0 | xargs -0 sha256sum >/boot/kexec_hashes.txt
                if [ -e /boot/kexec_default_hashes.txt ]; then
                        DEFAULT_FILES=$(cat /boot/kexec_default_hashes.txt | cut -f3 -d ' ')
-                       echo $DEFAULT_FILES | xargs sha256sum >/boot/kexec_default_hashes.txt
+                       DEBUG "DEBUGR: kexec_default_hashes.txt FOUND, extracting paths via cut -f3"
+                       DEBUG "DEBUGR: DEFAULT_FILES=[${DEFAULT_FILES}]"
+                       echo "DEBUGR: about to run 'echo \$DEFAULT_FILES | xargs sha256sum >/boot/kexec_default_hashes.txt'" >&2
+                       echo $DEFAULT_FILES | xargs sha256sum >/boot/kexec_default_hashes.txt; _rehash_rc=$?
+                       DEBUG "DEBUGR: rehash pipeline exit code = ${_rehash_rc}"
+                       [ ${_rehash_rc} -ne 0 ] && DEBUG "DEBUGR: rehash FAILED (non-zero exit)"
+               else
+                       DEBUG "DEBUGR: kexec_default_hashes.txt NOT FOUND (skipping rehash)"
                fi
 
                #also save the file & directory structure to detect added files
-               print_tree >/boot/kexec_tree.txt
-       )
-       [ $? -eq 0 ] || die "$paramsdir: Failed to update hashes."
+               DEBUG "DEBUGR: about to run print_tree"
+               print_tree >/boot/kexec_tree.txt; _print_tree_rc=$?
+               DEBUG "DEBUGR: print_tree exit code = ${_print_tree_rc}"
+               DEBUG "DEBUGR: end of subshell"
+       ); _subshell_rc=$?
+       DEBUG "DEBUGR: subshell exit code = ${_subshell_rc}"
+       DEBUG "DEBUGR: after subshell, rc=${_subshell_rc}"
+       [ ${_subshell_rc} -eq 0 ] || DEBUG "DEBUGR: die would trigger (rc=${_subshell_rc})"
+       [ ${_subshell_rc} -eq 0 ] || die "$paramsdir: Failed to update hashes."
+       DEBUG "DEBUGR: after die check (rc=${_subshell_rc}) - CONTINUING"
 
        # Remove any package trigger log files
        # We don't need them after the user decides to sign
- This confirmed:
  - DEBUGR: kexec_default_hashes.txt FOUND
  - DEBUGR: DEFAULT_FILES=[./xen-4.19.4.gz   ← stale xen path
  - DEBUGR: rehash pipeline exit code = 123   ← sha256sum FAILED
  - DEBUGR: rehash FAILED (non-zero exit)     ← xargs returned 123
  - DEBUGR: about to run print_tree           ← subshell SURVIVED!
  - DEBUGR: print_tree exit code = 0          ← masked the failure
  - DEBUGR: end of subshell
  - DEBUGR: subshell exit code = 0            ← print_tree's exit code
  - DEBUGR: after die check (rc=0) - CONTINUING ← die never called

So the bug is new.

Note that replacing xen-4.19.4.gz with xen-4.19.5.gz landed July 2nd, as per normal testing->release falling through all current automated testing in place for QubesOS QA/Dasharo.

CC: @marmarek @macpijan @pietrushnic, let me know how we want to handle this forward.

  • One possibility is for QubesOS to drive emergency Xen binary update, not removing old one so grub.cfg entries are regenerated leaving user the possibility to revert older binary from availabe boot entries (as for current kernel/initramfs)
  • Release a bugfix release for all platforms using Qubes (Novacustom/Nitrokey/Purism/HardenedVault/Others/All)
  • Push mitigation as detailed in OP. No impact known for any other OSes since 2018; typically OS updates installs new kernel/initramfs do not force deletion of old ones "atomically": this is left for a cleanup process which respects the number of rollback of boot entries defined.

@tlaurion tlaurion force-pushed the fix-stale-rehash-kexec-sign-config branch 3 times, most recently from 7acbb08 to 3950b73 Compare July 6, 2026 00:54
@tlaurion tlaurion requested a review from Copilot July 6, 2026 00:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review any files in this pull request.

@tlaurion tlaurion marked this pull request as draft July 7, 2026 19:30
…paths on -u update

Remove the -u block that re-hashed file paths extracted from the old
kexec_default_hashes.txt.  Those paths are from a previously saved
default boot entry.  When a distro replaces a versioned boot file
in-place (e.g. xen-4.19.4.gz to xen-4.19.5.gz), the old path no
longer exists on disk and sha256sum fails.

The re-hash was redundant: the find ./ -type f ! -path "./kexec*"
-print0 | xargs -0 sha256sum command in the same -u block already
generates a complete, fresh hash of all current /boot files.

The re-hash also behaved unpredictably across bash builds.  Before
commit 1f6a975 (2025-02-07), the subshell had no TRACE_FUNC
and set -e was not enforced on the pipeline failure -- the sha256sum
error was silently masked by print_tree returning 0, the subshell
exited 0, and a partial hash file was signed.  After 1f6a975
added TRACE_FUNC as the first command in the subshell, set -e
became enforced, making the same pipeline failure fatally abort
the subshell before print_tree could mask it.  In both cases the
re-hash produced the wrong result; the fix is to remove it.

The user re-saves the default boot entry from the boot menu to
regenerate kexec_default_hashes.txt with current paths.

Regression lineage:
- 2017         - file created without re-hash
- Mar 14, 2018 - re-hash added in update_checksums() inside gui-init
                 (Kyle Rankin, commit 769f6a7)
- Jul  5, 2019 - update_checksums() moved to /etc/functions
                 (Matt DeVillier, commit ed2f19d)
- Oct  8, 2020 - re-hash moved into kexec-sign-config under -u flag;
                 -u and -r passed together in a single call
                 (alex-nitrokey, commit 8bfae52)
- Feb  7, 2025 - TRACE_FUNC placed as first command inside the subshell --
                 this is where a stale-path sha256sum failure changes
                 from silently masked to fatally aborting
                 (Thierry Laurion, commit 1f6a975)

Mitigation on unpatched firmware (recovery shell):
  mount -o remount,rw /boot
  rm /boot/kexec_default_hashes.txt
  mount -o remount,ro /boot
Then re-run Update checksums.  With the file gone, the -e guard
skips the re-hash entirely.  Then set a new default from the boot
menu to regenerate kexec_default_hashes.txt with current paths.

Signed-off-by: Thierry Laurion <insurgo@riseup.net>
@tlaurion tlaurion force-pushed the fix-stale-rehash-kexec-sign-config branch from a3e7ab0 to d16641e Compare July 7, 2026 20:24
@tlaurion tlaurion requested a review from Copilot July 7, 2026 20:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 7 changed files in this pull request and generated 1 comment.

Comment thread initrd/etc/luks-functions.sh Outdated
Inside bash (... ) subshells, set -e does not reliably abort on
pipeline failures -- enforcement depends on the type of the first
command in the subshell (builtin vs function call).  A later command
like print_tree that always returns 0 masks the pipeline failure
exit code, the subshell exits 0, and the outer [ $? -eq 0 ] || DIE
never triggers.  The caller receives a successful exit with partial
or corrupted output.

This is a known bug in bash 5.1's execute_cmd.c: when the first
command in a (... ) subshell is a builtin (cd), the executing subshell
path pollutes the builtin_ignoring_errexit state, causing
exit_immediately_on_error to be set to 0.  Subsequent pipeline
failures are silently ignored.  When the first command is a function
call, the execute_shell_function() path uses its own setjmp/longjmp
context that does not touch the errexit state, so pipeline failures
are caught normally.  This was fixed in bash 5.2 (optimize_subshell_
command optimization) and documented in BashFAQ/105.

Fix by explicitly capturing the pipeline exit code with

    pipeline_exit=0
    cmd | other_cmd || pipeline_exit=$?
    exit $pipeline_exit

inside each affected subshell, so the exit code propagates to the
outer guard regardless of set -e reliability.

Files changed:

  initrd/bin/oem-factory-reset.sh
    - hash generation subshell: find|xargs pipeline exit captured
      before print_tree masks it

  initrd/bin/root-hashes-gui.sh
    - root hash computation subshell: find|xargs pipeline exit captured

  initrd/bin/kexec-insert-key.sh
    - initramfs cpio archive subshell: find|cpio pipeline exit captured

  initrd/bin/inject_firmware.sh
    - firmware initrd injection subshell: find|cpio pipeline exit captured

  initrd/bin/kexec-save-default.sh
    - default boot hash creation subshell: kexec-boot|xargs pipeline
      exit captured

  initrd/etc/luks-functions.sh
    - DRK keyslot detection: process substitution <(cryptsetup|grep|sed)
      silently loses the pipeline exit code.  Replaced with an explicit
      $(cryptsetup ...) || continue check that warns and skips the
      container on failure.

Signed-off-by: Thierry Laurion <insurgo@riseup.net>
@tlaurion tlaurion force-pushed the fix-stale-rehash-kexec-sign-config branch from d16641e to 6ebc734 Compare July 7, 2026 20:31
@tlaurion tlaurion requested a review from Copilot July 7, 2026 20:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 7 changed files in this pull request and generated no new comments.

@tlaurion tlaurion marked this pull request as ready for review July 7, 2026 20:41

@marmarek marmarek left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks okay, and also I confirm it works with 4.19.4 -> 4.19.5 update: https://openqa.qubes-os.org/tests/187684#step/startup/5

@tlaurion

tlaurion commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Looks okay, and also I confirm it works with 4.19.4 -> 4.19.5 update: https://openqa.qubes-os.org/tests/187684#step/startup/5

Thanks a lot @marmarek.

@tlaurion tlaurion merged commit 37076d0 into linuxboot:master Jul 8, 2026
55 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug feature freeze - 2025-01-30 (originally targeted 2024-11-20) upstream

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants