]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
updatectl: Show a helpful error if an update is partially downloaded
authorPhilip Withnall <pwithnall@gnome.org>
Wed, 22 Apr 2026 16:31:27 +0000 (17:31 +0100)
committerLuca Boccassi <luca.boccassi@gmail.com>
Fri, 22 May 2026 12:57:29 +0000 (13:57 +0100)
If an update is partially downloaded and the user tries to update again,
`updatectl` can’t currently do anything (it doesn’t yet support resuming
downloads). At the moment, though, it’ll return success as if the system
was up to date, even though it isn’t up to date.

Instead, print a more helpful error message telling the user to try
vacuuming the partial version and trying again.

I decided not to make it automatically vacuum the partial version, as
that seems like a way to get into a nasty retry loop if, for example,
the checksum provided by the server doesn’t match that of the downloaded
file (which is one way to trigger this code path).

Add an integration test which simulates this failure by corrupting the
`SHA256SUMS` file, trying to download an update, and then working
through the recovery steps.

Signed-off-by: Philip Withnall <pwithnall@gnome.org>
Fixes: https://github.com/systemd/systemd/issues/41502
(cherry picked from commit 66b950cd3f47f49087ddd4a2f4812e82b209f2b7)

src/sysupdate/sysupdate.c
src/sysupdate/updatectl.c
test/units/TEST-72-SYSUPDATE.sh

index 8989aa60e58265f7ee49f9bf42e0914282091605..328b7ef374594289ff241a22d41239bed6879917 100644 (file)
@@ -1033,9 +1033,7 @@ static int context_acquire(
         if (FLAGS_SET(us->flags, UPDATE_INCOMPLETE))
                 log_info("Selected update '%s' is already installed, but incomplete. Repairing.", us->version);
         else if (FLAGS_SET(us->flags, UPDATE_PARTIAL)) {
-                log_info("Selected update '%s' is already acquired and partially installed. Vacuum it to try installing again.", us->version);
-
-                return 0;
+                return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), "Selected update '%s' is already acquired and partially installed. Vacuum it to try installing again.", us->version);
         } else if (FLAGS_SET(us->flags, UPDATE_PENDING)) {
                 log_info("Selected update '%s' is already acquired and pending installation.", us->version);
 
index ed1335d172d2b981547f598af840ec102a42564a..9102185c59d9e40f76f15a84f70a82e8e2d302cf 100644 (file)
@@ -863,6 +863,10 @@ static int update_render_progress(sd_event_source *source, void *userdata) {
                         clear_progress_bar_unbuffered(target);
                         fprintf(stderr, "%s: %s Already up-to-date\n", target, GREEN_CHECK_MARK());
                         n--; /* Don't consider this target in the total */
+                } else if (progress == -EUCLEAN) {
+                        clear_progress_bar_unbuffered(target);
+                        fprintf(stderr, "%s: %s Update is already acquired and partially installed. Vacuum it to try installing again.\n", target, RED_CROSS_MARK());
+                        total += 100;
                 } else if (progress < 0) {
                         clear_progress_bar_unbuffered(target);
                         fprintf(stderr, "%s: %s %s\n", target, RED_CROSS_MARK(), STRERROR(progress));
index b929485bd5b6c1009b98500138694ea6e2ef8f21..8986a905c2ba0fbd01ea1ea0dae7ec34cdc2f755 100755 (executable)
@@ -66,6 +66,7 @@ update_checksums_with_best_before() {
 new_version() {
     local sector_size="${1:?}"
     local version="${2:?}"
+    local corrupt="${3:-}"
 
     # Create a pair of random partition payloads, and compress one.
     # To make not the initial bytes of part1-xxx.raw accidentally match one of the compression header,
@@ -90,11 +91,26 @@ new_version() {
     echo $RANDOM >"$WORKDIR/source/dir-$version/bar.txt"
     tar --numeric-owner -C "$WORKDIR/source/dir-$version/" -czf "$WORKDIR/source/dir-$version.tar.gz" .
 
-    update_checksums
+    if [[ "$corrupt" == "corrupt-checksum" ]]; then
+        # As requested, add a deliberately corrupt checksum for this file. This
+        # will get overwritten next time update_checksums() is called, but the
+        # integration test will probably have moved on to other things by then.
+        {
+            echo "abad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1dea  part1-$version.raw"
+            echo "abad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1dea  part2-$version.raw"
+            echo "abad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1dea  part2-$version.raw.gz"
+            echo "abad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1dea  uki-$version.efi"
+            echo "abad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1dea  uki-extra-$version.efi"
+            echo "abad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1dea  dir-$version.tar.gz"
+        } >> "$WORKDIR/source/SHA256SUMS"
+    else
+        update_checksums
+    fi
 }
 
 update_now() {
     local update_type="${1:?}"
+    local checks="${2:-}"
 
     # Update to newest version. First there should be an update ready, then we
     # do the update, and then there should not be any ready anymore
@@ -105,7 +121,10 @@ update_now() {
     # modes. Some updates in the test suite need to be monolithic (e.g. when
     # repairing an installation), so that can be overridden via the local.
 
-    "$SYSUPDATE" --verify=no check-new
+    if [[ "$checks" != "no-checks" ]]; then
+        "$SYSUPDATE" --verify=no check-new
+    fi
+
     if [[ "$update_type" == "monolithic" ]]; then
         "$SYSUPDATE" --verify=no update
     elif [[ "$update_type" == "split-offline" ]]; then
@@ -125,7 +144,10 @@ update_now() {
     else
         exit 1
     fi
-    (! "$SYSUPDATE" --verify=no check-new)
+
+    if [[ "$checks" != "no-checks" ]]; then
+        (! "$SYSUPDATE" --verify=no check-new)
+    fi
 }
 
 verify_version() {
@@ -460,6 +482,28 @@ EOF
     verify_version_current "$blockdev" "$sector_size" v8 1
     verify_version "$blockdev" "$sector_size" v7 2
 
+    # Create a 9th version but corrupt the checksum in SHA256SUMS so pulling it
+    # fails when verifying the checksum, in order to create a current+partial
+    # state. Try to update again and verify that this results in an error.
+    # Vacuum the partial version, regenerate it on the server, try updating
+    # again and it should succeed.
+    new_version "$sector_size" v9 "corrupt-checksum"
+    (! update_now "$update_type")
+    "$SYSUPDATE" --offline list v9 | grep "partial" >/dev/null
+    verify_version_current "$blockdev" "$sector_size" v8 1
+    # don’t verify the other part of the block device as it’s in an indeterminate state
+    (! update_now "$update_type" "no-checks") |& tee "$WORKDIR"/update_now-9
+    cat "$WORKDIR"/update_now-9
+    grep "is already acquired and partially installed. Vacuum it to try installing again." "$WORKDIR"/update_now-9
+    "$SYSUPDATE" --offline vacuum |& grep "Removing old partial" >/dev/null
+    verify_version_current "$blockdev" "$sector_size" v8 1
+    # don’t verify the other part of the block device as it’s in an indeterminate state
+    "$SYSUPDATE" --verify=no list v9 | grep "candidate" >/dev/null
+    new_version "$sector_size" v9
+    update_now "$update_type"
+    verify_version "$blockdev" "$sector_size" v8 1
+    verify_version_current "$blockdev" "$sector_size" v9 2
+
     # Cleanup
     [[ -b "$blockdev" ]] && losetup --detach "$blockdev"
     rm "$BACKING_FILE"