]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
sysupdate: Implement acquire and install steps for transfers
authorPhilip Withnall <pwithnall@gnome.org>
Wed, 31 Dec 2025 00:02:06 +0000 (00:02 +0000)
committerPhilip Withnall <pwithnall@gnome.org>
Mon, 9 Feb 2026 12:03:47 +0000 (12:03 +0000)
Instead of using a random temporary path for file transfers, use a
predictable one which indicates whether the transfer is partially
complete or pending installation. Similarly for partitions.

This is another step towards being able to split the ‘update’ step into
‘acquire’ and ‘install’.

Signed-off-by: Philip Withnall <pwithnall@gnome.org>
Helps: https://github.com/systemd/systemd/issues/34814

src/sysupdate/sysupdate-transfer.c
src/sysupdate/sysupdate-transfer.h

index 0d017f9043a85ebe91bcd6355c0c1804d94317bf..6d1262c1612fb2b6b7748ab4619c3b6998cdc977 100644 (file)
@@ -41,7 +41,6 @@
 #include "sysupdate-resource.h"
 #include "sysupdate-transfer.h"
 #include "time-util.h"
-#include "tmpfile-util.h"
 #include "web-util.h"
 
 /* Default value for InstancesMax= for fs object targets */
@@ -51,7 +50,8 @@ Transfer* transfer_free(Transfer *t) {
         if (!t)
                 return NULL;
 
-        t->temporary_path = rm_rf_subvolume_and_free(t->temporary_path);
+        free(t->temporary_partial_path);
+        free(t->temporary_pending_path);
 
         free(t->id);
 
@@ -67,6 +67,9 @@ Transfer* transfer_free(Transfer *t) {
         strv_free(t->appstream);
 
         partition_info_destroy(&t->partition_info);
+        free(t->temporary_partial_partition_label);
+        free(t->temporary_pending_partition_label);
+        free(t->final_partition_label);
 
         resource_destroy(&t->source);
         resource_destroy(&t->target);
@@ -767,6 +770,7 @@ static int transfer_instance_vacuum(
                 /* label "_empty" means "no contents" for our purposes */
                 pinfo.label = (char*) "_empty";
 
+                log_debug("Relabelling partition '%s' to '%s'.", pinfo.device, pinfo.label);
                 r = patch_partition(t->target.path, &pinfo, PARTITION_LABEL);
                 if (r < 0)
                         return r;
@@ -1128,6 +1132,8 @@ static int run_callout(
 
 int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, void *userdata) {
         _cleanup_free_ char *formatted_pattern = NULL, *digest = NULL;
+        _cleanup_free_ char *formatted_partial_pattern = NULL;
+        _cleanup_free_ char *formatted_pending_pattern = NULL;
         char offset[DECIMAL_STR_MAX(uint64_t)+1], max_size[DECIMAL_STR_MAX(uint64_t)+1];
         const char *where = NULL;
         InstanceMetadata f;
@@ -1147,7 +1153,11 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
         }
 
         assert(!t->final_path);
-        assert(!t->temporary_path);
+        assert(!t->temporary_partial_path);
+        assert(!t->temporary_pending_path);
+        assert(!t->final_partition_label);
+        assert(!t->temporary_partial_partition_label);
+        assert(!t->temporary_pending_partition_label);
         assert(!strv_isempty(t->target.patterns));
 
         /* Format the target name using the first pattern specified */
@@ -1157,6 +1167,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
                 return log_error_errno(r, "Failed to format target pattern: %m");
 
         if (RESOURCE_IS_FILESYSTEM(t->target.type)) {
+                _cleanup_free_ char *final_dir = NULL, *final_filename = NULL, *partial_filename = NULL, *pending_filename = NULL;
 
                 if (!path_is_safe(formatted_pattern))
                         return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as file name, refusing: %s", formatted_pattern);
@@ -1169,9 +1180,37 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
                 if (r < 0)
                         return log_error_errno(r, "Cannot create target directory: %m");
 
-                r = tempfn_random(t->final_path, "sysupdate", &t->temporary_path);
+                /* Build the paths for the partial and pending files, which hold the resource while it’s
+                 * being acquired and after it’s been acquired (but before it’s moved to the final_path
+                 * when it’s installed).
+                 *
+                 * Split the filename off the `final_path`, then add a prefix to it for each of partial and
+                 * pending, then join them back on to the same directory. */
+                r = path_split_prefix_filename(t->final_path, &final_dir, &final_filename);
                 if (r < 0)
-                        return log_error_errno(r, "Failed to generate temporary target path: %m");
+                        return log_error_errno(r, "Failed to parse path: %m");
+
+                if (!strprepend(&partial_filename, ".sysupdate.partial.", final_filename))
+                        return log_oom();
+
+                if (!strprepend(&pending_filename, ".sysupdate.pending.", final_filename))
+                        return log_oom();
+
+                t->temporary_partial_path = path_join(final_dir, partial_filename);
+                if (!t->temporary_partial_path)
+                        return log_oom();
+
+                r = mkdir_parents(t->temporary_partial_path, 0755);
+                if (r < 0)
+                        return log_error_errno(r, "Cannot create target directory: %m");
+
+                t->temporary_pending_path = path_join(final_dir, pending_filename);
+                if (!t->temporary_pending_path)
+                        return log_oom();
+
+                r = mkdir_parents(t->temporary_pending_path, 0755);
+                if (r < 0)
+                        return log_error_errno(r, "Cannot create target directory: %m");
 
                 where = t->final_path;
         }
@@ -1183,6 +1222,28 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
                 if (!r)
                         return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as GPT partition label, refusing: %s", formatted_pattern);
 
+                if (!strprepend(&formatted_partial_pattern, "PRT#", formatted_pattern))
+                        return log_oom();
+                r = gpt_partition_label_valid(formatted_partial_pattern);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to determine if formatted pattern is suitable as GPT partition label: %s", formatted_partial_pattern);
+                if (!r)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as GPT partition label, refusing: %s", formatted_partial_pattern);
+
+                free_and_replace(t->temporary_partial_partition_label, formatted_partial_pattern);
+
+                if (!strprepend(&formatted_pending_pattern, "PND#", formatted_pattern))
+                        return log_oom();
+                r = gpt_partition_label_valid(formatted_pending_pattern);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to determine if formatted pattern is suitable as GPT partition label: %s", formatted_pending_pattern);
+                if (!r)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as GPT partition label, refusing: %s", formatted_pending_pattern);
+
+                free_and_replace(t->temporary_pending_partition_label, formatted_pending_pattern);
+
+                t->final_partition_label = TAKE_PTR(formatted_pattern);
+
                 r = find_suitable_partition(
                                 t->target.path,
                                 i->metadata.size,
@@ -1195,6 +1256,20 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
                 xsprintf(max_size, "%" PRIu64, t->partition_info.size);
 
                 where = t->partition_info.device;
+
+                /* Rename the partition to `PRT#<VERSION>` to indicate that a transfer to it is in progress. */
+                r = free_and_strdup_warn(&t->partition_info.label, t->temporary_partial_partition_label);
+                if (r < 0)
+                        return r;
+                t->partition_change = PARTITION_LABEL;
+
+                log_debug("Relabelling partition '%s' to '%s'.", t->partition_info.device, t->partition_info.label);
+                r = patch_partition(
+                                t->target.path,
+                                &t->partition_info,
+                                t->partition_change);
+                if (r < 0)
+                        return r;
         }
 
         assert(where);
@@ -1233,7 +1308,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
                                                "--direct",          /* just copy/unpack the specified file, don't do anything else */
                                                arg_sync ? "--sync=yes" : "--sync=no",
                                                i->path,
-                                               t->temporary_path),
+                                               t->temporary_partial_path),
                                         t, i, cb, userdata);
                         break;
 
