sysupdate.d: Add way to drop binaries into $BOOT
authorAdrian Vovk <adrianvovk@gmail.com>
Fri, 26 May 2023 04:47:47 +0000 (00:47 -0400)
committerLennart Poettering <lennart@poettering.net>
Sat, 3 Jun 2023 07:13:27 +0000 (09:13 +0200)
As described in the BLS, we should place binaries into the XBOOTLDR
directory if it is available, otherwise into the ESP. Thus, we might
need to put binaries into /boot or into /efi depending on the existence
of the XBOOTLDR partition.

With this change, we introduce a new PathRelativeTo= config option that
makes this functionality possible

man/sysupdate.d.xml
src/sysupdate/sysupdate-resource.c
src/sysupdate/sysupdate-resource.h
src/sysupdate/sysupdate-transfer.c
test/units/testsuite-72.sh

index c4cdd7971b877730b382da0e11097b0c375b1e45..260c260f9886ac1209c2902a7acef66322ae77fa 100644 (file)
@@ -81,8 +81,8 @@
 
       <listitem><para>Finally, a file <literal>https://download.example.com/foobarOS_47.efi.xz</literal> (a
       unified kernel, as per <ulink url="https://uapi-group.org/specifications/specs/boot_loader_specification">Boot Loader
-      Specification</ulink> Type #2) should be downloaded, decompressed and written to the ESP file system,
-      i.e. to <filename>EFI/Linux/foobarOS_47.efi</filename> in the ESP.</para></listitem>
+      Specification</ulink> Type #2) should be downloaded, decompressed and written to the $BOOT file system,
+      i.e. to <filename>EFI/Linux/foobarOS_47.efi</filename> in the ESP or XBOOTLDR partition.</para></listitem>
     </orderedlist>
 
     <para>The version-independent generalization of this would be (using the special marker
@@ -98,7 +98,7 @@
       set to <literal>foobarOS_@v_verity</literal>.</para></listitem>
 
       <listitem><para>A transfer of a file <literal>https://download.example.com/foobarOS_@v.efi.xz</literal>
-      → a local file <filename>/efi/EFI/Linux/foobarOS_@v.efi</filename>.</para></listitem>
+      → a local file <filename>$BOOT/EFI/Linux/foobarOS_@v.efi</filename>.</para></listitem>
     </orderedlist>
 
     <para>An update can only complete if the relevant URLs provide their resources for the same version,
         <citerefentry><refentrytitle>systemd-repart</refentrytitle><manvolnum>8</manvolnum></citerefentry>.</para></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><varname>PathRelativeTo=</varname></term>
+
+        <listitem><para>Specifies what partition <varname>Path=</varname> should be relative to. Takes one of
+        <constant>root</constant>, <constant>esp</constant>, <constant>xbootldr</constant>, or <constant>boot</constant>.
+        If unspecified, defaults to <constant>root</constant>.</para>
+
+        <para>If set to <constant>boot</constant>, the specified <varname>Path=</varname> will be resolved
+        relative to the mount point of the $BOOT partition (i.e. the ESP or XBOOTLDR), as defined by the
+        <ulink url="https://uapi-group.org/specifications/specs/boot_loader_specification">Boot Loader
+        Specification</ulink>.</para>
+
+        <para>The values <constant>esp</constant>, <constant>xbootldr</constant>, and
+        <constant>boot</constant> are only supported when <varname>Type=</varname> is set to
+        <constant>regular-file</constant> or <constant>directory</constant>.</para></listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><varname>MatchPattern=</varname></term>
 
@@ -820,7 +837,8 @@ MatchPattern=foobarOS_@v.efi.xz
 
 [Target]
 Type=regular-file
-Path=/efi/EFI/Linux
+Path=/EFI/Linux
+PathRelativeTo=boot
 MatchPattern=foobarOS_@v+@l-@d.efi \
              foobarOS_@v+@l.efi \
              foobarOS_@v.efi
@@ -829,12 +847,12 @@ TriesLeft=3
 TriesDone=0
 InstancesMax=2</programlisting></para>
 
-        <para>The above installs a unified kernel image into the ESP (which is mounted to
-        <filename>/efi/</filename>), as per <ulink url="https://uapi-group.org/specifications/specs/boot_loader_specification">Boot
-        Loader Specification</ulink> Type #2. This defines three possible patterns for the names of the
-        kernel images, as per <ulink url="https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT">Automatic Boot
-        Assessment</ulink>, and ensures when installing new kernels, they are set up with 3 tries left. No
-        more than two parallel kernels are kept.</para>
+        <para>The above installs a unified kernel image into the $BOOT partition, as per
+        <ulink url="https://uapi-group.org/specifications/specs/boot_loader_specification">Boot Loader
+        Specification</ulink> Type #2. This defines three possible patterns for the names of the kernel
+        images, as per <ulink url="https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT">Automatic Boot Assessment</ulink>,
+        and ensures when installing new kernels, they are set up with 3 tries left. No more than two parallel
+        kernels are kept.</para>
 
         <para>With this setup the web server would serve the following files, for a hypothetical version 7 of
         the OS:</para>
index 201b37528a877d2acc90bd571e53e99cf4ff342f..30973a360dffc5e247dc77dad060972fbd1a7b92 100644 (file)
@@ -13,6 +13,7 @@
 #include "env-util.h"
 #include "fd-util.h"
 #include "fileio.h"
+#include "find-esp.h"
 #include "glyph-util.h"
 #include "gpt.h"
 #include "hexdecoct.h"
@@ -520,6 +521,12 @@ int resource_resolve_path(
 
         assert(rr);
 
+        if (IN_SET(rr->path_relative_to, PATH_RELATIVE_TO_ESP, PATH_RELATIVE_TO_XBOOTLDR, PATH_RELATIVE_TO_BOOT) &&
+            !IN_SET(rr->type, RESOURCE_REGULAR_FILE, RESOURCE_DIRECTORY))
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                       "Paths relative to %s are only allowed for regular-file or directory resources.",
+                                       path_relative_to_to_string(rr->path_relative_to));
+
         if (rr->path_auto) {
                 struct stat orig_root_stats;
 
@@ -595,12 +602,31 @@ int resource_resolve_path(
 
                 r = get_block_device_harder_fd(fd, &d);
 
-        } else if (RESOURCE_IS_FILESYSTEM(rr->type) && root) {
-                _cleanup_free_ char *resolved = NULL;
+        } else if (RESOURCE_IS_FILESYSTEM(rr->type)) {
+                _cleanup_free_ char *resolved = NULL, *relative_to = NULL;
+                ChaseFlags chase_flags = CHASE_PREFIX_ROOT;
+
+                if (rr->path_relative_to == PATH_RELATIVE_TO_ROOT) {
+                        relative_to = strdup(empty_to_root(root));
+                        if (!relative_to)
+                                return log_oom();
+                } else { /* boot, esp, or xbootldr */
+                        r = 0;
+                        if (IN_SET(rr->path_relative_to, PATH_RELATIVE_TO_BOOT, PATH_RELATIVE_TO_XBOOTLDR))
+                                r = find_xbootldr_and_warn(root, NULL, /* unprivileged_mode= */ -1, &relative_to, NULL, NULL);
+                        if (r == -ENOKEY || rr->path_relative_to == PATH_RELATIVE_TO_ESP)
+                                r = find_esp_and_warn(root, NULL, -1, &relative_to, NULL, NULL, NULL, NULL, NULL);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to resolve $BOOT: %m");
+                        log_debug("Resolved $BOOT to '%s'", relative_to);
+
+                        /* Since this partition is read from EFI, there should be no symlinks */
+                        chase_flags |= CHASE_PROHIBIT_SYMLINKS;
+                }
 
-                r = chase(rr->path, root, CHASE_PREFIX_ROOT, &resolved, NULL);
+                r = chase(rr->path, relative_to, chase_flags, &resolved, NULL);
                 if (r < 0)
-                        return log_error_errno(r, "Failed to resolve '%s': %m", rr->path);
+                        return log_error_errno(r, "Failed to resolve '%s' (relative to '%s'): %m", rr->path, relative_to);
 
                 free_and_replace(rr->path, resolved);
                 return 0;
@@ -641,3 +667,12 @@ static const char *resource_type_table[_RESOURCE_TYPE_MAX] = {
 };
 
 DEFINE_STRING_TABLE_LOOKUP(resource_type, ResourceType);
+
+static const char *path_relative_to_table[_PATH_RELATIVE_TO_MAX] = {
+        [PATH_RELATIVE_TO_ROOT]     = "root",
+        [PATH_RELATIVE_TO_ESP]      = "esp",
+        [PATH_RELATIVE_TO_XBOOTLDR] = "xbootldr",
+        [PATH_RELATIVE_TO_BOOT]     = "boot",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(path_relative_to, PathRelativeTo);
index 3209988c24fcd801e2f88d0b2f877a416f39dd9c..8bf19a45ec31d31e313fd75f66b3299622d44137 100644 (file)
@@ -66,12 +66,24 @@ static inline bool RESOURCE_IS_URL(ResourceType t) {
                       RESOURCE_URL_FILE);
 }
 
+typedef enum PathRelativeTo {
+        /* Please make sure to folow the naming of the corresponding PartitionDesignator enum values,
+         * where this makes sense, like for the following three. */
+        PATH_RELATIVE_TO_ROOT,
+        PATH_RELATIVE_TO_ESP,
+        PATH_RELATIVE_TO_XBOOTLDR,
+        PATH_RELATIVE_TO_BOOT, /* Refers to $BOOT from the BLS. No direct counterpart in PartitionDesignator */
+        _PATH_RELATIVE_TO_MAX,
+        _PATH_RELATIVE_TO_INVALID = -EINVAL,
+} PathRelativeTo;
+
 struct Resource {
         ResourceType type;
 
         /* Where to look for instances, and what to match precisely */
         char *path;
         bool path_auto; /* automatically find root path (only available if target resource, not source resource) */
+        PathRelativeTo path_relative_to;
         char **patterns;
         GptPartitionType partition_type;
         bool partition_type_set;
@@ -94,3 +106,6 @@ int resource_resolve_path(Resource *rr, const char *root, const char *node);
 
 ResourceType resource_type_from_string(const char *s) _pure_;
 const char *resource_type_to_string(ResourceType t) _const_;
+
+PathRelativeTo path_relative_to_from_string(const char *s) _pure_;
+const char *path_relative_to_to_string(PathRelativeTo r) _const_;
index f7009315a2e596bc7e73ff342cc32e68c3aa84ed..bbc3a5bcaa0475bad88fb069f00f04294636b1fb 100644 (file)
@@ -297,7 +297,6 @@ static int config_parse_resource_path(
                 const char *rvalue,
                 void *data,
                 void *userdata) {
-
         _cleanup_free_ char *resolved = NULL;
         Resource *rr = ASSERT_PTR(data);
         int r;
@@ -327,6 +326,9 @@ static int config_parse_resource_path(
 
 static DEFINE_CONFIG_PARSE_ENUM(config_parse_resource_type, resource_type, ResourceType, "Invalid resource type");
 
+static DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT(config_parse_resource_path_relto, path_relative_to, PathRelativeTo,
+                                             PATH_RELATIVE_TO_ROOT, "Invalid PathRelativeTo= value");
+
 static int config_parse_resource_ptype(
                 const char *unit,
                 const char *filename,
@@ -418,27 +420,29 @@ int transfer_read_definition(Transfer *t, const char *path) {
         assert(path);
 
         ConfigTableItem table[] = {
-                { "Transfer",    "MinVersion",              config_parse_min_version,          0, &t->min_version        },
-                { "Transfer",    "ProtectVersion",          config_parse_protect_version,      0, &t->protected_versions },
-                { "Transfer",    "Verify",                  config_parse_bool,                 0, &t->verify             },
-                { "Source",      "Type",                    config_parse_resource_type,        0, &t->source.type        },
-                { "Source",      "Path",                    config_parse_resource_path,        0, &t->source             },
-                { "Source",      "MatchPattern",            config_parse_resource_pattern,     0, &t->source.patterns    },
-                { "Target",      "Type",                    config_parse_resource_type,        0, &t->target.type        },
-                { "Target",      "Path",                    config_parse_resource_path,        0, &t->target             },
-                { "Target",      "MatchPattern",            config_parse_resource_pattern,     0, &t->target.patterns    },
-                { "Target",      "MatchPartitionType",      config_parse_resource_ptype,       0, &t->target             },
-                { "Target",      "PartitionUUID",           config_parse_partition_uuid,       0, t                      },
-                { "Target",      "PartitionFlags",          config_parse_partition_flags,      0, t                      },
-                { "Target",      "PartitionNoAuto",         config_parse_tristate,             0, &t->no_auto            },
-                { "Target",      "PartitionGrowFileSystem", config_parse_tristate,             0, &t->growfs             },
-                { "Target",      "ReadOnly",                config_parse_tristate,             0, &t->read_only          },
-                { "Target",      "Mode",                    config_parse_mode,                 0, &t->mode               },
-                { "Target",      "TriesLeft",               config_parse_uint64,               0, &t->tries_left         },
-                { "Target",      "TriesDone",               config_parse_uint64,               0, &t->tries_done         },
-                { "Target",      "InstancesMax",            config_parse_instances_max,        0, &t->instances_max      },
-                { "Target",      "RemoveTemporary",         config_parse_bool,                 0, &t->remove_temporary   },
-                { "Target",      "CurrentSymlink",          config_parse_current_symlink,      0, &t->current_symlink    },
+                { "Transfer",    "MinVersion",              config_parse_min_version,          0, &t->min_version             },
+                { "Transfer",    "ProtectVersion",          config_parse_protect_version,      0, &t->protected_versions      },
+                { "Transfer",    "Verify",                  config_parse_bool,                 0, &t->verify                  },
+                { "Source",      "Type",                    config_parse_resource_type,        0, &t->source.type             },
+                { "Source",      "Path",                    config_parse_resource_path,        0, &t->source                  },
+                { "Source",      "PathRelativeTo",          config_parse_resource_path_relto,  0, &t->source.path_relative_to },
+                { "Source",      "MatchPattern",            config_parse_resource_pattern,     0, &t->source.patterns         },
+                { "Target",      "Type",                    config_parse_resource_type,        0, &t->target.type             },
+                { "Target",      "Path",                    config_parse_resource_path,        0, &t->target                  },
+                { "Target",      "PathRelativeTo",          config_parse_resource_path_relto,  0, &t->target.path_relative_to },
+                { "Target",      "MatchPattern",            config_parse_resource_pattern,     0, &t->target.patterns         },
+                { "Target",      "MatchPartitionType",      config_parse_resource_ptype,       0, &t->target                  },
+                { "Target",      "PartitionUUID",           config_parse_partition_uuid,       0, t                           },
+                { "Target",      "PartitionFlags",          config_parse_partition_flags,      0, t                           },
+                { "Target",      "PartitionNoAuto",         config_parse_tristate,             0, &t->no_auto                 },
+                { "Target",      "PartitionGrowFileSystem", config_parse_tristate,             0, &t->growfs                  },
+                { "Target",      "ReadOnly",                config_parse_tristate,             0, &t->read_only               },
+                { "Target",      "Mode",                    config_parse_mode,                 0, &t->mode                    },
+                { "Target",      "TriesLeft",               config_parse_uint64,               0, &t->tries_left              },
+                { "Target",      "TriesDone",               config_parse_uint64,               0, &t->tries_done              },
+                { "Target",      "InstancesMax",            config_parse_instances_max,        0, &t->instances_max           },
+                { "Target",      "RemoveTemporary",         config_parse_bool,                 0, &t->remove_temporary        },
+                { "Target",      "CurrentSymlink",          config_parse_current_symlink,      0, &t->current_symlink         },
                 {}
         };
 
index 9effc982baef3def78fe21cda2b8bc6f131b83bb..63d1988a8499bb168240edaf9bab7fd7dae3997c 100755 (executable)
@@ -30,6 +30,7 @@ size=2048, type=2c7357ed-ebd2-46d9-aec1-23d437ec2bf5, name=_empty
 EOF
 
 rm -rf /var/tmp/72-dirs
+mkdir -p /var/tmp/72-dirs
 
 rm -rf /var/tmp/72-defs
 mkdir -p /var/tmp/72-defs
@@ -74,6 +75,30 @@ MatchPattern=dir-@v
 InstancesMax=3
 EOF
 
+cat >/var/tmp/72-defs/04-fourth.conf <<"EOF"
+[Source]
+Type=regular-file
+Path=/var/tmp/72-source
+MatchPattern=uki-@v.efi
+
+[Target]
+Type=regular-file
+Path=/EFI/Linux
+PathRelativeTo=boot
+MatchPattern=uki_@v+@l-@d.efi \
+             uki_@v+@l.efi \
+             uki_@v.efi
+Mode=0444
+TriesLeft=3
+TriesDone=0
+InstancesMax=2
+EOF
+
+rm -rf /var/tmp/72-esp /var/tmp/72-xbootldr
+mkdir -p /var/tmp/72-esp/EFI/Linux /var/tmp/72-xbootldr/EFI/Linux
+export SYSTEMD_ESP_PATH=/var/tmp/72-esp
+export SYSTEMD_XBOOTLDR_PATH=/var/tmp/72-xbootldr
+
 rm -rf /var/tmp/72-source
 mkdir -p /var/tmp/72-source
 
@@ -83,13 +108,16 @@ new_version() {
     dd if=/dev/urandom of="/var/tmp/72-source/part2-$1.raw" bs=1024 count=1024
     gzip -k -f "/var/tmp/72-source/part2-$1.raw"
 
+    # Create a random "UKI" payload
+    echo $RANDOM >"/var/tmp/72-source/uki-$1.efi"
+
+    # Create tarball of a directory
     mkdir -p "/var/tmp/72-source/dir-$1"
     echo $RANDOM >"/var/tmp/72-source/dir-$1/foo.txt"
     echo $RANDOM >"/var/tmp/72-source/dir-$1/bar.txt"
-
     tar --numeric-owner -C "/var/tmp/72-source/dir-$1/" -czf "/var/tmp/72-source/dir-$1.tar.gz" .
 
-    ( cd /var/tmp/72-source/ && sha256sum part* dir-*.tar.gz >SHA256SUMS )
+    ( cd /var/tmp/72-source/ && sha256sum uki* part* dir-*.tar.gz >SHA256SUMS )
 }
 
 update_now() {
@@ -103,8 +131,16 @@ update_now() {
 
 verify_version() {
     # Expects: version ID + sector offset of both partitions to compare
+
+    # Check the partitions
     dd if=/var/tmp/72-joined.raw bs=1024 skip="$2" count=1024 | cmp "/var/tmp/72-source/part1-$1.raw"
     dd if=/var/tmp/72-joined.raw bs=1024 skip="$3" count=1024 | cmp "/var/tmp/72-source/part2-$1.raw"
+
+    # Check the UKI
+    cmp "/var/tmp/72-source/uki-$1.efi" "/var/tmp/72-xbootldr/EFI/Linux/uki_$1+3-0.efi"
+    test -z "$(ls -A /var/tmp/72-esp/EFI/Linux)"
+
+    # Check the directories
     cmp "/var/tmp/72-source/dir-$1/foo.txt" /var/tmp/72-dirs/current/foo.txt
     cmp "/var/tmp/72-source/dir-$1/bar.txt" /var/tmp/72-dirs/current/bar.txt
 }
@@ -123,6 +159,7 @@ verify_version v2 2048 4096
 new_version v3
 update_now
 verify_version v3 1024 3072
+test ! -f "/var/tmp/72-xbootldr/EFI/Linux/uki_v1+3-0.efi"
 
 # Create fourth version, and update through a file:// URL. This should be
 # almost as good as testing HTTP, but is simpler for us to set up. file:// is
@@ -163,7 +200,7 @@ update_now
 verify_version v4 2048 4096
 
 rm  /var/tmp/72-joined.raw
-rm -r /var/tmp/72-dirs /var/tmp/72-defs /var/tmp/72-source
+rm -r /var/tmp/72-{dirs,defs,source,xbootldr,esp}
 
 echo OK >/testok