Preserve patched_at across the post-restart re-mount

create_task.py verify decides "loaded" by comparing middlewared's start time
against hook_status.json's patched_at. The post-restart re-mount restores the
same patch the boot pass already applied, but it runs after the restart — so
letting apply.sh re-stamp made patched_at newer than the process that had
correctly imported the patch, and verify reported FAIL while everything was
working. That would have fired on every boot where docker's nvidia sysext
merge detaches the overlay.

Found on hardware while exercising the candidate: a new lying status
introduced by the fix for a lying status. The snapshot lives in /run, not the
repo — a leftover file there would leave the tree dirty, which update.sh
refuses to run over.
This commit is contained in:
2026-08-26 05:20:23 +00:00
parent 3915f92dec
commit 118d172367
3 changed files with 52 additions and 1 deletions
+8 -1
View File
@@ -76,7 +76,14 @@ worse than no alert, because one day it carries a security fix.
straight back into the same race. So the overlay is re-mounted for the benefit
of the next restart, and the question of whether *this* middlewared actually
holds the patch is left to the one thing that can answer it exactly — the
in-process alert below.
in-process alert below. That re-mount preserves `hook_status.json`'s
`patched_at`: `create_task.py verify` decides "loaded" by comparing
middlewared's start time against that stamp, so a re-apply running *after* the
restart would have made the stamp newer than the process which correctly
imported the patch, and `verify` would have reported FAIL forever on every
boot where the sysext merge detaches the overlay. Caught on hardware while
validating the candidate — a new lying status introduced by the fix for a
lying status.
Two supporting fixes fell out of the same failure. `_ensure_writable` treated
"one of our overlays is listed on this directory" as "already done" — but it
+19
View File
@@ -143,7 +143,26 @@ else
_log "nvidia sysext merge follows it) -- re-mounting for the next restart."
_log "Whether THIS middlewared loaded the patch is answered in-process by"
_log "the 'installed but NOT loaded' alert, not by this check."
# Preserve hook_status.json's patched_at across this re-mount.
#
# create_task.py verify decides "loaded" by comparing middlewared's start
# time against patched_at. This re-apply restores the SAME patch the boot
# pass already applied, but it runs *after* the restart -- so letting it
# re-stamp would make patched_at newer than the process that correctly
# imported the patch, and verify would report FAIL forever, on every boot
# where docker's sysext merge detaches the overlay. That is precisely the
# lying-status failure this release exists to remove, so do not introduce a
# new one. The snapshot lives in /run, never in the repo: a leftover file
# there would leave the tree dirty and update.sh refuses to run over that.
_saved_status=/run/truecloud-hook_status.pre
cp -p "$PATCH_DIR/hook_status.json" "$_saved_status" 2>/dev/null
TRUECLOUD_REAPPLY=1 /bin/bash "$PATCH_DIR/patch/apply.sh"
if [ -f "$_saved_status" ]; then
mv -f "$_saved_status" "$PATCH_DIR/hook_status.json" 2>/dev/null
fi
fi
_log "=== deferred restart complete ==="
+25
View File
@@ -172,3 +172,28 @@ def test_mount_retries_on_a_private_workdir():
body = src[start:end]
assert body.count("mount -t overlay") == 2, "expected a retry mount"
assert 'work="/run/truecloud-${tag}-work.$$"' in body
def test_post_restart_remount_preserves_the_patched_at_stamp():
"""create_task.py verify compares middlewared's start time to patched_at.
The post-restart re-mount restores the same patch the boot pass applied, so
letting apply.sh re-stamp would make patched_at newer than the process that
correctly imported it -- verify would then report FAIL forever on every boot
where docker's sysext merge detaches the overlay.
"""
src = wait_restart_source()
tail = src[src.index("systemctl try-restart middlewared"):]
assert "hook_status.json" in tail
save = tail.index("/run/truecloud-hook_status.pre")
reapply = tail.index("TRUECLOUD_REAPPLY=1")
restore = tail.rindex("hook_status.json")
assert save < reapply < restore, "snapshot must bracket the re-apply"
def test_status_snapshot_is_not_written_into_the_repo():
"""A leftover file in the repo dir leaves the tree dirty, and update.sh
refuses to run over a dirty tree -- that once made the patch un-updatable."""
src = wait_restart_source()
assert "/run/truecloud-hook_status.pre" in src
assert '"$PATCH_DIR/hook_status.json.pre' not in src