@@ -1274,7 +1349,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
                                        arg_sync ? "--sync=yes" : "--sync=no",
                                        t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no",
                                        i->path,
-                                       t->temporary_path),
+                                       t->temporary_partial_path),
                                 t, i, cb, userdata);
                 break;
 
@@ -1291,7 +1366,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
                                        arg_sync ? "--sync=yes" : "--sync=no",
                                        t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no",
                                        i->path,
-                                       t->temporary_path),
+                                       t->temporary_partial_path),
                                 t, i, cb, userdata);
                 break;
 
@@ -1311,7 +1386,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
                                                "--verify", digest,  /* validate by explicit SHA256 sum */
                                                arg_sync ? "--sync=yes" : "--sync=no",
                                                i->path,
-                                               t->temporary_path),
+                                               t->temporary_partial_path),
                                         t, i, cb, userdata);
                         break;
 
@@ -1351,7 +1426,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
                                        t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no",
                                        arg_sync ? "--sync=yes" : "--sync=no",
                                        i->path,
-                                       t->temporary_path),
+                                       t->temporary_partial_path),
                                 t, i, cb, userdata);
                 break;
 
@@ -1363,7 +1438,8 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
 
         if (RESOURCE_IS_FILESYSTEM(t->target.type)) {
                 bool need_sync = false;
-                assert(t->temporary_path);
+                assert(t->temporary_partial_path);
+                assert(t->temporary_pending_path);
 
                 /* Apply file attributes if set */
                 if (f.mtime != USEC_INFINITY) {
@@ -1371,8 +1447,8 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
 
                         timespec_store(&ts, f.mtime);
 
-                        if (utimensat(AT_FDCWD, t->temporary_path, (struct timespec[2]) { ts, ts }, AT_SYMLINK_NOFOLLOW) < 0)
-                                return log_error_errno(errno, "Failed to adjust mtime of '%s': %m", t->temporary_path);
+                        if (utimensat(AT_FDCWD, t->temporary_partial_path, (struct timespec[2]) { ts, ts }, AT_SYMLINK_NOFOLLOW) < 0)
+                                return log_error_errno(errno, "Failed to adjust mtime of '%s': %m", t->temporary_partial_path);
 
                         need_sync = true;
                 }
@@ -1381,9 +1457,9 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
                         /* Try with AT_SYMLINK_NOFOLLOW first, because it's the safe thing to do. Older
                          * kernels don't support that however, in that case we fall back to chmod(). Not as
                          * safe, but shouldn't be a problem, given that we don't create symlinks here. */
-                        if (fchmodat(AT_FDCWD, t->temporary_path, f.mode, AT_SYMLINK_NOFOLLOW) < 0 &&
-                            (!ERRNO_IS_NOT_SUPPORTED(errno) || chmod(t->temporary_path, f.mode) < 0))
-                                return log_error_errno(errno, "Failed to adjust mode of '%s': %m", t->temporary_path);
+                        if (fchmodat(AT_FDCWD, t->temporary_partial_path, f.mode, AT_SYMLINK_NOFOLLOW) < 0 &&
+                            (!ERRNO_IS_NOT_SUPPORTED(errno) || chmod(t->temporary_partial_path, f.mode) < 0))
+                                return log_error_errno(errno, "Failed to adjust mode of '%s': %m", t->temporary_partial_path);
 
                         need_sync = true;
                 }
@@ -1391,20 +1467,34 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
                 /* Synchronize */
                 if (arg_sync && need_sync) {
                         if (t->target.type == RESOURCE_REGULAR_FILE)
-                                r = fsync_path_and_parent_at(AT_FDCWD, t->temporary_path);
+                                r = fsync_path_and_parent_at(AT_FDCWD, t->temporary_partial_path);
                         else {
                                 assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME));
-                                r = syncfs_path(AT_FDCWD, t->temporary_path);
+                                r = syncfs_path(AT_FDCWD, t->temporary_partial_path);
                         }
                         if (r < 0)
-                                return log_error_errno(r, "Failed to synchronize file system backing '%s': %m", t->temporary_path);
+                                return log_error_errno(r, "Failed to synchronize file system backing '%s': %m", t->temporary_partial_path);
                 }
 
                 t->install_read_only = f.read_only;
+
+                /* Rename the file from `.sysupdate.partial.<VERSION>` to `.sysupdate.pending.<VERSION>` to indicate it’s ready to install. */
+                log_debug("Renaming resource instance '%s' to '%s'.", t->temporary_partial_path, t->temporary_pending_path);
+                r = install_file(AT_FDCWD, t->temporary_partial_path,
+                                 AT_FDCWD, t->temporary_pending_path,
+                                 INSTALL_REPLACE|
+                                 (t->install_read_only > 0 ? INSTALL_READ_ONLY : 0)|
+                                 (t->target.type == RESOURCE_REGULAR_FILE ? INSTALL_FSYNC_FULL : INSTALL_SYNCFS));
+                if (r < 0)
+                        return log_error_errno(r, "Failed to move '%s' into pending place: %m", t->temporary_pending_path);
         }
 
         if (t->target.type == RESOURCE_PARTITION) {
-                free_and_replace(t->partition_info.label, formatted_pattern);
+                /* Now rename the partition again to `PND#<VERSION>` to indicate that the acquire is complete
+                 * and the partition is ready for install. */
+                r = free_and_strdup_warn(&t->partition_info.label, t->temporary_pending_partition_label);
+                if (r < 0)
+                        return r;
                 t->partition_change = PARTITION_LABEL;
 
                 if (f.partition_uuid_set) {
@@ -1431,6 +1521,14 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
                         t->partition_info.growfs = f.growfs;
                         t->partition_change |= PARTITION_GROWFS;
                 }
+
+                log_debug("Relabelling partition '%s' to '%s'.", t->partition_info.device, t->partition_info.label);
+                r = patch_partition(
+                                t->target.path,
+                                &t->partition_info,
+                                t->partition_change);
+                if (r < 0)
+                        return r;
         }
 
         /* For regular file cases the only step left is to install the file in place, which install_file()
@@ -1451,13 +1549,13 @@ int transfer_install_instance(
         assert(t);
         assert(i);
         assert(i->resource);
-        assert(t == container_of(i->resource, Transfer, source));
+        assert(i->is_pending || t == container_of(i->resource, Transfer, source));
 
-        if (t->temporary_path) {
+        if (t->temporary_pending_path) {
                 assert(RESOURCE_IS_FILESYSTEM(t->target.type));
                 assert(t->final_path);
 
-                r = install_file(AT_FDCWD, t->temporary_path,
+                r = install_file(AT_FDCWD, t->temporary_pending_path,
                                  AT_FDCWD, t->final_path,
                                  INSTALL_REPLACE|
                                  (t->install_read_only > 0 ? INSTALL_READ_ONLY : 0)|
@@ -1471,11 +1569,17 @@ int transfer_install_instance(
                          t->final_path,
                          resource_type_to_string(t->target.type));
 
-                t->temporary_path = mfree(t->temporary_path);
+                t->temporary_pending_path = mfree(t->temporary_pending_path);
         }
 
-        if (t->partition_change != 0) {
+        if (t->temporary_pending_partition_label) {
                 assert(t->target.type == RESOURCE_PARTITION);
+                assert(t->final_partition_label);
+
+                r = free_and_strdup_warn(&t->partition_info.label, t->final_partition_label);
+                if (r < 0)
+                        return r;
+                t->partition_change = PARTITION_LABEL;
 
                 r = patch_partition(
                                 t->target.path,
index 1a6cba804fcc0d0537fe9e0d30483cb25d0c2417..92c19227953abae580632a7aff054d53da867065 100644 (file)
@@ -39,13 +39,17 @@ typedef struct Transfer {
         int growfs;
 
         /* If we create a new file/dir/subvol in the fs, the temporary and final path we create it under, as well as the read-only flag for it */
-        char *temporary_path;
+        char *temporary_partial_path;
+        char *temporary_pending_path;
         char *final_path;
         int install_read_only;
 
         /* If we write to a partition in a partition table, the metrics of it */
         PartitionInfo partition_info;
         PartitionChange partition_change;
+        char *final_partition_label;
+        char *temporary_partial_partition_label;
+        char *temporary_pending_partition_label;
 
         Context *context;
 } Transfer;