summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--NEWS2
-rw-r--r--TODO17
-rw-r--r--man/loader.conf.xml7
-rw-r--r--man/rules/meson.build8
-rw-r--r--man/systemd-boot.xml25
-rw-r--r--man/systemd-sysupdate.xml287
-rw-r--r--man/sysupdate.d.xml885
-rw-r--r--meson.build30
-rw-r--r--meson_options.txt2
-rw-r--r--src/basic/strv.h4
-rw-r--r--src/sysupdate/meson.build22
-rw-r--r--src/sysupdate/sysupdate-cache.c88
-rw-r--r--src/sysupdate/sysupdate-cache.h18
-rw-r--r--src/sysupdate/sysupdate-instance.c63
-rw-r--r--src/sysupdate/sysupdate-instance.h67
-rw-r--r--src/sysupdate/sysupdate-partition.c379
-rw-r--r--src/sysupdate/sysupdate-partition.h49
-rw-r--r--src/sysupdate/sysupdate-pattern.c602
-rw-r--r--src/sysupdate/sysupdate-pattern.h12
-rw-r--r--src/sysupdate/sysupdate-resource.c633
-rw-r--r--src/sysupdate/sysupdate-resource.h97
-rw-r--r--src/sysupdate/sysupdate-transfer.c1247
-rw-r--r--src/sysupdate/sysupdate-transfer.h62
-rw-r--r--src/sysupdate/sysupdate-update-set.c63
-rw-r--r--src/sysupdate/sysupdate-update-set.h32
-rw-r--r--src/sysupdate/sysupdate-util.c17
-rw-r--r--src/sysupdate/sysupdate-util.h6
-rw-r--r--src/sysupdate/sysupdate.c1411
-rw-r--r--src/sysupdate/sysupdate.h21
-rw-r--r--src/test/test-strv.c7
l---------test/TEST-72-SYSUPDATE/Makefile1
-rwxr-xr-xtest/TEST-72-SYSUPDATE/test.sh16
-rw-r--r--test/units/testsuite-72.service8
-rwxr-xr-xtest/units/testsuite-72.sh170
-rw-r--r--units/meson.build4
-rw-r--r--units/systemd-sysupdate-reboot.service.in20
-rw-r--r--units/systemd-sysupdate-reboot.timer20
-rw-r--r--units/systemd-sysupdate.service.in34
-rw-r--r--units/systemd-sysupdate.timer30
39 files changed, 6445 insertions, 21 deletions
diff --git a/NEWS b/NEWS
index c591694a72..4677c75177 100644
--- a/NEWS
+++ b/NEWS
@@ -12493,7 +12493,7 @@ CHANGES WITH 197:
based on a calendar time specification such as "Thu,Fri
2013-*-1,5 11:12:13" which refers to 11:12:13 of the first
or fifth day of any month of the year 2013, given that it is
- a thursday or friday. This brings timer event support
+ a Thursday or a Friday. This brings timer event support
considerably closer to cron's capabilities. For details on
the supported calendar time specification language see
systemd.time(7).
diff --git a/TODO b/TODO
index b4ee6d135c..eb2ce13114 100644
--- a/TODO
+++ b/TODO
@@ -251,11 +251,26 @@ Features:
that images cannot be misused.
* New udev block device symlink names:
- /dev/disk/by-parttypelabel/<pttype>/<ptlabel>. Use case: if pt label is used
+ /dev/disk/by-parttypelabel/<pttype>-<ptlabel>. Use case: if pt label is used
as partition image version string, this is a safe way to reference a specific
version of a specific partition type, in particular where related partitions
are processed (e.g. verity + rootfs both named "LennartOS_0.7").
+* sysupdate:
+ - add fuzzing to the pattern parser
+ - support casync as download mechanism
+ - direct TPM2 PCR change handling, possible renrolling LUKS2 media if needed.
+ - "systemd-sysupdate update --all" support, that iterates through all components
+ defined on the host, plus all images installed into /var/lib/machines/,
+ /var/lib/portable/ and so on.
+ - figure out what to do about system extensions (i.e. they need to imply an
+ update component, since otherwise system extenion' sysupdate.d/ files would
+ override the host's update files.)
+ - Allow invocation with a single transfer definition, i.e. with
+ --definitions= pointing to a file rather than a dir.
+ - add ability to disable implicit decompression of downloaded artifacts,
+ i.e. a Compress=no option in the transfer definitions
+
* in sd-id128: also parse UUIDs in RFC4122 URN syntax (i.e. chop off urn:uuid: prefix)
* DynamicUser= + StateDirectory= → use uid mapping mounts, too, in order to
diff --git a/man/loader.conf.xml b/man/loader.conf.xml
index e5453c7dcd..1186d3e2b4 100644
--- a/man/loader.conf.xml
+++ b/man/loader.conf.xml
@@ -30,10 +30,11 @@
<title>Description</title>
<para>
- <citerefentry><refentrytitle>systemd-boot</refentrytitle><manvolnum>7</manvolnum></citerefentry>
- will read <filename><replaceable>ESP</replaceable>/loader/loader.conf</filename> and any files with the
+ <citerefentry><refentrytitle>systemd-boot</refentrytitle><manvolnum>7</manvolnum></citerefentry> will
+ read <filename><replaceable>ESP</replaceable>/loader/loader.conf</filename>, and any files with the
<literal>.conf</literal> extension under
- <filename><replaceable>ESP</replaceable>/loader/entries/</filename> on the EFI system partition (ESP).
+ <filename><replaceable>ESP</replaceable>/loader/entries/</filename> on the EFI system partition (ESP) as
+ defined by <ulink url="https://systemd.io/BOOT_LOADER_SPECIFICATION">Boot Loader Specification</ulink>.
</para>
<para>Each configuration file must consist of an option name, followed by
diff --git a/man/rules/meson.build b/man/rules/meson.build
index 2e334ff331..aa969344ab 100644
--- a/man/rules/meson.build
+++ b/man/rules/meson.build
@@ -987,6 +987,13 @@ manpages = [
'5',
['system.conf.d', 'systemd-user.conf', 'user.conf.d'],
''],
+ ['systemd-sysupdate',
+ '8',
+ ['systemd-sysupdate-reboot.service',
+ 'systemd-sysupdate-reboot.timer',
+ 'systemd-sysupdate.service',
+ 'systemd-sysupdate.timer'],
+ 'ENABLE_SYSUPDATE'],
['systemd-sysusers', '8', ['systemd-sysusers.service'], ''],
['systemd-sysv-generator', '8', [], 'HAVE_SYSV_COMPAT'],
['systemd-time-wait-sync.service',
@@ -1058,6 +1065,7 @@ manpages = [
['systemd.time', '7', [], ''],
['systemd.timer', '5', [], ''],
['systemd.unit', '5', [], ''],
+ ['sysupdate.d', '5', [], 'ENABLE_SYSUPDATE'],
['sysusers.d', '5', [], 'ENABLE_SYSUSERS'],
['telinit', '8', [], 'HAVE_SYSV_COMPAT'],
['timedatectl', '1', [], 'ENABLE_TIMEDATECTL'],
diff --git a/man/systemd-boot.xml b/man/systemd-boot.xml
index ceea368ef2..6029330dd1 100644
--- a/man/systemd-boot.xml
+++ b/man/systemd-boot.xml
@@ -39,23 +39,22 @@
<itemizedlist>
<listitem><para>Boot entries defined with <ulink
- url="https://systemd.io/BOOT_LOADER_SPECIFICATION">Boot Loader Specification</ulink> description files
- located in <filename>/loader/entries/</filename> on the ESP and the Extended Boot Loader
- Partition. These usually describe Linux kernel images with associated initrd images, but alternatively
- may also describe arbitrary other EFI executables.</para></listitem>
-
- <listitem><para>Unified kernel images following the <ulink
- url="https://systemd.io/BOOT_LOADER_SPECIFICATION">Boot Loader Specification</ulink>, as executable EFI
- binaries in <filename>/EFI/Linux/</filename> on the ESP and the Extended Boot Loader Partition.
- </para></listitem>
+ url="https://systemd.io/BOOT_LOADER_SPECIFICATION">Boot Loader Specification</ulink> Type #1
+ description files located in <filename>/loader/entries/</filename> on the ESP and the Extended Boot
+ Loader Partition. These usually describe Linux kernel images with associated initrd images, but
+ alternatively may also describe other arbitrary EFI executables.</para></listitem>
+
+ <listitem><para>Unified kernel images, <ulink url="https://systemd.io/BOOT_LOADER_SPECIFICATION">Boot
+ Loader Specification</ulink> Type #2, which are executable EFI binaries in
+ <filename>/EFI/Linux/</filename> on the ESP and the Extended Boot Loader Partition.</para></listitem>
- <listitem><para>The Microsoft Windows EFI boot manager, if installed</para></listitem>
+ <listitem><para>The Microsoft Windows EFI boot manager, if installed.</para></listitem>
- <listitem><para>The Apple macOS boot manager, if installed</para></listitem>
+ <listitem><para>The Apple macOS boot manager, if installed.</para></listitem>
- <listitem><para>The EFI Shell binary, if installed</para></listitem>
+ <listitem><para>The EFI Shell binary, if installed.</para></listitem>
- <listitem><para>A reboot into the UEFI firmware setup option, if supported by the firmware</para></listitem>
+ <listitem><para>A reboot into the UEFI firmware setup option, if supported by the firmware.</para></listitem>
</itemizedlist>
<para><command>systemd-boot</command> supports the following features:</para>
diff --git a/man/systemd-sysupdate.xml b/man/systemd-sysupdate.xml
new file mode 100644
index 0000000000..81f57e8b52
--- /dev/null
+++ b/man/systemd-sysupdate.xml
@@ -0,0 +1,287 @@
+<?xml version='1.0'?> <!--*-nxml-*-->
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
+ "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
+<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
+
+<refentry id="systemd-sysupdate" conditional='ENABLE_SYSUPDATE'
+ xmlns:xi="http://www.w3.org/2001/XInclude">
+
+ <refentryinfo>
+ <title>systemd-sysupdate</title>
+ <productname>systemd</productname>
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>systemd-sysupdate</refentrytitle>
+ <manvolnum>8</manvolnum>
+ </refmeta>
+
+ <refnamediv>
+ <refname>systemd-sysupdate</refname>
+ <refname>systemd-sysupdate.service</refname>
+ <refname>systemd-sysupdate.timer</refname>
+ <refname>systemd-sysupdate-reboot.service</refname>
+ <refname>systemd-sysupdate-reboot.timer</refname>
+ <refpurpose>Automatically Update OS or Other Resources</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <cmdsynopsis>
+ <command>systemd-sysupdate</command>
+ <arg choice="opt" rep="repeat">OPTIONS</arg>
+ </cmdsynopsis>
+
+ <para><filename>systemd-sysupdate.service</filename></para>
+ </refsynopsisdiv>
+
+ <refsect1>
+ <title>Description</title>
+
+ <para><command>systemd-sysupdate</command> atomically updates the host OS, container images, portable
+ service images or other sources, based on the transfer configuration files described in
+ <citerefentry><refentrytitle>sysupdate.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>.</para>
+
+ <para>This tool implements file, directory, or partition based update schemes, supporting multiple
+ parallel installed versions of specific resources in an A/B (or even: A/B/C, A/B/C/D/, …) style. A/B
+ updating means that when one version of a resource is currently being used, the next version can be
+ downloaded, unpacked, and prepared in an entirely separate location, indepdently of the first, and — once
+ complete — be activated, swapping the roles so that it becomes the used one and the previously used one
+ becomes the the one that is replaced by the next update, and so on. The resources to update are defined
+ in transfer files, one for each resource to be updated. For example, resources that may be updated with
+ this tool could be: a root file system partition, a matching Verity partition plus one kernel image. The
+ combination of the three would be considered a complete OS update.</para>
+
+ <para>The tool updates partitions, files or directory trees always in whole, and operates with at least
+ two versions of each of these resources: the <emphasis>current</emphasis> version, plus the
+ <emphasis>next</emphasis> version: the one that is being updated to, and which is initially incomplete as
+ the downloaded data is written to it; plus optionally more versions. Once the download of a newer version
+ is complete it becomes the current version, releasing the version previously considered current for
+ deletion/replacement/updating.</para>
+
+ <para>When installing new versions the tool will directly download, decompress, unpack and write the new
+ version into the destination. This is done in a robust fashion so that an incomplete download can be
+ recognized on next invocation, and flushed out before a new attempt is initiated.</para>
+
+ <para>Note that when writing updates to a partition, the partition has to exist already, as
+ <command>systemd-sysupdate</command> will not automatically create new partitions. Use a tool such as
+ <citerefentry><refentrytitle>systemd-repart</refentrytitle><manvolnum>8</manvolnum></citerefentry> to
+ automatically create additional partitions to be used with <command>systemd-sysupdate</command> on
+ boot.</para>
+
+ <para>The tool can both be used on the running OS, to update the OS in "online" state from within itself,
+ and on "offline" disk images, to update them from the outside based on transfer files
+ embedded in the disk images. For the latter, see <option>--image=</option> below. The latter is
+ particularly interesting to update container images or portable service images.</para>
+
+ <para>The <filename>systemd-sysupdate.service</filename> system service will automatically update the
+ host OS based on the installed transfer files. It is triggered in regular intervals via
+ <filename>systemd-sysupdate.timer</filename>. The <filename>systemd-sysupdate-reboot.service</filename>
+ will automatically reboot the system after a new version is installed. It is triggered via
+ <filename>systemd-sysupdate-reboot.timer</filename>. The two services are separate from each other as it
+ is typically advisable to download updates regularly while the system is up, but delay reboots until the
+ appropriate time (i.e. typically at night). The two sets of service/timer units may be enabled
+ separately.</para>
+
+ <para>For details about transfer files and examples see
+ <citerefentry><refentrytitle>sysupdate.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>.</para>
+ </refsect1>
+
+ <refsect1>
+ <title>Command</title>
+
+ <para>The following commands are understood:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><option>list</option> <optional><replaceable>VERSION</replaceable></optional></term>
+
+ <listitem><para>If invoked without an argument, enumerates downloadable and installed versions, and
+ shows a summarizing table with the discovered versions and their properties, including whether
+ there's a newer candidate version to update to. If a version argument is specified, shows details
+ about the specific version, including the individual files that need to be transferred to acquire the
+ version.</para>
+
+ <para>If no command is explicitly specified this command is implied.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>check-new</option></term>
+
+ <listitem><para>Checks if there's a new version available. This internally enumerates downloadable and
+ installed versions and returns exit status 0 if there's a new version to update to, non-zero
+ otherwise. If there is a new version to update to, its version identifier is written to standard
+ output.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>update</option> <optional><replaceable>VERSION</replaceable></optional></term>
+
+ <listitem><para>Installs (updates to) the specified version, or if none is specified to the newest
+ version available. If the version is already installed or no newer version available, no operation is
+ executed.</para>
+
+ <para>If a new version to install/update to is found, old installed versions are deleted until at
+ least one new version can be installed, as configured via <varname>InstanceMax=</varname> in
+ <citerefentry><refentrytitle>sysupdate.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>, or
+ via the available partition slots of the right type. This implicit operation can also be invoked
+ explicitly via the <command>vacuum</command> command described below.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>vacuum</option></term>
+
+ <listitem><para>Deletes old installed versions until the limits configured via
+ <varname>InstanceMax=</varname> in
+ <citerefentry><refentrytitle>sysupdate.d</refentrytitle><manvolnum>5</manvolnum></citerefentry> are
+ met again. Normally, it should not be necessary to invoke this command explicitly, since it is
+ implicitly invoked whenever a new update is initiated.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>pending</option></term>
+
+ <listitem><para>Checks whether a newer version of the OS is installed than the one currently
+ running. Returns zero if so, non-zero otherwise. This compares the newest installed version's
+ identifier with the OS image version as reported by the <varname>IMAGE_VERSION=</varname> field in
+ <filename>/etc/os-release</filename>. If the former is newer than the latter, an update was
+ apparently completed but not activated (i.e. rebooted into) yet.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>reboot</option></term>
+
+ <listitem><para>Similar to the <option>pending</option> command but immediately reboots in case a
+ newer version of the OS has been installed than the one currently running. This operation can be done
+ implicitly together with the <command>update</command> command, after a completed update via the
+ <option>--reboot</option> switch, see below. This command will execute no operation (and return
+ success) if no update has been installed, and thus the system was not rebooted.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>components</option></term>
+
+ <listitem><para>Lists components that can be updated. This enumerates the
+ <filename>/etc/sysupdate.*.d/</filename>, <filename>/run/sysupdate.*.d/</filename> and
+ <filename>/usr/lib/sysupdate.*.d/</filename> directories that contain transfer files. This command is
+ useful to list possible parameters for <option>--component=</option> (see below).</para></listitem>
+ </varlistentry>
+
+ <xi:include href="standard-options.xml" xpointer="help" />
+ <xi:include href="standard-options.xml" xpointer="version" />
+ </variablelist>
+ </refsect1>
+
+ <refsect1>
+ <title>Options</title>
+
+ <para>The following options are understood:</para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><option>--component=</option></term>
+ <term><option>-C</option></term>
+
+ <listitem><para>Selects the component to update. Takes a component name as argument. This has the
+ effect of slightly altering the search logic for transfer files. If this switch is not used, the
+ transfer files are loaded from <filename>/etc/sysupdate.d/*.conf</filename>,
+ <filename>/run/sysupdate.d/*.conf</filename> and <filename>/usr/lib/sysupdate.d/*.conf</filename>. If
+ this switch is used, the specified component name is used to alter the directories to look in to be
+ <filename>/etc/sysupdate.<replaceable>component</replaceable>.d/*.conf</filename>,
+ <filename>/run/sysupdate.<replaceable>component</replaceable>.d/*.conf</filename> and
+ <filename>/usr/lib/sysupdate.<replaceable>component</replaceable>.d/*.conf</filename>, each time with
+ the <filename><replaceable>component</replaceable></filename> string replaced with the specified
+ component name.</para>
+
+ <para>Use the <command>components</command> command to list available components to update. This enumerates
+ the directories matching this naming rule.</para>
+
+ <para>Components may be used to define a separate set of transfer files for different components of
+ the OS that shall be updated separately. Do not use this concept for resources that shall always be
+ updated together in a synchronous fashion. Simply define multiple transfer files within the same
+ <filename>sysupdate.d/</filename> directory for these cases.</para>
+
+ <para>This option may not be combined with <option>--definitions=</option>.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--definitions=</option></term>
+
+ <listitem><para>A path to a directory. If specified, the transfer <filename>*.conf</filename> files
+ are read from this directory instead of <filename>/usr/lib/sysupdate.d/*.conf</filename>,
+ <filename>/etc/sysupdate.d/*.conf</filename>, and <filename>/run/sysupdate.d/*.conf</filename>.</para>
+
+ <para>This option may not be combined with <option>--component=</option>.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--root=</option></term>
+
+ <listitem><para>Takes a path to a directory to use as root file system when searching for
+ <filename>sysupdate.d/*.conf</filename> files.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--image=</option></term>
+
+ <listitem><para>Takes a path to a disk image file or device to mount and use in a similar fashion to
+ <option>--root=</option>, see above. If this is used and partition resources are updated this is done
+ inside the specified disk image.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--instances-max=</option></term>
+ <term><option>-m</option></term>
+
+ <listitem><para>Takes a decimal integer greater than or equal to 2. Controls how many versions to
+ keep at any time. This option may also be configured inside the transfer files, via the
+ <varname>InstancesMax=</varname> setting, see
+ <citerefentry><refentrytitle>sysupdate.d</refentrytitle><manvolnum>5</manvolnum></citerefentry> for
+ details.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--sync=</option></term>
+
+ <listitem><para>Takes a boolean argument, defaults to yes. This may be used to specify whether the
+ newly updated resource versions shall be synchronized to disk when appropriate (i.e. after the
+ download is complete, before it is finalized, and again after finalization). This should not be
+ turned off, except to improve runtime performance in testing environments.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--verify=</option></term>
+
+ <listitem><para>Takes a boolean argument, defaults to yes. Controls whether to cryptographically
+ verify downloads. Do not turn this off, except in testing environments.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--reboot</option></term>
+
+ <listitem><para>When used in combination with the <command>update</command> command and a new version is
+ installed, automatically reboots the system immediately afterwards.</para></listitem>
+ </varlistentry>
+
+ <xi:include href="standard-options.xml" xpointer="no-pager" />
+ <xi:include href="standard-options.xml" xpointer="no-legend" />
+ <xi:include href="standard-options.xml" xpointer="json" />
+ </variablelist>
+ </refsect1>
+
+ <refsect1>
+ <title>Exit status</title>
+
+ <para>On success, 0 is returned, a non-zero failure code otherwise.</para>
+ </refsect1>
+
+ <refsect1>
+ <title>See Also</title>
+ <para>
+ <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+ <citerefentry><refentrytitle>sysupdate.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>,
+ <citerefentry><refentrytitle>systemd-repart</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+ </para>
+ </refsect1>
+
+</refentry>
diff --git a/man/sysupdate.d.xml b/man/sysupdate.d.xml
new file mode 100644
index 0000000000..03d27b9fbc
--- /dev/null
+++ b/man/sysupdate.d.xml
@@ -0,0 +1,885 @@
+<?xml version='1.0'?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
+ "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
+<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
+
+<refentry id="sysupdate.d" conditional='ENABLE_SYSUPDATE'
+ xmlns:xi="http://www.w3.org/2001/XInclude">
+
+ <refentryinfo>
+ <title>sysupdate.d</title>
+ <productname>systemd</productname>
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>sysupdate.d</refentrytitle>
+ <manvolnum>5</manvolnum>
+ </refmeta>
+
+ <refnamediv>
+ <refname>sysupdate.d</refname>
+ <refpurpose>Transfer Definition Files for Automatic Updates</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <para><literallayout><filename>/etc/sysupdate.d/*.conf</filename>
+<filename>/run/sysupdate.d/*.conf</filename>
+<filename>/usr/lib/sysupdate.d/*.conf</filename>
+ </literallayout></para>
+ </refsynopsisdiv>
+
+ <refsect1>
+ <title>Description</title>
+
+ <para><filename>sysupdate.d/*.conf</filename> files describe how specific resources on the local system
+ shall be updated from a remote source. Each such file defines one such transfer: typically a remote
+ HTTP/HTTPS resource as source; and a local file, directory or partition as target. This may be used as a
+ simple, automatic, atomic update mechanism for the OS itself, for containers, portable services or system
+ extension images — but in fact may be used to update any kind of file from a remote source.</para>
+
+ <para>The
+ <citerefentry><refentrytitle>systemd-sysupdate</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+ command reads these files and uses them to determine which local resources should be updated, and then
+ executes the update.</para>
+
+ <para>Both the remote HTTP/HTTPS source and the local target typically exist in multiple, concurrent
+ versions, in order to implement flexible update schemes, e.g. A/B updating (or a superset thereof,
+ e.g. A/B/C, A/B/C/D, …).</para>
+
+ <para>Each <filename>*.conf</filename> file defines one transfer, i.e. describes one resource to
+ update. Typically, multiple of these files (i.e. multiple of such transfers) are defined together, and
+ are bound together by a common version identifier in order to update multiple resources at once on each
+ update operation, for example to update a kernel, a root file system and a Verity partition in a single,
+ combined, synchronized operation, so that only a combined update of all three together constitutes a
+ complete update.</para>
+
+ <para>Each <filename>*.conf</filename> file contains three sections: [Transfer], [Source] and [Target].</para>
+ </refsect1>
+
+ <refsect1>
+ <title>Basic Mode of Operation</title>
+
+ <para>Disk-image based OS updates typically consist of multiple different resources that need to be
+ updated together, for example a secure OS update might consist of a root file system image to drop into a
+ partition, a matching Verity integrity data partition image, and a kernel image prepared to boot into the
+ combination of the two partitions. The first two resources are files that are downloaded and placed in a
+ disk partition, the latter is a file that is downloaded and placed in a regular file in the boot file
+ system (e.g. EFI system partition). Hence, during an update of a hypothetical operating system "foobarOS"
+ to a hypothetical version 47 the following operations should take place:</para>
+
+ <orderedlist>
+ <listitem><para>A file <literal>https://download.example.com/foobarOS_47.root.xz</literal> should be
+ downloaded, decompressed and written to a previously unused partition with GPT partition type UUID
+ 4f68bce3-e8cd-4db1-96e7-fbcaf984b709 for x86-64, as per <ulink
+ url="https://systemd.io/DISCOVERABLE_PARTITIONS">Discoverable Partitions
+ Specification</ulink>.</para></listitem>
+
+ <listitem><para>Similarly, a file <literal>https://download.example.com/foobarOS_47.verity.xz</literal>
+ should be downloaded, decompressed and written to a previously empty partition with GPT partition type
+ UUID of 2c7357ed-ebd2-46d9-aec1-23d437ec2bf5 (i.e the partition type for Verity integrity information
+ for x86-64 root file systems).</para></listitem>
+
+ <listitem><para>Finally, a file <literal>https://download.example.com/foobarOS_47.efi.xz</literal> (a
+ unified kernel, as per <ulink url="https://systemd.io/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>
+ </orderedlist>
+
+ <para>The version-independent generalization of this would be (using the special marker
+ <literal>@v</literal> as wildcard for the version identifier):</para>
+
+ <orderedlist>
+ <listitem><para>A transfer of a file <literal>https://download.example.com/foobarOS_@v.root.xz</literal>
+ → a local, previously empty GPT partition of type 4f68bce3-e8cd-4db1-96e7-fbcaf984b709, with the label to
+ be set to <literal>foobarOS_@v</literal>.</para></listitem>
+
+ <listitem><para>A transfer of a file <literal>https://download.example.com/foobarOS_@v.verity.xz</literal>
+ → a local, previously empty GPT partition of type 2c7357ed-ebd2-46d9-aec1-23d437ec2bf5, with the label to be
+ 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>
+ </orderedlist>
+
+ <para>An update can only complete if the relevant URLs provide their resources for the same version,
+ i.e. for the same value of <literal>@v</literal>.</para>
+
+ <para>The above may be translated into three <filename>*.conf</filename> files in
+ <filename>sysupdate.d/</filename>, one for each resource to transfer. The <filename>*.conf</filename>
+ files configure the type of download, and what place to write the download to (i.e. whether to a
+ partition or a file in the file system). Most importantly these files contain the URL, partition name and
+ filename patterns shown above that describe how these resources are called on the source and how they
+ shall be called on the target.</para>
+
+ <para>In order to enumerate available versions and figuring out candidates to update to, a mechanism is
+ necessary to list suitable files:</para>
+
+ <itemizedlist>
+ <listitem><para>For partitions: the surrounding GPT partition table contains a list of defined
+ partitions, including a partition type UUID and a partition label (in this scheme the partition label
+ plays a role for the partition similar to the filename for a regular file)</para></listitem>
+
+ <listitem><para>For regular files: the directory listing of the directory the files are contained in
+ provides a list of existing files in a straightforward way.</para></listitem>
+
+ <listitem><para>For HTTP/HTTPS sources a simple scheme is used: a manifest file
+ <filename>SHA256SUMS</filename>, following the format defined by <citerefentry
+ project='man-pages'><refentrytitle>sha256sum</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+ lists file names and their SHA256 hashes.</para></listitem>
+ </itemizedlist>
+
+ <para>Transfers are done in the alphabetical order of the <filename>.conf</filename> file names they are
+ defined in. First, the resource data is downloaded directly into a target file/directory/partition. Once
+ this is completed for all defined transfers, in a second step the files/directories/partitions are
+ renamed to their final names as defined by the target <varname>MatchPattern=</varname>, again in the
+ order the <filename>.conf</filename> transfer file names dictate. This step is not atomic, however it is
+ guaranteed to be executed strictly in order with suitable disk synchronization in place. Typically, when
+ updating an OS one of the transfers defines the entry point when booting. Thus it is generally a good idea
+ to order the resources via the transfer configuration file names so that the entry point is written
+ last, ensuring that any abnormal termination does not leave an entry point around whose backing is not
+ established yet. In the example above it would hence make sense to establish the EFI kernel image last
+ and thus give its transfer configuration file the alphabetically last name.</para>
+
+ <para>See below for an extended, more specific example based on the above.</para>
+ </refsect1>
+
+ <refsect1>
+ <title>Resource Types</title>
+
+ <para>Each transfer file defines one source resource to transfer to one target resource. The following
+ resource types are supported:</para>
+
+ <orderedlist>
+
+ <listitem><para>Resources of type <literal>url-file</literal> encapsulate a file on a web server,
+ referenced via a HTTP or HTTPS URL. When an update takes place, the file is downloaded and decompressed
+ and then written to the target file or partition. This resource type is only available for sources, not
+ for targets. The list of available versions of resources of this type is encoded in
+ <filename>SHA256SUMS</filename> manifest files, accompanied by
+ <filename>SHA256SUMS.gpg</filename> detached signatures.</para></listitem>
+
+ <listitem><para>The <literal>url-tar</literal> resource type is similar, but the file must be a
+ <filename>.tar</filename> archive. When an update takes place, the file is decompressed and unpacked
+ into a directory or btrfs subvolume. This resource type is only available for sources, not for
+ targets. Just like <literal>url-file</literal>, <literal>url-tar</literal> version enumeration makes
+ use of <filename>SHA256SUMS</filename> files, authenticated via
+ <filename>SHA256SUMS.gpg</filename>.</para></listitem>
+
+ <listitem><para>The <literal>regular-file</literal> resource type encapsulates a local regular file on
+ disk. During updates the file is uncompressed and written to the target file or partition. This
+ resource type is available both as source and as target. When updating no integrity or authentication
+ verification is done for resources of this type.</para></listitem>
+
+ <listitem><para>The <literal>partition</literal> resource type is similar to
+ <literal>regular-file</literal>, and encapsulates a GPT partition on disk. When updating, the partition
+ must exist already, and have the correct GPT partition type. A partition whose GPT partition label is
+ set to <literal>_empty</literal> is considered empty, and a candidate to place a newly downloaded
+ resource in. The GPT partition label is used to store version information, once a partition is
+ updated. This resource type is only available for target resources.</para></listitem>
+
+ <listitem><para>The <literal>tar</literal> resource type encapsulates local <filename>.tar</filename>
+ archive files. When an update takes place, the files are uncompressed and unpacked into a target
+ directory or btrfs subvolume. Behaviour of <literal>tar</literal> and <literal>url-tar</literal> is
+ generally similar, but the latter downloads from remote sources, and does integrity and authentication
+ checks while the former does not. The <literal>tar</literal> resource type is only available for source
+ resources.</para></listitem>
+
+ <listitem><para>The <literal>directory</literal> resource type encapsulates local directory trees. This
+ type is available both for source and target resources. If an update takes place on a source resource
+ of this type, a recursive copy of the directory is done.</para></listitem>
+
+ <listitem><para>The <literal>subvolume</literal> resource type is identical to
+ <literal>directory</literal>, except when used as the target, in which case the file tree is placed in
+ a btrfs subvolume instead of a plain directory, if the backing file system supports it (i.e. is
+ btrfs).</para></listitem>
+ </orderedlist>
+
+ <para>As already indicated, only a subset of source and target resource type combinations are
+ supported:</para>
+
+ <table>
+ <title>Resource Types</title>
+
+ <tgroup cols='3' align='left' colsep='1' rowsep='1'>
+ <colspec colname="name" />
+ <colspec colname="explanation" />
+
+ <thead>
+ <row>
+ <entry>Identifier</entry>
+ <entry>Description</entry>
+ <entry>Usable as Source</entry>
+ <entry>When Used as Source: Compatible Targets</entry>
+ <entry>When Used as Source: Integrity + Authentication</entry>
+ <entry>When Used as Source: Decompression</entry>
+ <entry>Usable as Target</entry>
+ <entry>When Used as Target: Compatible Sources</entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry><constant>url-file</constant></entry>
+ <entry>HTTP/HTTPS files</entry>
+ <entry>yes</entry>
+ <entry><constant>regular-file</constant>, <constant>partition</constant></entry>
+ <entry>yes</entry>
+ <entry>yes</entry>
+ <entry>no</entry>
+ <entry>-</entry>
+ </row>
+
+ <row>
+ <entry><constant>url-tar</constant></entry>
+ <entry>HTTP/HTTPS <filename>.tar</filename> archives</entry>
+ <entry>yes</entry>
+ <entry><constant>directory</constant>, <constant>subvolume</constant></entry>
+ <entry>yes</entry>
+ <entry>yes</entry>
+ <entry>no</entry>
+ <entry>-</entry>
+ </row>
+
+ <row>
+ <entry><constant>regular-file</constant></entry>
+ <entry>Local files</entry>
+ <entry>yes</entry>
+ <entry><constant>regular-file</constant>, <constant>partition</constant></entry>
+ <entry>no</entry>
+ <entry>yes</entry>
+ <entry>yes</entry>
+ <entry><constant>url-file</constant>, <constant>regular-file</constant></entry>
+ </row>
+
+ <row>
+ <entry><constant>partition</constant></entry>
+ <entry>Local GPT partitions</entry>
+ <entry>no</entry>
+ <entry>-</entry>
+ <entry>-</entry>
+ <entry>-</entry>
+ <entry>yes</entry>
+ <entry><constant>url-file</constant>, <constant>regular-file</constant></entry>
+ </row>
+
+ <row>
+ <entry><constant>tar</constant></entry>
+ <entry>Local <filename>.tar</filename> archives</entry>
+ <entry>yes</entry>
+ <entry><constant>directory</constant>, <constant>subvolume</constant></entry>
+ <entry>no</entry>
+ <entry>yes</entry>
+ <entry>no</entry>
+ <entry>-</entry>
+ </row>
+
+ <row>
+ <entry><constant>directory</constant></entry>
+ <entry>Local directories</entry>
+ <entry>yes</entry>
+ <entry><constant>directory</constant>, <constant>subvolume</constant></entry>
+ <entry>no</entry>
+ <entry>no</entry>
+ <entry>yes</entry>
+ <entry><constant>url-tar</constant>, <constant>tar</constant>, <constant>directory</constant>, <constant>subvolume</constant></entry>
+ </row>
+
+ <row>
+ <entry><constant>subvolume</constant></entry>
+ <entry>Local btrfs subvolumes</entry>
+ <entry>yes</entry>
+ <entry><constant>directory</constant>, <constant>subvolume</constant></entry>
+ <entry>no</entry>
+ <entry>no</entry>
+ <entry>yes</entry>
+ <entry><constant>url-tar</constant>, <constant>tar</constant>, <constant>directory</constant>, <constant>subvolume</constant></entry>
+ </row>
+
+ </tbody>
+ </tgroup>
+ </table>
+ </refsect1>
+
+ <refsect1>
+ <title>Match Patterns</title>
+
+ <para>Both the source and target resources typically exist in multiple versions concurrently. An update
+ operation is done whenever the newest of the source versions is newer than the newest of the target
+ versions. To determine the newest version of the resources a directory listing, partition listing or
+ manifest listing is used, a subset of qualifying entries selected from that, and the version identifier
+ extracted from the file names or partition labels of these selected entries. Subset selection and
+ extraction of the version identifier (plus potentially other metadata) is done via match patterns,
+ configured in <varname>MatchPattern=</varname> in the [Source] and [Target] sections. These patterns are
+ strings that describe how files or partitions are named, with named wildcards for specific fields such as
+ the version identifier. The following wildcards are defined:</para>
+
+ <table>
+ <title>Match Pattern Wildcards</title>
+
+ <tgroup cols='2' align='left' colsep='1' rowsep='1'>
+ <colspec colname="name" />
+ <colspec colname="explanation" />
+
+ <thead>
+ <row>
+ <entry>Wildcard</entry>
+ <entry>Description</entry>
+ <entry>Format</entry>
+ <entry>Notes</entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry><literal>@v</literal></entry>
+ <entry>Version identifier</entry>
+ <entry>Valid version string</entry>
+ <entry>Mandatory</entry>
+ </row>
+
+ <row>
+ <entry><literal>@u</literal></entry>
+ <entry>GPT partition UUID</entry>
+ <entry>Valid 128-Bit UUID string</entry>
+ <entry>Only relevant if target resource type chosen as <constant>partition</constant></entry>
+ </row>
+
+ <row>
+ <entry><literal>@f</literal></entry>
+ <entry>GPT partition flags</entry>
+ <entry>Formatted hexadecimal integer</entry>
+ <entry>Only relevant if target resource type chosen as <constant>partition</constant></entry>
+ </row>
+
+ <row>
+ <entry><literal>@a</literal></entry>
+ <entry>GPT partition flag NoAuto</entry>
+ <entry>Either <literal>0</literal> or <literal>1</literal></entry>
+ <entry>Controls NoAuto bit of the GPT partition flags, as per <ulink url="https://systemd.io/DISCOVERABLE_PARTITIONS">Discoverable Partitions Specification</ulink>; only relevant if target resource type chosen as <constant>partition</constant></entry>
+ </row>
+
+ <row>
+ <entry><literal>@g</literal></entry>
+ <entry>GPT partition flag GrowFileSystem</entry>
+ <entry>Either <literal>0</literal> or <literal>1</literal></entry>
+ <entry>Controls GrowFileSystem bit of the GPT partition flags, as per <ulink url="https://systemd.io/DISCOVERABLE_PARTITIONS">Discoverable Partitions Specification</ulink>; only relevant if target resource type chosen as <constant>partition</constant></entry>
+ </row>
+
+ <row>
+ <entry><literal>@r</literal></entry>
+ <entry>Read-only flag</entry>
+ <entry>Either <literal>0</literal> or <literal>1</literal></entry>
+ <entry>Controls ReadOnly bit of the GPT partition flags, as per <ulink url="https://systemd.io/DISCOVERABLE_PARTITIONS">Discoverable Partitions Specification</ulink> and other output read-only flags, see <varname>ReadOnly=</varname> below.</entry>
+ </row>
+
+ <row>
+ <entry><literal>@t</literal></entry>
+ <entry>File modification time</entry>
+ <entry>Formatted decimal integer, µs since UNIX epoch Jan 1st 1970</entry>
+ <entry>Only relevant if target resource type chosen as <constant>regular-file</constant></entry>
+ </row>
+
+ <row>
+ <entry><literal>@m</literal></entry>
+ <entry>File access mode</entry>
+ <entry>Formatted octal integer, in UNIX fashion</entry>
+ <entry>Only relevant if target resource type chosen as <constant>regular-file</constant></entry>
+ </row>
+
+ <row>
+ <entry><literal>@s</literal></entry>
+ <entry>File size after decompression</entry>
+ <entry>Formatted decimal integer</entry>
+ <entry>Useful for measuring progress and to improve partition allocation logic</entry>
+ </row>
+
+ <row>
+ <entry><literal>@d</literal></entry>
+ <entry>Tries done</entry>
+ <entry>Formatted decimal integer</entry>
+ <entry>Useful when operating with kernel image files, as per <ulink url="https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT">Automatic Boot Assessment</ulink></entry>
+ </row>
+
+ <row>
+ <entry><literal>@l</literal></entry>
+ <entry>Tries left</entry>
+ <entry>Formatted decimal integer</entry>
+ <entry>Useful when operating with kernel images, as per <ulink url="https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT">Automatic Boot Assessment</ulink></entry>
+ </row>
+
+ <row>
+ <entry><literal>@h</literal></entry>
+ <entry>SHA256 hash of compressed file</entry>
+ <entry>64 hexadecimal characters</entry>
+ <entry>The SHA256 hash of the compressed file; not useful for <constant>url-file</constant> or <constant>url-tar</constant> where the SHA256 hash is already included in the manifest file anyway.</entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ <para>Of these wildcards only <literal>@v</literal> must be present in a valid pattern, all other
+ wildcards are optional. Each wildcard may be used at most once in each pattern. A typical wildcard
+ matching a file system source image could be <literal>MatchPattern=foobar_@v.raw.xz</literal>, i.e. any file
+ whose name begins with <literal>foobar_</literal>, followed by a version ID and suffixed by
+ <literal>.raw.xz</literal>.</para>
+
+ <para>Do not confuse the <literal>@</literal> pattern matching wildcard prefix with the
+ <literal>%</literal> specifier expansion prefix. The former encapsulate a variable part of a match
+ pattern string, the latter are simple shortcuts that are expanded while the drop-in files are
+ parsed. For details about specifiers, see below.</para>
+ </refsect1>
+
+ <refsect1>
+ <title>[Transfer] Section Options</title>
+
+ <para>This section defines general properties of this transfer.</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><varname>MinVersion=</varname></term>
+
+ <listitem><para>Specifies the minimum version to require for this transfer to take place. If the
+ source or target patterns in this transfer definition match files older than this version they will
+ be considered obsolete, and never be considered for the update operation.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><varname>ProtectVersion=</varname></term>
+
+ <listitem><para>Takes one or more version strings to mark as "protected". Protected versions are
+ never removed while making room for new, updated versions. This is useful to ensure that the
+ currently booted OS version (or auxiliary resources associated with it) is not replaced/overwritten
+ during updates, in order to avoid runtime file system corruptions.</para>
+
+ <para>Like many of the settings in these configuration files this setting supports specifier
+ expansion. It's particularly useful to set this setting to one of the <literal>%A</literal>,
+ <literal>%B</literal> or <literal>%w</literal> specifiers to automatically refer to the current OS
+ version of the running system. See below for details on supported specifiers.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><varname>Verify=</varname></term>
+
+ <listitem><para>Takes a boolean, defaults to yes. Controls whether to cryptographically verify
+ downloaded resources (specifically: validate the GPG signatures for downloaded
+ <filename>SHA256SUMS</filename> manifest files, via their detached signature files
+ <filename>SHA256SUMS.gpg</filename> in combination with the system keyring
+ <filename>/usr/lib/systemd/import-pubring.gpg</filename> or
+ <filename>/etc/systemd/import-pubring.gpg</filename>).</para>
+
+ <para>This option is essential to provide integrity guarantees for downloaded resources and thus
+ should be left enabled, outside of test environments.</para>
+
+ <para>Note that the downloaded payload files are unconditionally checked against the SHA256 hashes
+ listed in the manifest. This option only controls whether the signatures of these manifests are
+ verified.</para>
+
+ <para>This option only has an effect if the source resource type is selected as
+ <constant>url-file</constant> or <constant>url-tar</constant>, as integrity and authentication
+ checking is only available for transfers from remote sources.</para></listitem>
+ </varlistentry>
+
+ </variablelist>
+ </refsect1>
+
+ <refsect1>
+ <title>[Source] Section Options</title>
+
+ <para>This section defines properties of the transfer source:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><varname>Type=</varname></term>
+
+ <listitem><para>Specifies the resource type of the source for the transfer. Takes one of
+ <constant>url-file</constant>, <constant>url-tar</constant>, <constant>tar</constant>,
+ <constant>regular-file</constant>, <constant>directory</constant> or
+ <constant>subvolume</constant>. For details about the resource types, see above. This option is
+ mandatory.</para>
+
+ <para>Note that only some combinations of source and target resource types are supported, see
+ above.</para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ <variablelist>
+ <varlistentry>
+ <term><varname>Path=</varname></term>
+
+ <listitem><para>Specifies where to find source versions of this resource.</para>
+
+ <para>If the source type is selected as <constant>url-file</constant> or
+ <constant>url-tar</constant> this must be a HTTP/HTTPS URL. The URL is suffixed with
+ <filename>/SHA256SUMS</filename> to acquire the manifest file, with
+ <filename>/SHA256SUMS.gpg</filename> to acquire the detached signature file for it, and with the file
+ names listed in the manifest file in case an update is executed and a resource shall be
+ downloaded.</para>
+
+ <para>For all other source resource types this must be a local path in the file system, referring to
+ a local directory to find the versions of this resource in.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><varname>MatchPattern=</varname></term>
+
+ <listitem><para>Specifies one or more file name match patterns that select the subset of files that
+ are update candidates as source for this transfer. See above for details on match patterns.</para>
+
+ <para>This option is mandatory. Any pattern listed must contain at least the <literal>@v</literal>
+ wildcard, so that a version identifier may be extracted from the filename. All other wildcards are
+ optional.</para></listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect1>
+
+ <refsect1>
+ <title>[Target] Section Options</title>
+
+ <para>This section defines properties of the transfer target:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><varname>Type=</varname></term>
+
+ <listitem><para>Specifies the resource type of the target for the transfer. Takes one of
+ <constant>partition</constant>, <constant>regular-file</constant>, <constant>directory</constant> or
+ <constant>subvolume</constant>. For details about the resource types, see above. This option is
+ mandatory.</para>
+
+ <para>Note that only some combinations of source and target resource types are supported, see above.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><varname>Path=</varname></term>
+
+ <listitem><para>Specifies a file system path where to look for already installed versions or place
+ newly downloaded versions of this configured resource. If <varname>Type=</varname> is set to
+ <constant>partition</constant>, expects a path to a (whole) block device node, or the special string
+ <literal>auto</literal> in which case the block device the root file system of the currently booted
+ system is automatically determined and used. If <varname>Type=</varname> is set to
+ <constant>regular-file</constant>, <constant>directory</constant> or <constant>subvolume</constant>,
+ must refer to a path in the local file system referencing the directory to find or place the version
+ files or directories under.</para>
+
+ <para>Note that this mechanism cannot be used to create or remove partitions, in case
+ <varname>Type=</varname> is set to <constant>partition</constant>. Partitions must exist already, and
+ a special partition label <literal>_empty</literal> is used to indicate empty partitions. To
+ automatically generate suitable partitions on first boot, use a tool such as
+ <citerefentry><refentrytitle>systemd-repart</refentrytitle><manvolnum>8</manvolnum></citerefentry>.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><varname>MatchPattern=</varname></term>
+
+ <listitem><para>Specifies one or more file name or partition label match patterns that select the
+ subset of files or partitions that are update candidates as targets for this transfer. See above for
+ details on match patterns.</para>
+
+ <para>This option is mandatory. Any pattern listed must contain at least the <literal>@v</literal>
+ wildcard, so that a version identifier may be extracted from the filename. All other wildcards are
+ optional.</para>
+
+ <para>This pattern is both used for matching existing installed versions and for determining the name
+ of new versions to install. If multiple patterns are specified, the first specified is used for
+ naming newly installed versions.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><varname>MatchPartitionType=</varname></term>
+
+ <listitem><para>When the target <varname>Type=</varname> is chosen as <constant>partition</constant>,
+ specifies the GPT partition type to look for. Only partitions of this type are considered, all other
+ partitions are ignored. If not specified, the GPT partition type <constant>linux-generic</constant>
+ is used. Accepts either a literal type UUID or a symbolic type identifier. For a list of supported
+ type identifiers, see the <varname>Type=</varname> setting in
+ <citerefentry><refentrytitle>repart.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><varname>PartitionUUID=</varname></term>
+ <term><varname>PartitionFlags=</varname></term>
+ <term><varname>PartitionNoAuto=</varname></term>
+ <term><varname>PartitionGrowFileSystem=</varname></term>
+
+ <listitem><para>When the target <varname>Type=</varname> is picked as <constant>partition</constant>,
+ selects the GPT partition UUID and partition flags to use for the updated partition. Expects a valid
+ UUID string, a hexadecimal integer, or booleans, respectively. If not set, but the source match
+ pattern includes wildcards for these fields (i.e. <literal>@u</literal>, <literal>@f</literal>,
+ <literal>@a</literal>, or <literal>@g</literal>), the values from the patterns are used. If neither
+ configured with wildcards or these explicit settings, the values are left untouched. If both the
+ overall <varname>PartitionFlags=</varname> flags setting and the individual flag settings
+ <varname>PartitionNoAuto=</varname> and <varname>PartitionGrowFileSystem=</varname> are used (or the
+ wildcards for them), then the latter override the former, i.e. the individual flag bit overrides the
+ overall flags value. See <ulink url="https://systemd.io/DISCOVERABLE_PARTITIONS">Discoverable
+ Partitions Specification</ulink> for details about these flags.</para>
+
+ <para>Note that these settings are not used for matching, they only have effect on newly written
+ partitions in case a transfer takes place.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><varname>ReadOnly=</varname></term>
+
+ <listitem><para>Controls whether to mark the resulting file, subvolume or partition read-only. If the
+ target type is <constant>partition</constant> this controls the ReadOnly partition flag, as per
+ <ulink url="https://systemd.io/DISCOVERABLE_PARTITIONS">Discoverable Partitions
+ Specification</ulink>, similar to the <varname>PartitionNoAuto=</varname> and
+ <varname>PartitionGrowFileSystem=</varname> flags described above. If the target type is
+ <constant>regular-file</constant>, the writable bit is removed from the access mode. If the the
+ target type is <constant>subvolume</constant>, the subvolume will be marked read-only as a
+ whole. Finally, if the target <varname>Type=</varname> is selected as <constant>directory</constant>,
+ the "immutable" file attribute is set, see <citerefentry
+ project='man-pages'><refentrytitle>chattr</refentrytitle><manvolnum>1</manvolnum></citerefentry> for
+ details.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><varname>Mode=</varname></term>
+
+ <listitem><para>The UNIX file access mode to use for newly created files in case the target resource
+ type is picked as <constant>regular-file</constant>. Expects an octal integer, in typical UNIX
+ fashion. If not set, but the source match pattern includes a wildcard for this field
+ (i.e. <literal>@t</literal>), the value from the pattern is used.</para>
+
+ <para>Note that this setting is not used for matching, it only has an effect on newly written
+ files when a transfer takes place.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><varname>TriesDone=</varname></term>
+ <term><varname>TriesLeft=</varname></term>
+
+ <listitem><para>These options take positive, decimal integers, and control the number of attempts
+ done and left for this file. These settings are useful for managing kernel images, following the
+ scheme defined in <ulink url="https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT">Automatic Boot
+ Assessment</ulink>, and only have an effect if the target pattern includes the <literal>@d</literal>
+ or <literal>@l</literal> wildcards.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><varname>InstancesMax=</varname></term>
+
+ <listitem><para>Takes a decimal integer equal to or greater than 2. This configures how many concurrent
+ versions of the resource to keep. Whenever a new update is initiated it is made sure that no more
+ than the number of versions specified here minus one exist in the target. Any excess versions are
+ deleted (in case the target <varname>Type=</varname> of <constant>regular-file</constant>,
+ <constant>directory</constant>, <constant>subvolume</constant> is used) or emptied (in case the
+ target <varname>Type=</varname> of <constant>partition</constant> is used; emptying in this case
+ simply means to set the partition label to the special string <literal>_empty</literal>; note that no
+ partitions are actually removed). After an update is completed the number of concurrent versions of
+ the target resources is equal to or below the number specified here.</para>
+
+ <para>Note that this setting may be set differently for each transfer. However, it generally is
+ advisable to keep this setting the same for all transfers, since otherwise incomplete combinations of
+ files or partitions will be left installed.</para>
+
+ <para>If the target <varname>Type=</varname> is selected as <constant>partition</constant>, the number
+ of concurrent versions to keep is additionally restricted by the number of partition slots of the
+ right type in the partition table. i.e. if there are only 2 partition slots for the selected
+ partition type, setting this value larger than 2 is without effect, since no more than 2 concurrent
+ versions could be stored in the image anyway.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><varname>RemoveTemporary=</varname></term>
+
+ <listitem><para>Takes a boolean argument. If this option is enabled (which is the default) before
+ initiating an update, all left-over, incomplete updates from a previous attempt are removed from the
+ target directory. This only has an effect if the target resource <varname>Type=</varname> is selected
+ as <constant>regular-file</constant>, <constant>directory</constant> or
+ <constant>subvolume</constant>.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><varname>CurrentSymlink=</varname></term>
+
+ <listitem><para>Takes a symlink name as argument. If this option is used, as the last step of the
+ update a symlink under the specified name is created/updated pointing to the completed update. This
+ is useful in to provide a stable name always pointing to the newest version of the resource. This is
+ only supported if the target resource <varname>Type=</varname> is selected as
+ <constant>regular-file</constant>, <constant>directory</constant> or
+ <constant>subvolume</constant>.</para></listitem>
+ </varlistentry>
+
+ </variablelist>
+ </refsect1>
+
+ <refsect1>
+ <title>Specifiers</title>
+
+ <para>Specifiers may be used in the <varname>MinVersion=</varname>, <varname>ProtectVersion=</varname>,
+ <varname>Path=</varname>, <varname>MatchPattern=</varname> and <varname>CurrentSymlink=</varname>
+ settings. The following expansions are understood:</para>
+ <table class='specifiers'>
+ <title>Specifiers available</title>
+ <tgroup cols='3' align='left' colsep='1' rowsep='1'>
+ <colspec colname="spec" />
+ <colspec colname="mean" />
+ <colspec colname="detail" />
+ <thead>
+ <row>
+ <entry>Specifier</entry>
+ <entry>Meaning</entry>
+ <entry>Details</entry>
+ </row>
+ </thead>
+ <tbody>
+ <xi:include href="standard-specifiers.xml" xpointer="a"/>
+ <xi:include href="standard-specifiers.xml" xpointer="A"/>
+ <xi:include href="standard-specifiers.xml" xpointer="b"/>
+ <xi:include href="standard-specifiers.xml" xpointer="B"/>
+ <xi:include href="standard-specifiers.xml" xpointer="H"/>
+ <xi:include href="standard-specifiers.xml" xpointer="l"/>
+ <xi:include href="standard-specifiers.xml" xpointer="m"/>
+ <xi:include href="standard-specifiers.xml" xpointer="M"/>
+ <xi:include href="standard-specifiers.xml" xpointer="o"/>
+ <xi:include href="standard-specifiers.xml" xpointer="v"/>
+ <xi:include href="standard-specifiers.xml" xpointer="w"/>
+ <xi:include href="standard-specifiers.xml" xpointer="W"/>
+ <xi:include href="standard-specifiers.xml" xpointer="T"/>
+ <xi:include href="standard-specifiers.xml" xpointer="V"/>
+ <xi:include href="standard-specifiers.xml" xpointer="percent"/>
+ </tbody>
+ </tgroup>
+ </table>
+
+ <para>Do not confuse the <literal>%</literal> specifier expansion prefix with the <literal>@</literal>
+ pattern matching wildcard prefix. The former are simple shortcuts that are expanded while the drop-in
+ files are parsed, the latter encapsulate a variable part of a match pattern string. For details about
+ pattern matching wildcards, see above.</para>
+ </refsect1>
+
+ <refsect1>
+ <title>Examples</title>
+
+ <example>
+ <title>Updates for a Verity Enabled Secure OS</title>
+
+ <para>With the following three files we define a root file system partition, a matching Verity
+ partition, and a unified kernel image to update as one. This example is an extension of the example
+ discussed earlier in this man page.</para>
+
+ <para><programlisting># /usr/lib/sysupdate.d/50-verity.conf
+[Transfer]
+ProtectVersion=%A
+
+[Source]
+Type=url-file
+Path=https://download.example.com/
+MatchPattern=foobarOS_@v_@u.verity.xz
+
+[Target]
+Type=partition
+Path=auto
+MatchPattern=foobarOS_@v_verity
+MatchPartitionType=root-verity
+PartitionFlags=0
+PartitionReadOnly=1</programlisting></para>
+
+ <para>The above defines the update mechanism for the Verity partition of the root file system. Verity
+ partition images are downloaded from
+ <literal>https://download.example.com/foobarOS_@v_@u.verity.xz</literal> and written to a suitable
+ local partition, which is marked read-only. Under the assumption this update is run from the image
+ itself the current image version (i.e. the <literal>%A</literal> specifier) is marked as protected, to
+ ensure it is not corrupted while booted. Note that the partition UUID for the target partition is
+ encoded in the source file name. Fixating the partition UUID can be useful to ensure that
+ <literal>roothash=</literal> on the kernel command line is sufficient to pinpoint both the Verity and
+ root file system partition, and also encode the Verity root level hash (under the assumption the UUID
+ in the file names match their top-level hash, the way
+ <citerefentry><refentrytitle>systemd-gpt-auto-generator</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+ suggests).</para>
+
+ <para><programlisting># /usr/lib/sysupdate.d/60-root.conf
+[Transfer]
+ProtectVersion=%A
+
+[Source]
+Type=url-file
+Path=https://download.example.com/
+MatchPattern=foobarOS_@v_@u.root.xz
+
+[Target]
+Type=partition
+Path=auto
+MatchPattern=foobarOS_@v
+MatchPartitionType=root
+PartitionFlags=0
+PartitionReadOnly=1</programlisting></para>
+
+ <para>The above defines a matching transfer definition for the root file system.</para>
+
+ <para><programlisting># /usr/lib/sysupdate.d/70-kernel.conf
+[Transfer]
+ProtectVersion=%A
+
+[Source]
+Type=url-file
+Path=https://download.example.com/
+MatchPattern=foobarOS_@v.efi.xz
+
+[Target]
+Type=file
+Path=/efi/EFI/Linux
+MatchPattern=foobarOS_@v+@l-@d.efi \
+ foobarOS_@v+@l.efi \
+ foobarOS_@v.efi
+Mode=0444
+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://systemd.io/BOOT_LOADER_SPECIFICATION">Boot
+ Loader Specification</ulink> Type #2. This defines three possible patterns for the names of the
+ kernel images 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>
+
+ <itemizedlist>
+ <listitem><para><filename>SHA256SUMS</filename> – The manifest file, containing available files and their SHA256 hashes</para></listitem>
+ <listitem><para><filename>SHA256SUMS.gpg</filename> – The detached cryptographic signature for the manifest file</para></listitem>
+ <listitem><para><filename>foobarOS_7_8b8186b1-2b4e-4eb6-ad39-8d4d18d2a8fb.verity.xz</filename> – The Verity image for version 7</para></listitem>
+ <listitem><para><filename>foobarOS_7_f4d1234f-3ebf-47c4-b31d-4052982f9a2f.root.xz</filename> – The root file system image for version 7</para></listitem>
+ <listitem><para><filename>foobarOS_7_efi.xz</filename> – The unified kernel image for version 7</para></listitem>
+ </itemizedlist>
+
+ <para>For each new OS release a new set of the latter three files would be added, each time with an
+ updated version. The <filename>SHA256SUMS</filename> manifest should then be updated accordingly,
+ listing all files for all versions that shall be offered for download.</para>
+ </example>
+
+ <example>
+ <title>Updates for Plain Directory Container Image</title>
+
+ <para><programlisting>
+[Source]
+Type=url-tar
+Path=https://download.example.com/
+MatchPattern=myContainer_@v.tar.gz
+
+[Target]
+Type=subvolume
+Path=/var/lib/machines
+MatchPattern=myContainer_@v
+CurrentSymlink=myContainer</programlisting></para>
+
+ <para>On updates this downloads <literal>https://download.example.com/myContainer_@v.tar.gz</literal>
+ and decompresses/unpacks it to <filename>/var/lib/machines/myContainer_@v</filename>. After each update
+ a symlink <filename>/var/lib/machines/myContainer</filename> is created/updated always pointing to the
+ most recent update.</para>
+ </example>
+ </refsect1>
+
+ <refsect1>
+ <title>See Also</title>
+ <para>
+ <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+ <citerefentry><refentrytitle>systemd-sysupdate</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
+ <citerefentry><refentrytitle>systemd-repart</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+ </para>
+ </refsect1>
+
+</refentry>
diff --git a/meson.build b/meson.build
index 107192b211..05dcc79cfa 100644
--- a/meson.build
+++ b/meson.build
@@ -1644,6 +1644,18 @@ conf.set('DEFAULT_DNSSEC_MODE',
'DNSSEC_' + default_dnssec.underscorify().to_upper())
conf.set_quoted('DEFAULT_DNSSEC_MODE_STR', default_dnssec)
+want_sysupdate = get_option('sysupdate')
+if want_sysupdate != 'false'
+ have = (conf.get('HAVE_OPENSSL') == 1 and
+ conf.get('HAVE_LIBFDISK') == 1)
+ if want_sysupdate == 'true' and not have
+ error('sysupdate support was requested, but dependencies are not available')
+ endif
+else
+ have = false
+endif
+conf.set10('ENABLE_SYSUPDATE', have)
+
want_importd = get_option('importd')
if want_importd != 'false'
have = (conf.get('HAVE_LIBCURL') == 1 and
@@ -2006,6 +2018,7 @@ subdir('src/rpm')
subdir('src/shutdown')
subdir('src/sysext')
subdir('src/systemctl')
+subdir('src/sysupdate')
subdir('src/timedate')
subdir('src/timesync')
subdir('src/tmpfiles')
@@ -3074,6 +3087,22 @@ if conf.get('ENABLE_REPART') == 1
endif
endif
+if conf.get('ENABLE_SYSUPDATE') == 1
+ exe = executable(
+ 'systemd-sysupdate',
+ systemd_sysupdate_sources,
+ include_directories : includes,
+ link_with : [libshared],
+ dependencies : [threads,
+ libblkid,
+ libfdisk,
+ libopenssl],
+ install_rpath : rootlibexecdir,
+ install : true,
+ install_dir : rootlibexecdir)
+ public_programs += exe
+endif
+
if conf.get('ENABLE_VCONSOLE') == 1
executable(
'systemd-vconsole-setup',
@@ -4117,6 +4146,7 @@ foreach tuple : [
['rfkill'],
['sysext'],
['systemd-analyze', conf.get('ENABLE_ANALYZE') == 1],
+ ['sysupdate'],
['sysusers'],
['timedated'],
['timesyncd'],
diff --git a/meson_options.txt b/meson_options.txt
index 284109cadf..27cfa9b697 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -100,6 +100,8 @@ option('binfmt', type : 'boolean',
description : 'support for custom binary formats')
option('repart', type : 'combo', choices : ['auto', 'true', 'false'],
description : 'install the systemd-repart tool')
+option('sysupdate', type : 'combo', choices : ['auto', 'true', 'false'],
+ description : 'install the systemd-sysupdate tool')
option('coredump', type : 'boolean',
description : 'install the coredump handler')
option('pstore', type : 'boolean',
diff --git a/src/basic/strv.h b/src/basic/strv.h
index 985499272f..bc76a2861c 100644
--- a/src/basic/strv.h
+++ b/src/basic/strv.h
@@ -133,8 +133,8 @@ bool strv_overlap(char * const *a, char * const *b) _pure_;
size_t _len = strv_length(h); \
_len > 0 ? h + _len - 1 : NULL; \
}); \
- i && (s = i) >= h; \
- i--)
+ (s = i); \
+ i > h ? i-- : (i = NULL))
#define STRV_FOREACH_BACKWARDS(s, l) \
_STRV_FOREACH_BACKWARDS(s, l, UNIQ_T(h, UNIQ), UNIQ_T(i, UNIQ))
diff --git a/src/sysupdate/meson.build b/src/sysupdate/meson.build
new file mode 100644
index 0000000000..2b1a256026
--- /dev/null
+++ b/src/sysupdate/meson.build
@@ -0,0 +1,22 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+systemd_sysupdate_sources = files('''
+ sysupdate-instance.c
+ sysupdate-instance.h
+ sysupdate-partition.c
+ sysupdate-partition.h
+ sysupdate-pattern.c
+ sysupdate-pattern.h
+ sysupdate-resource.c
+ sysupdate-resource.h
+ sysupdate-transfer.c
+ sysupdate-transfer.h
+ sysupdate-update-set.c
+ sysupdate-update-set.h
+ sysupdate-util.c
+ sysupdate-util.h
+ sysupdate-cache.c
+ sysupdate-cache.h
+ sysupdate.c
+ sysupdate.h
+'''.split())
diff --git a/src/sysupdate/sysupdate-cache.c b/src/sysupdate/sysupdate-cache.c
new file mode 100644
index 0000000000..8dad3ee479
--- /dev/null
+++ b/src/sysupdate/sysupdate-cache.c
@@ -0,0 +1,88 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "memory-util.h"
+#include "sysupdate-cache.h"
+
+#define WEB_CACHE_ENTRIES_MAX 64U
+#define WEB_CACHE_ITEM_SIZE_MAX (64U*1024U*1024U)
+
+static WebCacheItem* web_cache_item_free(WebCacheItem *i) {
+ if (!i)
+ return NULL;
+
+ free(i->url);
+ return mfree(i);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(WebCacheItem*, web_cache_item_free);
+
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(web_cache_hash_ops, char, string_hash_func, string_compare_func, WebCacheItem, web_cache_item_free);
+
+int web_cache_add_item(
+ Hashmap **web_cache,
+ const char *url,
+ bool verified,
+ const void *data,
+ size_t size) {
+
+ _cleanup_(web_cache_item_freep) WebCacheItem *item = NULL;
+ _cleanup_free_ char *u = NULL;
+ int r;
+
+ assert(web_cache);
+ assert(url);
+ assert(data || size == 0);
+
+ if (size > WEB_CACHE_ITEM_SIZE_MAX)
+ return -E2BIG;
+
+ item = web_cache_get_item(*web_cache, url, verified);
+ if (item && memcmp_nn(item->data, item->size, data, size) == 0)
+ return 0;
+
+ if (hashmap_size(*web_cache) >= (size_t) (WEB_CACHE_ENTRIES_MAX + !!hashmap_get(*web_cache, url)))
+ return -ENOSPC;
+
+ r = hashmap_ensure_allocated(web_cache, &web_cache_hash_ops);
+ if (r < 0)
+ return r;
+
+ u = strdup(url);
+ if (!u)
+ return -ENOMEM;
+
+ item = malloc(offsetof(WebCacheItem, data) + size + 1);
+ if (!item)
+ return -ENOMEM;
+
+ *item = (WebCacheItem) {
+ .url = TAKE_PTR(u),
+ .size = size,
+ .verified = verified,
+ };
+
+ /* Just to be extra paranoid, let's NUL terminate the downloaded buffer */
+ *(uint8_t*) mempcpy(item->data, data, size) = 0;
+
+ web_cache_item_free(hashmap_remove(*web_cache, url));
+
+ r = hashmap_put(*web_cache, item->url, item);
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(item);
+ return 1;
+}
+
+WebCacheItem* web_cache_get_item(Hashmap *web_cache, const char *url, bool verified) {
+ WebCacheItem *i;
+
+ i = hashmap_get(web_cache, url);
+ if (!i)
+ return NULL;
+
+ if (i->verified != verified)
+ return NULL;
+
+ return i;
+}
diff --git a/src/sysupdate/sysupdate-cache.h b/src/sysupdate/sysupdate-cache.h
new file mode 100644
index 0000000000..d6a7897399
--- /dev/null
+++ b/src/sysupdate/sysupdate-cache.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "hashmap.h"
+
+typedef struct WebCacheItem {
+ char *url;
+ bool verified;
+ size_t size;
+ uint8_t data[];
+} WebCacheItem;
+
+/* A simple in-memory cache for downloaded manifests. Very likely multiple transfers will use the same
+ * manifest URLs, hence let's make sure we only download them once within each sysupdate invocation. */
+
+int web_cache_add_item(Hashmap **cache, const char *url, bool verified, const void *data, size_t size);
+
+WebCacheItem* web_cache_get_item(Hashmap *cache, const char *url, bool verified);
diff --git a/src/sysupdate/sysupdate-instance.c b/src/sysupdate/sysupdate-instance.c
new file mode 100644
index 0000000000..16bfab912f
--- /dev/null
+++ b/src/sysupdate/sysupdate-instance.c
@@ -0,0 +1,63 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <sys/stat.h>
+
+#include "sysupdate-instance.h"
+
+void instance_metadata_destroy(InstanceMetadata *m) {
+ assert(m);
+ free(m->version);
+}
+
+int instance_new(
+ Resource *rr,
+ const char *path,
+ const InstanceMetadata *f,
+ Instance **ret) {
+
+ _cleanup_(instance_freep) Instance *i = NULL;
+ _cleanup_free_ char *p = NULL, *v = NULL;
+
+ assert(rr);
+ assert(path);
+ assert(f);
+ assert(f->version);
+ assert(ret);
+
+ p = strdup(path);
+ if (!p)
+ return log_oom();
+
+ v = strdup(f->version);
+ if (!v)
+ return log_oom();
+
+ i = new(Instance, 1);
+ if (!i)
+ return log_oom();
+
+ *i = (Instance) {
+ .resource = rr,
+ .metadata = *f,
+ .path = TAKE_PTR(p),
+ .partition_info = PARTITION_INFO_NULL,
+ };
+
+ i->metadata.version = TAKE_PTR(v);
+
+ *ret = TAKE_PTR(i);
+ return 0;
+}
+
+Instance *instance_free(Instance *i) {
+ if (!i)
+ return NULL;
+
+ instance_metadata_destroy(&i->metadata);
+
+ free(i->path);
+ partition_info_destroy(&i->partition_info);
+
+ return mfree(i);
+}
diff --git a/src/sysupdate/sysupdate-instance.h b/src/sysupdate/sysupdate-instance.h
new file mode 100644
index 0000000000..2860d295da
--- /dev/null
+++ b/src/sysupdate/sysupdate-instance.h
@@ -0,0 +1,67 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+#include <stdbool.h>
+#include <sys/types.h>
+
+#include "sd-id128.h"
+
+#include "fs-util.h"
+#include "time-util.h"
+
+typedef struct InstanceMetadata InstanceMetadata;
+typedef struct Instance Instance;
+
+#include "sysupdate-resource.h"
+#include "sysupdate-partition.h"
+
+struct InstanceMetadata {
+ /* Various bits of metadata for each instance, that is either derived from the filename/GPT label or
+ * from metadata of the file/partition itself */
+ char *version;
+ sd_id128_t partition_uuid;
+ bool partition_uuid_set;
+ uint64_t partition_flags; /* GPT partition flags */
+ bool partition_flags_set;
+ usec_t mtime;
+ mode_t mode;
+ uint64_t size; /* uncompressed size of the file */
+ uint64_t tries_done, tries_left; /* for boot assessment counters */
+ int no_auto;
+ int read_only;
+ int growfs;
+ uint8_t sha256sum[32]; /* SHA256 sum of the download (i.e. compressed) file */
+ bool sha256sum_set;
+};
+
+#define INSTANCE_METADATA_NULL \
+ { \
+ .mtime = USEC_INFINITY, \
+ .mode = MODE_INVALID, \
+ .size = UINT64_MAX, \
+ .tries_done = UINT64_MAX, \
+ .tries_left = UINT64_MAX, \
+ .no_auto = -1, \
+ .read_only = -1, \
+ .growfs = -1, \
+ }
+
+struct Instance {
+ /* A pointer back to the resource this belongs to */
+ Resource *resource;
+
+ /* Metadata of this version */
+ InstanceMetadata metadata;
+
+ /* Where we found the instance */
+ char *path;
+ PartitionInfo partition_info;
+};
+
+void instance_metadata_destroy(InstanceMetadata *m);
+
+int instance_new(Resource *rr, const char *path, const InstanceMetadata *f, Instance **ret);
+Instance *instance_free(Instance *i);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Instance*, instance_free);
diff --git a/src/sysupdate/sysupdate-partition.c b/src/sysupdate/sysupdate-partition.c
new file mode 100644
index 0000000000..f3e21001e4
--- /dev/null
+++ b/src/sysupdate/sysupdate-partition.c
@@ -0,0 +1,379 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/file.h>
+
+#include "alloc-util.h"
+#include "extract-word.h"
+#include "gpt.h"
+#include "id128-util.h"
+#include "parse-util.h"
+#include "stdio-util.h"
+#include "string-util.h"
+#include "sysupdate-partition.h"
+#include "util.h"
+
+void partition_info_destroy(PartitionInfo *p) {
+ assert(p);
+
+ p->label = mfree(p->label);
+ p->device = mfree(p->device);
+}
+
+static int fdisk_partition_get_attrs_as_uint64(
+ struct fdisk_partition *pa,
+ uint64_t *ret) {
+
+ uint64_t flags = 0;
+ const char *a;
+ int r;
+
+ assert(pa);
+ assert(ret);
+
+ /* Retrieve current flags as uint64_t mask */
+
+ a = fdisk_partition_get_attrs(pa);
+ if (!a) {
+ *ret = 0;
+ return 0;
+ }
+
+ for (;;) {
+ _cleanup_free_ char *word = NULL;
+
+ r = extract_first_word(&a, &word, ",", EXTRACT_DONT_COALESCE_SEPARATORS);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+
+ if (streq(word, "RequiredPartition"))
+ flags |= GPT_FLAG_REQUIRED_PARTITION;
+ else if (streq(word, "NoBlockIOProtocol"))
+ flags |= GPT_FLAG_NO_BLOCK_IO_PROTOCOL;
+ else if (streq(word, "LegacyBIOSBootable"))
+ flags |= GPT_FLAG_LEGACY_BIOS_BOOTABLE;
+ else {
+ const char *e;
+ unsigned u;
+
+ /* Drop "GUID" prefix if specified */
+ e = startswith(word, "GUID:") ?: word;
+
+ if (safe_atou(e, &u) < 0) {
+ log_debug("Unknown partition flag '%s', ignoring.", word);
+ continue;
+ }
+
+ if (u >= sizeof(flags)*8) { /* partition flags on GPT are 64bit. Let's ignore any further
+ bits should libfdisk report them */
+ log_debug("Partition flag above bit 63 (%s), ignoring.", word);
+ continue;
+ }
+
+ flags |= UINT64_C(1) << u;
+ }
+ }
+
+ *ret = flags;
+ return 0;
+}
+
+static int fdisk_partition_set_attrs_as_uint64(
+ struct fdisk_partition *pa,
+ uint64_t flags) {
+
+ _cleanup_free_ char *attrs = NULL;
+ int r;
+
+ assert(pa);
+
+ for (unsigned i = 0; i < sizeof(flags) * 8; i++) {
+ if (!FLAGS_SET(flags, UINT64_C(1) << i))
+ continue;
+
+ r = strextendf_with_separator(&attrs, ",", "%u", i);
+ if (r < 0)
+ return r;
+ }
+
+ return fdisk_partition_set_attrs(pa, strempty(attrs));
+}
+
+int read_partition_info(
+ struct fdisk_context *c,
+ struct fdisk_table *t,
+ size_t i,
+ PartitionInfo *ret) {
+
+ _cleanup_free_ char *label_copy = NULL, *device = NULL;
+ const char *pts, *ids, *label;
+ struct fdisk_partition *p;
+ struct fdisk_parttype *pt;
+ uint64_t start, size, flags;
+ sd_id128_t ptid, id;
+ size_t partno;
+ int r;
+
+ assert(c);
+ assert(t);
+ assert(ret);
+
+ p = fdisk_table_get_partition(t, i);
+ if (!p)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to read partition metadata: %m");
+
+ if (fdisk_partition_is_used(p) <= 0) {
+ *ret = (PartitionInfo) PARTITION_INFO_NULL;
+ return 0; /* not found! */
+ }
+
+ if (fdisk_partition_has_partno(p) <= 0 ||
+ fdisk_partition_has_start(p) <= 0 ||
+ fdisk_partition_has_size(p) <= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Found a partition without a number, position or size.");
+
+ partno = fdisk_partition_get_partno(p);
+
+ start = fdisk_partition_get_start(p);
+ assert(start <= UINT64_MAX / 512U);
+ start *= 512U;
+
+ size = fdisk_partition_get_size(p);
+ assert(size <= UINT64_MAX / 512U);
+ size *= 512U;
+
+ label = fdisk_partition_get_name(p);
+ if (!label)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Found a partition without a label.");
+
+ pt = fdisk_partition_get_type(p);
+ if (!pt)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to acquire type of partition: %m");
+
+ pts = fdisk_parttype_get_string(pt);
+ if (!pts)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to acquire type of partition as string: %m");
+
+ r = sd_id128_from_string(pts, &ptid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse partition type UUID %s: %m", pts);
+
+ ids = fdisk_partition_get_uuid(p);
+ if (!ids)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Found a partition without a UUID.");
+
+ r = sd_id128_from_string(ids, &id);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse partition UUID %s: %m", ids);
+
+ r = fdisk_partition_get_attrs_as_uint64(p, &flags);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get partition flags: %m");
+
+ r = fdisk_partition_to_string(p, c, FDISK_FIELD_DEVICE, &device);
+ if (r != 0)
+ return log_error_errno(r, "Failed to get partition device name: %m");
+
+ label_copy = strdup(label);
+ if (!label_copy)
+ return log_oom();
+
+ *ret = (PartitionInfo) {
+ .partno = partno,
+ .start = start,
+ .size = size,
+ .flags = flags,
+ .type = ptid,
+ .uuid = id,
+ .label = TAKE_PTR(label_copy),
+ .device = TAKE_PTR(device),
+ .no_auto = FLAGS_SET(flags, GPT_FLAG_NO_AUTO) && gpt_partition_type_knows_no_auto(ptid),
+ .read_only = FLAGS_SET(flags, GPT_FLAG_READ_ONLY) && gpt_partition_type_knows_read_only(ptid),
+ .growfs = FLAGS_SET(flags, GPT_FLAG_GROWFS) && gpt_partition_type_knows_growfs(ptid),
+ };
+
+ return 1; /* found! */
+}
+
+int find_suitable_partition(
+ const char *device,
+ uint64_t space,
+ sd_id128_t *partition_type,
+ PartitionInfo *ret) {
+
+ _cleanup_(partition_info_destroy) PartitionInfo smallest = PARTITION_INFO_NULL;
+ _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL;
+ _cleanup_(fdisk_unref_tablep) struct fdisk_table *t = NULL;
+ size_t n_partitions;
+ int r;
+
+ assert(device);
+ assert(ret);
+
+ c = fdisk_new_context();
+ if (!c)
+ return log_oom();
+
+ r = fdisk_assign_device(c, device, /* readonly= */ true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open device '%s': %m", device);
+
+ if (!fdisk_is_labeltype(c, FDISK_DISKLABEL_GPT))
+ return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON), "Disk %s has no GPT disk label, not suitable.", device);
+
+ r = fdisk_get_partitions(c, &t);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire partition table: %m");
+
+ n_partitions = fdisk_table_get_nents(t);
+ for (size_t i = 0; i < n_partitions; i++) {
+ _cleanup_(partition_info_destroy) PartitionInfo pinfo = PARTITION_INFO_NULL;
+
+ r = read_partition_info(c, t, i, &pinfo);
+ if (r < 0)
+ return r;
+ if (r == 0) /* not assigned */
+ continue;
+
+ /* Filter out non-matching partition types */
+ if (partition_type && !sd_id128_equal(pinfo.type, *partition_type))
+ continue;
+
+ if (!streq_ptr(pinfo.label, "_empty")) /* used */
+ continue;
+
+ if (space != UINT64_MAX && pinfo.size < space) /* too small */
+ continue;
+
+ if (smallest.partno != SIZE_MAX && smallest.size <= pinfo.size) /* already found smaller */
+ continue;
+
+ smallest = pinfo;
+ pinfo = (PartitionInfo) PARTITION_INFO_NULL;
+ }
+
+ if (smallest.partno == SIZE_MAX)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOSPC), "No available partition of a suitable size found.");
+
+ *ret = smallest;
+ smallest = (PartitionInfo) PARTITION_INFO_NULL;
+
+ return 0;
+}
+
+int patch_partition(
+ const char *device,
+ const PartitionInfo *info,
+ PartitionChange change) {
+
+ _cleanup_(fdisk_unref_partitionp) struct fdisk_partition *pa = NULL;
+ _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL;
+ bool tweak_no_auto, tweak_read_only, tweak_growfs;
+ int r, fd;
+
+ assert(device);
+ assert(info);
+ assert(change <= _PARTITION_CHANGE_MAX);
+
+ if (change == 0) /* Nothing to do */
+ return 0;
+
+ c = fdisk_new_context();
+ if (!c)
+ return log_oom();
+
+ r = fdisk_assign_device(c, device, /* readonly= */ false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open device '%s': %m", device);
+
+ assert_se((fd = fdisk_get_devfd(c)) >= 0);
+
+ /* Make sure udev doesn't read the device while we make changes (this lock is released automatically
+ * by the kernel when the fd is closed, i.e. when the fdisk context is freed, hence no explicit
+ * unlock by us here anywhere.) */
+ if (flock(fd, LOCK_EX) < 0)
+ return log_error_errno(errno, "Failed to lock block device '%s': %m", device);
+
+ if (!fdisk_is_labeltype(c, FDISK_DISKLABEL_GPT))
+ return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON), "Disk %s has no GPT disk label, not suitable.", device);
+
+ r = fdisk_get_partition(c, info->partno, &pa);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read partition %zu of GPT label of '%s': %m", info->partno, device);
+
+ if (change & PARTITION_LABEL) {
+ r = fdisk_partition_set_name(pa, info->label);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update partition label: %m");
+ }
+
+ if (change & PARTITION_UUID) {
+ r = fdisk_partition_set_uuid(pa, SD_ID128_TO_UUID_STRING(info->uuid));
+ if (r < 0)
+ return log_error_errno(r, "Failed to update partition UUID: %m");
+ }
+
+ /* Tweak the read-only flag, but only if supported by the partition type */
+ tweak_no_auto =
+ FLAGS_SET(change, PARTITION_NO_AUTO) &&
+ gpt_partition_type_knows_no_auto(info->type);
+ tweak_read_only =
+ FLAGS_SET(change, PARTITION_READ_ONLY) &&
+ gpt_partition_type_knows_read_only(info->type);
+ tweak_growfs =
+ FLAGS_SET(change, PARTITION_GROWFS) &&
+ gpt_partition_type_knows_growfs(info->type);
+
+ if (change & PARTITION_FLAGS) {
+ uint64_t flags;
+
+ /* Update the full flags parameter, and import the read-only flag into it */
+
+ flags = info->flags;
+ if (tweak_no_auto)
+ SET_FLAG(flags, GPT_FLAG_NO_AUTO, info->no_auto);
+ if (tweak_read_only)
+ SET_FLAG(flags, GPT_FLAG_READ_ONLY, info->read_only);
+ if (tweak_growfs)
+ SET_FLAG(flags, GPT_FLAG_GROWFS, info->growfs);
+
+ r = fdisk_partition_set_attrs_as_uint64(pa, flags);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update partition flags: %m");
+
+ } else if (tweak_no_auto || tweak_read_only || tweak_growfs) {
+ uint64_t old_flags, new_flags;
+
+ /* So we aren't supposed to update the full flags parameter, but we are supposed to update
+ * the RO flag of it. */
+
+ r = fdisk_partition_get_attrs_as_uint64(pa, &old_flags);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get old partition flags: %m");
+
+ new_flags = old_flags;
+ if (tweak_no_auto)
+ SET_FLAG(new_flags, GPT_FLAG_NO_AUTO, info->no_auto);
+ if (tweak_read_only)
+ SET_FLAG(new_flags, GPT_FLAG_READ_ONLY, info->read_only);
+ if (tweak_growfs)
+ SET_FLAG(new_flags, GPT_FLAG_GROWFS, info->growfs);
+
+ if (new_flags != old_flags) {
+ r = fdisk_partition_set_attrs_as_uint64(pa, new_flags);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update partition flags: %m");
+ }
+ }
+
+ r = fdisk_set_partition(c, info->partno, pa);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update partition: %m");
+
+ r = fdisk_write_disklabel(c);
+ if (r < 0)
+ return log_error_errno(r, "Failed to write updated partition table: %m");
+
+ return 0;
+}
diff --git a/src/sysupdate/sysupdate-partition.h b/src/sysupdate/sysupdate-partition.h
new file mode 100644
index 0000000000..672eb93e90
--- /dev/null
+++ b/src/sysupdate/sysupdate-partition.h
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+#include <sys/types.h>
+
+#include "sd-id128.h"
+
+#include "fdisk-util.h"
+#include "macro.h"
+
+typedef struct PartitionInfo PartitionInfo;
+
+typedef enum PartitionChange {
+ PARTITION_FLAGS = 1 << 0,
+ PARTITION_NO_AUTO = 1 << 1,
+ PARTITION_READ_ONLY = 1 << 2,
+ PARTITION_GROWFS = 1 << 3,
+ PARTITION_UUID = 1 << 4,
+ PARTITION_LABEL = 1 << 5,
+ _PARTITION_CHANGE_MAX = (1 << 6) - 1, /* all of the above */
+ _PARTITION_CHANGE_INVALID = -EINVAL,
+} PartitionChange;
+
+struct PartitionInfo {
+ size_t partno;
+ uint64_t start, size;
+ uint64_t flags;
+ sd_id128_t type, uuid;
+ char *label;
+ char *device; /* Note that this might point to some non-existing path in case we operate on a loopback file */
+ bool no_auto:1;
+ bool read_only:1;
+ bool growfs:1;
+};
+
+#define PARTITION_INFO_NULL \
+ { \
+ .partno = SIZE_MAX, \
+ .start = UINT64_MAX, \
+ .size = UINT64_MAX, \
+ }
+
+void partition_info_destroy(PartitionInfo *p);
+
+int read_partition_info(struct fdisk_context *c, struct fdisk_table *t, size_t i, PartitionInfo *ret);
+
+int find_suitable_partition(const char *device, uint64_t space, sd_id128_t *partition_type, PartitionInfo *ret);
+int patch_partition(const char *device, const PartitionInfo *info, PartitionChange change);
diff --git a/src/sysupdate/sysupdate-pattern.c b/src/sysupdate/sysupdate-pattern.c
new file mode 100644
index 0000000000..c9228687f7
--- /dev/null
+++ b/src/sysupdate/sysupdate-pattern.c
@@ -0,0 +1,602 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "hexdecoct.h"
+#include "list.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "stdio-util.h"
+#include "string-util.h"
+#include "sysupdate-pattern.h"
+#include "sysupdate-util.h"
+
+typedef enum PatternElementType {
+ PATTERN_LITERAL,
+ PATTERN_VERSION,
+ PATTERN_PARTITION_UUID,
+ PATTERN_PARTITION_FLAGS,
+ PATTERN_MTIME,
+ PATTERN_MODE,
+ PATTERN_SIZE,
+ PATTERN_TRIES_DONE,
+ PATTERN_TRIES_LEFT,
+ PATTERN_NO_AUTO,
+ PATTERN_READ_ONLY,
+ PATTERN_GROWFS,
+ PATTERN_SHA256SUM,
+ _PATTERN_ELEMENT_TYPE_MAX,
+ _PATTERN_ELEMENT_TYPE_INVALID = -EINVAL,
+} PatternElementType;
+
+typedef struct PatternElement PatternElement;
+
+struct PatternElement {
+ PatternElementType type;
+ LIST_FIELDS(PatternElement, elements);
+ char literal[];
+};
+
+static PatternElement *pattern_element_free_all(PatternElement *e) {
+ PatternElement *p;
+
+ while ((p = LIST_POP(elements, e)))
+ free(p);
+
+ return NULL;
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(PatternElement*, pattern_element_free_all);
+
+static PatternElementType pattern_element_type_from_char(char c) {
+ switch (c) {
+ case 'v':
+ return PATTERN_VERSION;
+ case 'u':
+ return PATTERN_PARTITION_UUID;
+ case 'f':
+ return PATTERN_PARTITION_FLAGS;
+ case 't':
+ return PATTERN_MTIME;
+ case 'm':
+ return PATTERN_MODE;
+ case 's':
+ return PATTERN_SIZE;
+ case 'd':
+ return PATTERN_TRIES_DONE;
+ case 'l':
+ return PATTERN_TRIES_LEFT;
+ case 'a':
+ return PATTERN_NO_AUTO;
+ case 'r':
+ return PATTERN_READ_ONLY;
+ case 'g':
+ return PATTERN_GROWFS;
+ case 'h':
+ return PATTERN_SHA256SUM;
+ default:
+ return _PATTERN_ELEMENT_TYPE_INVALID;
+ }
+}
+
+static bool valid_char(char x) {
+
+ /* Let's refuse control characters here, and let's reserve some characters typically used in pattern
+ * languages so that we can use them later, possibly. */
+
+ if ((unsigned) x < ' ' || x >= 127)
+ return false;
+
+ return !IN_SET(x, '$', '*', '?', '[', ']', '!', '\\', '/', '|');
+}
+
+static int pattern_split(
+ const char *pattern,
+ PatternElement **ret) {
+
+ _cleanup_(pattern_element_free_allp) PatternElement *first = NULL;
+ bool at = false, last_literal = true;
+ PatternElement *last = NULL;
+ uint64_t mask_found = 0;
+ size_t l, k = 0;
+
+ assert(pattern);
+
+ l = strlen(pattern);
+
+ for (const char *e = pattern; *e != 0; e++) {
+ if (*e == '@') {
+ if (!at) {
+ at = true;
+ continue;
+ }
+
+ /* Two at signs in a sequence, write out one */
+ at = false;
+
+ } else if (at) {
+ PatternElementType t;
+ uint64_t bit;
+
+ t = pattern_element_type_from_char(*e);
+ if (t < 0)
+ return log_debug_errno(t, "Unknown pattern field marker '@%c'.", *e);
+
+ bit = UINT64_C(1) << t;
+ if (mask_found & bit)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Pattern field marker '@%c' appears twice in pattern.", *e);
+
+ /* We insist that two pattern field markers are separated by some literal string that
+ * we can use to separate the fields when parsing. */
+ if (!last_literal)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Found two pattern field markers without separating literal.");
+
+ if (ret) {
+ PatternElement *z;
+
+ z = malloc(offsetof(PatternElement, literal));
+ if (!z)
+ return -ENOMEM;
+
+ z->type = t;
+ LIST_INSERT_AFTER(elements, first, last, z);
+ last = z;
+ }
+
+ mask_found |= bit;
+ last_literal = at = false;
+ continue;
+ }
+
+ if (!valid_char(*e))
+ return log_debug_errno(SYNTHETIC_ERRNO(EBADRQC), "Invalid character 0x%0x in pattern, refusing.", *e);
+
+ last_literal = true;
+
+ if (!ret)
+ continue;
+
+ if (!last || last->type != PATTERN_LITERAL) {
+ PatternElement *z;
+
+ z = malloc0(offsetof(PatternElement, literal) + l + 1); /* l is an upper bound to all literal elements */
+ if (!z)
+ return -ENOMEM;
+
+ z->type = PATTERN_LITERAL;
+ k = 0;
+
+ LIST_INSERT_AFTER(elements, first, last, z);
+ last = z;
+ }
+
+ assert(last);
+ assert(last->type == PATTERN_LITERAL);
+
+ last->literal[k++] = *e;
+ }
+
+ if (at)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Trailing @ character found, refusing.");
+ if (!(mask_found & (UINT64_C(1) << PATTERN_VERSION)))
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Version field marker '@v' not specified in pattern, refusing.");
+
+ if (ret)
+ *ret = TAKE_PTR(first);
+
+ return 0;
+}
+
+int pattern_match(const char *pattern, const char *s, InstanceMetadata *ret) {
+ _cleanup_(instance_metadata_destroy) InstanceMetadata found = INSTANCE_METADATA_NULL;
+ _cleanup_(pattern_element_free_allp) PatternElement *elements = NULL;
+ const char *p;
+ int r;
+
+ assert(pattern);
+ assert(s);
+
+ r = pattern_split(pattern, &elements);
+ if (r < 0)
+ return r;
+
+ p = s;
+ LIST_FOREACH(elements, e, elements) {
+ _cleanup_free_ char *t = NULL;
+ const char *n;
+
+ if (e->type == PATTERN_LITERAL) {
+ const char *k;
+
+ /* Skip literal fields */
+ k = startswith(p, e->literal);
+ if (!k)
+ goto nope;
+
+ p = k;
+ continue;
+ }
+
+ if (e->elements_next) {
+ /* The next element must be literal, as we use it to determine where to split */
+ assert(e->elements_next->type == PATTERN_LITERAL);
+
+ n = strstr(p, e->elements_next->literal);
+ if (!n)
+ goto nope;
+
+ } else
+ /* End of the string */
+ assert_se(n = strchr(p, 0));
+ t = strndup(p, n - p);
+ if (!t)
+ return -ENOMEM;
+
+ switch (e->type) {
+
+ case PATTERN_VERSION:
+ if (!version_is_valid(t)) {
+ log_debug("Version string is not valid, refusing: %s", t);
+ goto nope;
+ }
+
+ assert(!found.version);
+ found.version = TAKE_PTR(t);
+ break;
+
+ case PATTERN_PARTITION_UUID: {
+ sd_id128_t id;
+
+ if (sd_id128_from_string(t, &id) < 0)
+ goto nope;
+
+ assert(!found.partition_uuid_set);
+ found.partition_uuid = id;
+ found.partition_uuid_set = true;
+ break;
+ }
+
+ case PATTERN_PARTITION_FLAGS: {
+ uint64_t f;
+
+ if (safe_atoux64(t, &f) < 0)
+ goto nope;
+
+ if (found.partition_flags_set && found.partition_flags != f)
+ goto nope;
+
+ assert(!found.partition_flags_set);
+ found.partition_flags = f;
+ found.partition_flags_set = true;
+ break;
+ }
+
+ case PATTERN_MTIME: {
+ uint64_t v;
+
+ if (safe_atou64(t, &v) < 0)
+ goto nope;
+ if (v == USEC_INFINITY) /* Don't permit our internal special infinity value */
+ goto nope;
+ if (v / 1000000U > TIME_T_MAX) /* Make sure this fits in a timespec structure */
+ goto nope;
+
+ assert(found.mtime == USEC_INFINITY);
+ found.mtime = v;
+ break;
+ }
+
+ case PATTERN_MODE: {
+ mode_t m;
+
+ r = parse_mode(t, &m);
+ if (r < 0)
+ goto nope;
+ if (m & ~0775) /* Don't allow world-writable files or suid files to be generated this way */
+ goto nope;
+
+ assert(found.mode == MODE_INVALID);
+ found.mode = m;
+ break;
+ }
+
+ case PATTERN_SIZE: {
+ uint64_t u;
+
+ r = safe_atou64(t, &u);
+ if (r < 0)
+ goto nope;
+ if (u == UINT64_MAX)
+ goto nope;
+
+ assert(found.size == UINT64_MAX);
+ found.size = u;
+ break;
+ }
+
+ case PATTERN_TRIES_DONE: {
+ uint64_t u;
+
+ r = safe_atou64(t, &u);
+ if (r < 0)
+ goto nope;
+ if (u == UINT64_MAX)
+ goto nope;
+
+ assert(found.tries_done == UINT64_MAX);
+ found.tries_done = u;
+ break;
+ }
+
+ case PATTERN_TRIES_LEFT: {
+ uint64_t u;
+
+ r = safe_atou64(t, &u);
+ if (r < 0)
+ goto nope;
+ if (u == UINT64_MAX)
+ goto nope;
+
+ assert(found.tries_left == UINT64_MAX);
+ found.tries_left = u;
+ break;
+ }
+
+ case PATTERN_NO_AUTO:
+ r = parse_boolean(t);
+ if (r < 0)
+ goto nope;
+
+ assert(found.no_auto < 0);
+ found.no_auto = r;
+ break;
+
+ case PATTERN_READ_ONLY:
+ r = parse_boolean(t);
+ if (r < 0)
+ goto nope;
+
+ assert(found.read_only < 0);
+ found.read_only = r;
+ break;
+
+ case PATTERN_GROWFS:
+ r = parse_boolean(t);
+ if (r < 0)
+ goto nope;
+
+ assert(found.growfs < 0);
+ found.growfs = r;
+ break;
+
+ case PATTERN_SHA256SUM: {
+ _cleanup_free_ void *d = NULL;
+ size_t l;
+
+ if (strlen(t) != sizeof(found.sha256sum) * 2)
+ goto nope;
+
+ r = unhexmem(t, sizeof(found.sha256sum) * 2, &d, &l);
+ if (r == -ENOMEM)
+ return r;
+ if (r < 0)
+ goto nope;
+
+ assert(!found.sha256sum_set);
+ assert(l == sizeof(found.sha256sum));
+ memcpy(found.sha256sum, d, l);
+ found.sha256sum_set = true;
+ break;
+ }
+
+ default:
+ assert_se("unexpected pattern element");
+ }
+
+ p = n;
+ }
+
+ if (ret) {
+ *ret = found;
+ found = (InstanceMetadata) INSTANCE_METADATA_NULL;
+ }
+
+ return true;
+
+nope:
+ if (ret)
+ *ret = (InstanceMetadata) INSTANCE_METADATA_NULL;
+
+ return false;
+}
+
+int pattern_match_many(char **patterns, const char *s, InstanceMetadata *ret) {
+ _cleanup_(instance_metadata_destroy) InstanceMetadata found = INSTANCE_METADATA_NULL;
+ int r;
+
+ STRV_FOREACH(p, patterns) {
+ r = pattern_match(*p, s, &found);
+ if (r < 0)
+ return r;
+ if (r > 0) {
+ if (ret) {
+ *ret = found;
+ found = (InstanceMetadata) INSTANCE_METADATA_NULL;
+ }
+
+ return true;
+ }
+ }
+
+ if (ret)
+ *ret = (InstanceMetadata) INSTANCE_METADATA_NULL;
+
+ return false;
+}
+
+int pattern_valid(const char *pattern) {
+ int r;
+
+ r = pattern_split(pattern, NULL);
+ if (r == -EINVAL)
+ return false;
+ if (r < 0)
+ return r;
+
+ return true;
+}
+
+int pattern_format(
+ const char *pattern,
+ const InstanceMetadata *fields,
+ char **ret) {
+
+ _cleanup_(pattern_element_free_allp) PatternElement *elements = NULL;
+ _cleanup_free_ char *j = NULL;
+ int r;
+
+ assert(pattern);
+ assert(fields);
+ assert(ret);
+
+ r = pattern_split(pattern, &elements);
+ if (r < 0)
+ return r;
+
+ LIST_FOREACH(elements, e, elements) {
+
+ switch (e->type) {
+
+ case PATTERN_LITERAL:
+ if (!strextend(&j, e->literal))
+ return -ENOMEM;
+
+ break;
+
+ case PATTERN_VERSION:
+ if (!fields->version)
+ return -ENXIO;
+
+ if (!strextend(&j, fields->version))
+ return -ENOMEM;
+ break;
+
+ case PATTERN_PARTITION_UUID: {
+ char formatted[SD_ID128_STRING_MAX];
+
+ if (!fields->partition_uuid_set)
+ return -ENXIO;
+
+ if (!strextend(&j, sd_id128_to_string(fields->partition_uuid, formatted)))
+ return -ENOMEM;
+
+ break;
+ }
+
+ case PATTERN_PARTITION_FLAGS:
+ if (!fields->partition_flags_set)
+ return -ENXIO;
+
+ r = strextendf(&j, "%" PRIx64, fields->partition_flags);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case PATTERN_MTIME:
+ if (fields->mtime == USEC_INFINITY)
+ return -ENXIO;
+
+ r = strextendf(&j, "%" PRIu64, fields->mtime);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case PATTERN_MODE:
+ if (fields->mode == MODE_INVALID)
+ return -ENXIO;
+
+ r = strextendf(&j, "%03o", fields->mode);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case PATTERN_SIZE:
+ if (fields->size == UINT64_MAX)
+ return -ENXIO;
+
+ r = strextendf(&j, "%" PRIu64, fields->size);
+ if (r < 0)
+ return r;
+ break;
+
+ case PATTERN_TRIES_DONE:
+ if (fields->tries_done == UINT64_MAX)
+ return -ENXIO;
+
+ r = strextendf(&j, "%" PRIu64, fields->tries_done);
+ if (r < 0)
+ return r;
+ break;
+
+ case PATTERN_TRIES_LEFT:
+ if (fields->tries_left == UINT64_MAX)
+ return -ENXIO;
+
+ r = strextendf(&j, "%" PRIu64, fields->tries_left);
+ if (r < 0)
+ return r;
+ break;
+
+ case PATTERN_NO_AUTO:
+ if (fields->no_auto < 0)
+ return -ENXIO;
+
+ if (!strextend(&j, one_zero(fields->no_auto)))
+ return -ENOMEM;
+
+ break;
+
+ case PATTERN_READ_ONLY:
+ if (fields->read_only < 0)
+ return -ENXIO;
+
+ if (!strextend(&j, one_zero(fields->read_only)))
+ return -ENOMEM;
+
+ break;
+
+ case PATTERN_GROWFS:
+ if (fields->growfs < 0)
+ return -ENXIO;
+
+ if (!strextend(&j, one_zero(fields->growfs)))
+ return -ENOMEM;
+
+ break;
+
+ case PATTERN_SHA256SUM: {
+ _cleanup_free_ char *h = NULL;
+
+ if (!fields->sha256sum_set)
+ return -ENXIO;
+
+ h = hexmem(fields->sha256sum, sizeof(fields->sha256sum));
+ if (!h)
+ return -ENOMEM;
+
+ if (!strextend(&j, h))
+ return -ENOMEM;
+
+ break;
+ }
+
+ default:
+ assert_not_reached();
+ }
+ }
+
+ *ret = TAKE_PTR(j);
+ return 0;
+}
diff --git a/src/sysupdate/sysupdate-pattern.h b/src/sysupdate/sysupdate-pattern.h
new file mode 100644
index 0000000000..1c60fa0250
--- /dev/null
+++ b/src/sysupdate/sysupdate-pattern.h
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <stdbool.h>
+
+#include "sysupdate-instance.h"
+#include "time-util.h"
+
+int pattern_match(const char *pattern, const char *s, InstanceMetadata *ret);
+int pattern_match_many(char **patterns, const char *s, InstanceMetadata *ret);
+int pattern_valid(const char *pattern);
+int pattern_format(const char *pattern, const InstanceMetadata *fields, char **ret);
diff --git a/src/sysupdate/sysupdate-resource.c b/src/sysupdate/sysupdate-resource.c
new file mode 100644
index 0000000000..97d8973f71
--- /dev/null
+++ b/src/sysupdate/sysupdate-resource.c
@@ -0,0 +1,633 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "blockdev-util.h"
+#include "chase-symlinks.h"
+#include "dirent-util.h"
+#include "env-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "glyph-util.h"
+#include "gpt.h"
+#include "hexdecoct.h"
+#include "import-util.h"
+#include "macro.h"
+#include "process-util.h"
+#include "sort-util.h"
+#include "stat-util.h"
+#include "string-table.h"
+#include "sysupdate-cache.h"
+#include "sysupdate-instance.h"
+#include "sysupdate-pattern.h"
+#include "sysupdate-resource.h"
+#include "sysupdate.h"
+#include "utf8.h"
+
+void resource_destroy(Resource *rr) {
+ assert(rr);
+
+ free(rr->path);
+ strv_free(rr->patterns);
+
+ for (size_t i = 0; i < rr->n_instances; i++)
+ instance_free(rr->instances[i]);
+ free(rr->instances);
+}
+
+static int resource_add_instance(
+ Resource *rr,
+ const char *path,
+ const InstanceMetadata *f,
+ Instance **ret) {
+
+ Instance *i;
+ int r;
+
+ assert(rr);
+ assert(path);
+ assert(f);
+ assert(f->version);
+
+ if (!GREEDY_REALLOC(rr->instances, rr->n_instances + 1))
+ return log_oom();
+
+ r = instance_new(rr, path, f, &i);
+ if (r < 0)
+ return r;
+
+ rr->instances[rr->n_instances++] = i;
+
+ if (ret)
+ *ret = i;
+
+ return 0;
+}
+
+static int resource_load_from_directory(
+ Resource *rr,
+ mode_t m) {
+
+ _cleanup_(closedirp) DIR *d = NULL;
+ int r;
+
+ assert(rr);
+ assert(IN_SET(rr->type, RESOURCE_TAR, RESOURCE_REGULAR_FILE, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME));
+ assert(IN_SET(m, S_IFREG, S_IFDIR));
+
+ d = opendir(rr->path);
+ if (!d) {
+ if (errno == ENOENT) {
+ log_debug("Directory %s does not exist, not loading any resources.", rr->path);
+ return 0;
+ }
+
+ return log_error_errno(errno, "Failed to open directory '%s': %m", rr->path);
+ }
+
+ for (;;) {
+ _cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL;
+ _cleanup_free_ char *joined = NULL;
+ Instance *instance;
+ struct dirent *de;
+ struct stat st;
+
+ errno = 0;
+ de = readdir_no_dot(d);
+ if (!de) {
+ if (errno != 0)
+ return log_error_errno(errno, "Failed to read directory '%s': %m", rr->path);
+ break;
+ }
+
+ switch (de->d_type) {
+
+ case DT_UNKNOWN:
+ break;
+
+ case DT_DIR:
+ if (m != S_IFDIR)
+ continue;
+
+ break;
+
+ case DT_REG:
+ if (m != S_IFREG)
+ continue;
+ break;
+
+ default:
+ continue;
+ }
+
+ if (fstatat(dirfd(d), de->d_name, &st, AT_NO_AUTOMOUNT) < 0) {
+ if (errno == ENOENT) /* Gone by now? */
+ continue;
+
+ return log_error_errno(errno, "Failed to stat %s/%s: %m", rr->path, de->d_name);
+ }
+
+ if ((st.st_mode & S_IFMT) != m)
+ continue;
+
+ r = pattern_match_many(rr->patterns, de->d_name, &extracted_fields);
+ if (r < 0)
+ return log_error_errno(r, "Failed to match pattern: %m");
+ if (r == 0)
+ continue;
+
+ joined = path_join(rr->path, de->d_name);
+ if (!joined)
+ return log_oom();
+
+ r = resource_add_instance(rr, joined, &extracted_fields, &instance);
+ if (r < 0)
+ return r;
+
+ /* Inherit these from the source, if not explicitly overwritten */
+ if (instance->metadata.mtime == USEC_INFINITY)
+ instance->metadata.mtime = timespec_load(&st.st_mtim) ?: USEC_INFINITY;
+
+ if (instance->metadata.mode == MODE_INVALID)
+ instance->metadata.mode = st.st_mode & 0775; /* mask out world-writability and suid and stuff, for safety */
+ }
+
+ return 0;
+}
+
+static int resource_load_from_blockdev(Resource *rr) {
+ _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL;
+ _cleanup_(fdisk_unref_tablep) struct fdisk_table *t = NULL;
+ size_t n_partitions;
+ int r;
+
+ assert(rr);
+
+ c = fdisk_new_context();
+ if (!c)
+ return log_oom();
+
+ r = fdisk_assign_device(c, rr->path, /* readonly= */ true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open device '%s': %m", rr->path);
+
+ if (!fdisk_is_labeltype(c, FDISK_DISKLABEL_GPT))
+ return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON), "Disk %s has no GPT disk label, not suitable.", rr->path);
+
+ r = fdisk_get_partitions(c, &t);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire partition table: %m");
+
+ n_partitions = fdisk_table_get_nents(t);
+ for (size_t i = 0; i < n_partitions; i++) {
+ _cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL;
+ _cleanup_(partition_info_destroy) PartitionInfo pinfo = PARTITION_INFO_NULL;
+ Instance *instance;
+
+ r = read_partition_info(c, t, i, &pinfo);
+ if (r < 0)
+ return r;
+ if (r == 0) /* not assigned */
+ continue;
+
+ /* Check if partition type matches */
+ if (rr->partition_type_set && !sd_id128_equal(pinfo.type, rr->partition_type))
+ continue;
+
+ /* A label of "_empty" means "not used so far" for us */
+ if (streq_ptr(pinfo.label, "_empty")) {
+ rr->n_empty++;
+ continue;
+ }
+
+ r = pattern_match_many(rr->patterns, pinfo.label, &extracted_fields);
+ if (r < 0)
+ return log_error_errno(r, "Failed to match pattern: %m");
+ if (r == 0)
+ continue;
+
+ r = resource_add_instance(rr, pinfo.device, &extracted_fields, &instance);
+ if (r < 0)
+ return r;
+
+ instance->partition_info = pinfo;
+ pinfo = (PartitionInfo) PARTITION_INFO_NULL;
+
+ /* Inherit data from source if not configured explicitly */
+ if (!instance->metadata.partition_uuid_set) {
+ instance->metadata.partition_uuid = instance->partition_info.uuid;
+ instance->metadata.partition_uuid_set = true;
+ }
+
+ if (!instance->metadata.partition_flags_set) {
+ instance->metadata.partition_flags = instance->partition_info.flags;
+ instance->metadata.partition_flags_set = true;
+ }
+
+ if (instance->metadata.read_only < 0)
+ instance->metadata.read_only = instance->partition_info.read_only;
+ }
+
+ return 0;
+}
+
+static int download_manifest(
+ const char *url,
+ bool verify_signature,
+ char **ret_buffer,
+ size_t *ret_size) {
+
+ _cleanup_free_ char *buffer = NULL, *suffixed_url = NULL;
+ _cleanup_(close_pairp) int pfd[2] = { -1, -1 };
+ _cleanup_fclose_ FILE *manifest = NULL;
+ size_t size = 0;
+ pid_t pid;
+ int r;
+
+ assert(url);
+ assert(ret_buffer);
+ assert(ret_size);
+
+ /* Download a SHA256SUMS file as manifest */
+
+ r = import_url_append_component(url, "SHA256SUMS", &suffixed_url);
+ if (r < 0)
+ return log_error_errno(r, "Failed to append SHA256SUMS to URL: %m");
+
+ if (pipe2(pfd, O_CLOEXEC) < 0)
+ return log_error_errno(errno, "Failed to allocate pipe: %m");
+
+ log_info("%s Acquiring manifest file %s…", special_glyph(SPECIAL_GLYPH_DOWNLOAD), suffixed_url);
+
+ r = safe_fork("(sd-pull)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_LOG, &pid);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ /* Child */
+
+ const char *cmdline[] = {
+ "systemd-pull",
+ "raw",
+ "--direct", /* just download the specified URL, don't download anything else */
+ "--verify", verify_signature ? "signature" : "no", /* verify the manifest file */
+ suffixed_url,
+ "-", /* write to stdout */
+ NULL
+ };
+
+ pfd[0] = safe_close(pfd[0]);
+
+ r = rearrange_stdio(-1, pfd[1], STDERR_FILENO);
+ if (r < 0) {
+ log_error_errno(r, "Failed to rearrange stdin/stdout: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ (void) unsetenv("NOTIFY_SOCKET");
+ execv(pull_binary_path(), (char *const*) cmdline);
+ log_error_errno(errno, "Failed to execute %s tool: %m", pull_binary_path());
+ _exit(EXIT_FAILURE);
+ };
+
+ pfd[1] = safe_close(pfd[1]);
+
+ /* We'll first load the entire manifest into memory before parsing it. That's because the
+ * systemd-pull tool can validate the download only after its completion, but still pass the data to
+ * us as it runs. We thus need to check the return value of the process *before* parsing, to be
+ * reasonably safe. */
+
+ manifest = fdopen(pfd[0], "r");
+ if (!manifest)
+ return log_error_errno(errno, "Failed allocate FILE object for manifest file: %m");
+
+ TAKE_FD(pfd[0]);
+
+ r = read_full_stream(manifest, &buffer, &size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read manifest file from child: %m");
+
+ manifest = safe_fclose(manifest);
+
+ r = wait_for_terminate_and_check("(sd-pull)", pid, WAIT_LOG);
+ if (r < 0)
+ return r;
+ if (r != 0)
+ return -EPROTO;
+
+ *ret_buffer = TAKE_PTR(buffer);
+ *ret_size = size;
+
+ return 0;
+}
+
+static int resource_load_from_web(
+ Resource *rr,
+ bool verify,
+ Hashmap **web_cache) {
+
+ size_t manifest_size = 0, left = 0;
+ _cleanup_free_ char *buf = NULL;
+ const char *manifest, *p;
+ size_t line_nr = 1;
+ WebCacheItem *ci;
+ int r;
+
+ assert(rr);
+
+ ci = web_cache ? web_cache_get_item(*web_cache, rr->path, verify) : NULL;
+ if (ci) {
+ log_debug("Manifest web cache hit for %s.", rr->path);
+
+ manifest = (char*) ci->data;
+ manifest_size = ci->size;
+ } else {
+ log_debug("Manifest web cache miss for %s.", rr->path);
+
+ r = download_manifest(rr->path, verify, &buf, &manifest_size);
+ if (r < 0)
+ return r;
+
+ manifest = buf;
+ }
+
+ if (memchr(manifest, 0, manifest_size))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Manifest file has embedded NUL byte, refusing.");
+ if (!utf8_is_valid_n(manifest, manifest_size))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Manifest file is not valid UTF-8, refusing.");
+
+ p = manifest;
+ left = manifest_size;
+
+ while (left > 0) {
+ _cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL;
+ _cleanup_free_ char *fn = NULL;
+ _cleanup_free_ void *h = NULL;
+ Instance *instance;
+ const char *e;
+ size_t hlen;
+
+ /* 64 character hash + separator + filename + newline */
+ if (left < 67)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Corrupt manifest at line %zu, refusing.", line_nr);
+
+ if (p[0] == '\\')
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "File names with escapes not supported in manifest at line %zu, refusing.", line_nr);
+
+ r = unhexmem(p, 64, &h, &hlen);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse digest at manifest line %zu, refusing.", line_nr);
+
+ p += 64, left -= 64;
+
+ if (*p != ' ')
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing space separator at manifest line %zu, refusing.", line_nr);
+ p++, left--;
+
+ if (!IN_SET(*p, '*', ' '))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing binary/text input marker at manifest line %zu, refusing.", line_nr);
+ p++, left--;
+
+ e = memchr(p, '\n', left);
+ if (!e)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Truncated manifest file at line %zu, refusing.", line_nr);
+ if (e == p)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Empty filename specified at manifest line %zu, refusing.", line_nr);
+
+ fn = strndup(p, e - p);
+ if (!fn)
+ return log_oom();
+
+ if (!filename_is_valid(fn))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid filename specified at manifest line %zu, refusing.", line_nr);
+ if (string_has_cc(fn, NULL))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Filename contains control characters at manifest line %zu, refusing.", line_nr);
+
+ r = pattern_match_many(rr->patterns, fn, &extracted_fields);
+ if (r < 0)
+ return log_error_errno(r, "Failed to match pattern: %m");
+ if (r > 0) {
+ _cleanup_free_ char *path = NULL;
+
+ r = import_url_append_component(rr->path, fn, &path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to build instance URL: %m");
+
+ r = resource_add_instance(rr, path, &extracted_fields, &instance);
+ if (r < 0)
+ return r;
+
+ assert(hlen == sizeof(instance->metadata.sha256sum));
+
+ if (instance->metadata.sha256sum_set) {
+ if (memcmp(instance->metadata.sha256sum, h, hlen) != 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "SHA256 sum parsed from filename and manifest don't match at line %zu, refusing.", line_nr);
+ } else {
+ memcpy(instance->metadata.sha256sum, h, hlen);
+ instance->metadata.sha256sum_set = true;
+ }
+ }
+
+ left -= (e - p) + 1;
+ p = e + 1;
+
+ line_nr++;
+ }
+
+ if (!ci && web_cache) {
+ r = web_cache_add_item(web_cache, rr->path, verify, manifest, manifest_size);
+ if (r < 0)
+ log_debug_errno(r, "Failed to add manifest '%s' to cache, ignoring: %m", rr->path);
+ else
+ log_debug("Added manifest '%s' to cache.", rr->path);
+ }
+
+ return 0;
+}
+
+static int instance_cmp(Instance *const*a, Instance *const*b) {
+ int r;
+
+ assert(a);
+ assert(b);
+ assert(*a);
+ assert(*b);
+ assert((*a)->metadata.version);
+ assert((*b)->metadata.version);
+
+ /* Newest version at the beginning */
+ r = strverscmp_improved((*a)->metadata.version, (*b)->metadata.version);
+ if (r != 0)
+ return -r;
+
+ /* Instances don't have to be uniquely named (uniqueness on partition tables is not enforced at all,
+ * and since we allow multiple matching patterns not even in directories they are unique). Hence
+ * let's order by path as secondary ordering key. */
+ return path_compare((*a)->path, (*b)->path);
+}
+
+int resource_load_instances(Resource *rr, bool verify, Hashmap **web_cache) {
+ int r;
+
+ assert(rr);
+
+ switch (rr->type) {
+
+ case RESOURCE_TAR:
+ case RESOURCE_REGULAR_FILE:
+ r = resource_load_from_directory(rr, S_IFREG);
+ break;
+
+ case RESOURCE_DIRECTORY:
+ case RESOURCE_SUBVOLUME:
+ r = resource_load_from_directory(rr, S_IFDIR);
+ break;
+
+ case RESOURCE_PARTITION:
+ r = resource_load_from_blockdev(rr);
+ break;
+
+ case RESOURCE_URL_FILE:
+ case RESOURCE_URL_TAR:
+ r = resource_load_from_web(rr, verify, web_cache);
+ break;
+
+ default:
+ assert_not_reached();
+ }
+ if (r < 0)
+ return r;
+
+ typesafe_qsort(rr->instances, rr->n_instances, instance_cmp);
+ return 0;
+}
+
+Instance* resource_find_instance(Resource *rr, const char *version) {
+ Instance key = {
+ .metadata.version = (char*) version,
+ }, *k = &key;
+
+ return typesafe_bsearch(&k, rr->instances, rr->n_instances, instance_cmp);
+}
+
+int resource_resolve_path(
+ Resource *rr,
+ const char *root,
+ const char *node) {
+
+ _cleanup_free_ char *p = NULL;
+ dev_t d;
+ int r;
+
+ assert(rr);
+
+ if (rr->path_auto) {
+
+ /* NB: we don't actually check the backing device of the root fs "/", but of "/usr", in order
+ * to support environments where the root fs is a tmpfs, and the OS itself placed exclusively
+ * in /usr/. */
+
+ if (rr->type != RESOURCE_PARTITION)
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "Automatic root path discovery only supported for partition resources.");
+
+ if (node) { /* If --image= is specified, directly use the loopback device */
+ r = free_and_strdup_warn(&rr->path, node);
+ if (r < 0)
+ return r;
+
+ return 0;
+ }
+
+ if (root)
+ return log_error_errno(SYNTHETIC_ERRNO(EPERM),
+ "Block device is not allowed when using --root= mode.");
+
+ r = get_block_device_harder("/usr/", &d);
+
+ } else if (rr->type == RESOURCE_PARTITION) {
+ _cleanup_close_ int fd = -1, real_fd = -1;
+ _cleanup_free_ char *resolved = NULL;
+ struct stat st;
+
+ r = chase_symlinks(rr->path, root, CHASE_PREFIX_ROOT, &resolved, &fd);
+ if (r < 0)
+ return log_error_errno(r, "Failed to resolve '%s': %m", rr->path);
+
+ if (fstat(fd, &st) < 0)
+ return log_error_errno(r, "Failed to stat '%s': %m", resolved);
+
+ if (S_ISBLK(st.st_mode) && root)
+ return log_error_errno(SYNTHETIC_ERRNO(EPERM), "When using --root= or --image= access to device nodes is prohibited.");
+
+ if (S_ISREG(st.st_mode) || S_ISBLK(st.st_mode)) {
+ /* Not a directory, hence no need to find backing block device for the path */
+ free_and_replace(rr->path, resolved);
+ return 0;
+ }
+
+ if (!S_ISDIR(st.st_mode))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTDIR), "Target path '%s' does not refer to regular file, directory or block device, refusing.", rr->path);
+
+ if (node) { /* If --image= is specified all file systems are backed by the same loopback device, hence shortcut things. */
+ r = free_and_strdup_warn(&rr->path, node);
+ if (r < 0)
+ return r;
+
+ return 0;
+ }
+
+ real_fd = fd_reopen(fd, O_RDONLY|O_CLOEXEC|O_DIRECTORY);
+ if (real_fd < 0)
+ return log_error_errno(real_fd, "Failed to convert O_PATH file descriptor for %s to regular file descriptor: %m", rr->path);
+
+ r = get_block_device_harder_fd(fd, &d);
+
+ } else if (RESOURCE_IS_FILESYSTEM(rr->type) && root) {
+ _cleanup_free_ char *resolved = NULL;
+
+ r = chase_symlinks(rr->path, root, CHASE_PREFIX_ROOT, &resolved, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to resolve '%s': %m", rr->path);
+
+ free_and_replace(rr->path, resolved);
+ return 0;
+ } else
+ return 0; /* Otherwise assume there's nothing to resolve */
+
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine block device of file system: %m");
+
+ r = block_get_whole_disk(d, &d);
+ if (r < 0)
+ return log_error_errno(r, "Failed to find whole disk device for partition backing file system: %m");
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "File system is not placed on a partition block device, cannot determine whole block device backing root file system.");
+
+ r = device_path_make_canonical(S_IFBLK, d, &p);
+ if (r < 0)
+ return r;
+
+ if (rr->path)
+ log_info("Automatically discovered block device '%s' from '%s'.", p, rr->path);
+ else
+ log_info("Automatically discovered root block device '%s'.", p);
+
+ free_and_replace(rr->path, p);
+ return 1;
+}
+
+static const char *resource_type_table[_RESOURCE_TYPE_MAX] = {
+ [RESOURCE_URL_FILE] = "url-file",
+ [RESOURCE_URL_TAR] = "url-tar",
+ [RESOURCE_TAR] = "tar",
+ [RESOURCE_PARTITION] = "partition",
+ [RESOURCE_REGULAR_FILE] = "regular-file",
+ [RESOURCE_DIRECTORY] = "directory",
+ [RESOURCE_SUBVOLUME] = "subvolume",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(resource_type, ResourceType);
diff --git a/src/sysupdate/sysupdate-resource.h b/src/sysupdate/sysupdate-resource.h
new file mode 100644
index 0000000000..86be0d3389
--- /dev/null
+++ b/src/sysupdate/sysupdate-resource.h
@@ -0,0 +1,97 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+#include <stdbool.h>
+#include <sys/types.h>
+
+#include "sd-id128.h"
+
+#include "hashmap.h"
+#include "macro.h"
+
+/* Forward declare this type so that the headers below can use it */
+typedef struct Resource Resource;
+
+#include "sysupdate-instance.h"
+
+typedef enum ResourceType {
+ RESOURCE_URL_FILE,
+ RESOURCE_URL_TAR,
+ RESOURCE_TAR,
+ RESOURCE_PARTITION,
+ RESOURCE_REGULAR_FILE,
+ RESOURCE_DIRECTORY,
+ RESOURCE_SUBVOLUME,
+ _RESOURCE_TYPE_MAX,
+ _RESOURCE_TYPE_INVALID = -EINVAL,
+} ResourceType;
+
+static inline bool RESOURCE_IS_SOURCE(ResourceType t) {
+ return IN_SET(t,
+ RESOURCE_URL_FILE,
+ RESOURCE_URL_TAR,
+ RESOURCE_TAR,
+ RESOURCE_REGULAR_FILE,
+ RESOURCE_DIRECTORY,
+ RESOURCE_SUBVOLUME);
+}
+
+static inline bool RESOURCE_IS_TARGET(ResourceType t) {
+ return IN_SET(t,
+ RESOURCE_PARTITION,
+ RESOURCE_REGULAR_FILE,
+ RESOURCE_DIRECTORY,
+ RESOURCE_SUBVOLUME);
+}
+
+/* Returns true for all resources that deal with file system objects, i.e. where we operate on top of the
+ * file system layer, instead of below. */
+static inline bool RESOURCE_IS_FILESYSTEM(ResourceType t) {
+ return IN_SET(t,
+ RESOURCE_TAR,
+ RESOURCE_REGULAR_FILE,
+ RESOURCE_DIRECTORY,
+ RESOURCE_SUBVOLUME);
+}
+
+static inline bool RESOURCE_IS_TAR(ResourceType t) {
+ return IN_SET(t,
+ RESOURCE_TAR,
+ RESOURCE_URL_TAR);
+}
+
+static inline bool RESOURCE_IS_URL(ResourceType t) {
+ return IN_SET(t,
+ RESOURCE_URL_TAR,
+ RESOURCE_URL_FILE);
+}
+
+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) */
+ char **patterns;
+ sd_id128_t partition_type;
+ bool partition_type_set;
+
+ /* All instances of this resource we found */
+ Instance **instances;
+ size_t n_instances;
+
+ /* If this is a partition resource (RESOURCE_PARTITION), then how many partition slots are currently unassigned, that we can use */
+ size_t n_empty;
+};
+
+void resource_destroy(Resource *rr);
+
+int resource_load_instances(Resource *rr, bool verify, Hashmap **web_cache);
+
+Instance* resource_find_instance(Resource *rr, const char *version);
+
+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_;
diff --git a/src/sysupdate/sysupdate-transfer.c b/src/sysupdate/sysupdate-transfer.c
new file mode 100644
index 0000000000..a9fceed601
--- /dev/null
+++ b/src/sysupdate/sysupdate-transfer.c
@@ -0,0 +1,1247 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-id128.h"
+
+#include "alloc-util.h"
+#include "blockdev-util.h"
+#include "chase-symlinks.h"
+#include "conf-parser.h"
+#include "dirent-util.h"
+#include "fd-util.h"
+#include "glyph-util.h"
+#include "gpt.h"
+#include "hexdecoct.h"
+#include "install-file.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "rm-rf.h"
+#include "specifier.h"
+#include "stat-util.h"
+#include "stdio-util.h"
+#include "strv.h"
+#include "sync-util.h"
+#include "sysupdate-pattern.h"
+#include "sysupdate-resource.h"
+#include "sysupdate-transfer.h"
+#include "sysupdate-util.h"
+#include "sysupdate.h"
+#include "tmpfile-util.h"
+#include "web-util.h"
+
+/* Default value for InstancesMax= for fs object targets */
+#define DEFAULT_FILE_INSTANCES_MAX 3
+
+Transfer *transfer_free(Transfer *t) {
+ if (!t)
+ return NULL;
+
+ t->temporary_path = rm_rf_subvolume_and_free(t->temporary_path);
+
+ free(t->definition_path);
+ free(t->min_version);
+ strv_free(t->protected_versions);
+ free(t->current_symlink);
+ free(t->final_path);
+
+ partition_info_destroy(&t->partition_info);
+
+ resource_destroy(&t->source);
+ resource_destroy(&t->target);
+
+ return mfree(t);
+}
+
+Transfer *transfer_new(void) {
+ Transfer *t;
+
+ t = new(Transfer, 1);
+ if (!t)
+ return NULL;
+
+ *t = (Transfer) {
+ .source.type = _RESOURCE_TYPE_INVALID,
+ .target.type = _RESOURCE_TYPE_INVALID,
+ .remove_temporary = true,
+ .mode = MODE_INVALID,
+ .tries_left = UINT64_MAX,
+ .tries_done = UINT64_MAX,
+ .verify = true,
+
+ /* the three flags, as configured by the user */
+ .no_auto = -1,
+ .read_only = -1,
+ .growfs = -1,
+
+ /* the read only flag, as ultimately determined */
+ .install_read_only = -1,
+
+ .partition_info = PARTITION_INFO_NULL,
+ };
+
+ return t;
+}
+
+static const Specifier specifier_table[] = {
+ COMMON_SYSTEM_SPECIFIERS,
+ COMMON_TMP_SPECIFIERS,
+ {}
+};
+
+static int config_parse_protect_version(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_free_ char *resolved = NULL;
+ char ***protected_versions = data;
+ int r;
+
+ assert(rvalue);
+ assert(data);
+
+ r = specifier_printf(rvalue, NAME_MAX, specifier_table, arg_root, NULL, &resolved);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to expand specifiers in ProtectVersion=, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ if (!version_is_valid(resolved)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "ProtectVersion= string is not valid, ignoring: %s", resolved);
+ return 0;
+ }
+
+ r = strv_extend(protected_versions, resolved);
+ if (r < 0)
+ return log_oom();
+
+ return 0;
+}
+
+static int config_parse_min_version(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_free_ char *resolved = NULL;
+ char **version = data;
+ int r;
+
+ assert(rvalue);
+ assert(data);
+
+ r = specifier_printf(rvalue, NAME_MAX, specifier_table, arg_root, NULL, &resolved);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to expand specifiers in MinVersion=, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ if (!version_is_valid(rvalue)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "MinVersion= string is not valid, ignoring: %s", resolved);
+ return 0;
+ }
+
+ return free_and_replace(*version, resolved);
+}
+
+static int config_parse_current_symlink(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_free_ char *resolved = NULL;
+ char **current_symlink = data;
+ int r;
+
+ assert(rvalue);
+ assert(data);
+
+ r = specifier_printf(rvalue, NAME_MAX, specifier_table, arg_root, NULL, &resolved);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to expand specifiers in CurrentSymlink=, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ r = path_simplify_and_warn(resolved, 0, unit, filename, line, lvalue);
+ if (r < 0)
+ return 0;
+
+ return free_and_replace(*current_symlink, resolved);
+}
+
+static int config_parse_instances_max(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ uint64_t *instances_max = data, i;
+ int r;
+
+ assert(rvalue);
+ assert(data);
+
+ if (isempty(rvalue)) {
+ *instances_max = 0; /* Revert to default logic, see transfer_read_definition() */
+ return 0;
+ }
+
+ r = safe_atou64(rvalue, &i);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse InstancesMax= value, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ if (i < 2) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "InstancesMax= value must be at least 2, bumping: %s", rvalue);
+ *instances_max = 2;
+ } else
+ *instances_max = i;
+
+ return 0;
+}
+
+static int config_parse_resource_pattern(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ char ***patterns = data;
+ int r;
+
+ assert(rvalue);
+ assert(data);
+
+ if (isempty(rvalue)) {
+ *patterns = strv_free(*patterns);
+ return 0;
+ }
+
+ for (;;) {
+ _cleanup_free_ char *word = NULL, *resolved = NULL;
+
+ r = extract_first_word(&rvalue, &word, NULL, EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_RELAX);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to extract first pattern from MatchPattern=, ignoring: %s", rvalue);
+ return 0;
+ }
+ if (r == 0)
+ break;
+
+ r = specifier_printf(word, NAME_MAX, specifier_table, arg_root, NULL, &resolved);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to expand specifiers in MatchPattern=, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ if (!pattern_valid(resolved))
+ return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL),
+ "MatchPattern= string is not valid, refusing: %s", resolved);
+
+ r = strv_consume(patterns, TAKE_PTR(resolved));
+ if (r < 0)
+ return log_oom();
+ }
+
+ strv_uniq(*patterns);
+ return 0;
+}
+
+static int config_parse_resource_path(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_free_ char *resolved = NULL;
+ Resource *rr = data;
+ int r;
+
+ assert(rvalue);
+ assert(data);
+
+ if (streq(rvalue, "auto")) {
+ rr->path_auto = true;
+ rr->path = mfree(rr->path);
+ return 0;
+ }
+
+ r = specifier_printf(rvalue, PATH_MAX-1, specifier_table, arg_root, NULL, &resolved);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to expand specifiers in Path=, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ /* Note that we don't validate the path as being absolute or normalized. We'll do that in
+ * transfer_read_definition() as we might not know yet whether Path refers to an URL or a file system
+ * path. */
+
+ rr->path_auto = false;
+ return free_and_replace(rr->path, resolved);
+}
+
+static DEFINE_CONFIG_PARSE_ENUM(config_parse_resource_type, resource_type, ResourceType, "Invalid resource type");
+
+static int config_parse_resource_ptype(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Resource *rr = data;
+ int r;
+
+ assert(rvalue);
+ assert(data);
+
+ r = gpt_partition_type_uuid_from_string(rvalue, &rr->partition_type);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed parse partition type, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ rr->partition_type_set = true;
+ return 0;
+}
+
+static int config_parse_partition_uuid(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Transfer *t = data;
+ int r;
+
+ assert(rvalue);
+ assert(data);
+
+ r = sd_id128_from_string(rvalue, &t->partition_uuid);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed parse partition UUID, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ t->partition_uuid_set = true;
+ return 0;
+}
+
+static int config_parse_partition_flags(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Transfer *t = data;
+ int r;
+
+ assert(rvalue);
+ assert(data);
+
+ r = safe_atou64(rvalue, &t->partition_flags);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed parse partition flags, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ t->partition_flags_set = true;
+ return 0;
+}
+
+int transfer_read_definition(Transfer *t, const char *path) {
+ int r;
+
+ assert(t);
+ 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 },
+ {}
+ };
+
+ r = config_parse(NULL, path, NULL,
+ "Transfer\0"
+ "Source\0"
+ "Target\0",
+ config_item_table_lookup, table,
+ CONFIG_PARSE_WARN,
+ t,
+ NULL);
+ if (r < 0)
+ return r;
+
+ if (!RESOURCE_IS_SOURCE(t->source.type))
+ return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+ "Source Type= must be one of url-file, url-tar, tar, regular-file, directory, subvolume.");
+
+ if (t->target.type < 0) {
+ switch (t->source.type) {
+
+ case RESOURCE_URL_FILE:
+ case RESOURCE_REGULAR_FILE:
+ t->target.type =
+ t->target.path && path_startswith(t->target.path, "/dev/") ?
+ RESOURCE_PARTITION : RESOURCE_REGULAR_FILE;
+ break;
+
+ case RESOURCE_URL_TAR:
+ case RESOURCE_TAR:
+ case RESOURCE_DIRECTORY:
+ t->target.type = RESOURCE_DIRECTORY;
+ break;
+
+ case RESOURCE_SUBVOLUME:
+ t->target.type = RESOURCE_SUBVOLUME;
+ break;
+
+ default:
+ assert_not_reached();
+ }
+ }
+
+ if (!RESOURCE_IS_TARGET(t->target.type))
+ return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+ "Target Type= must be one of partition, regular-file, directory, subvolume.");
+
+ if ((IN_SET(t->source.type, RESOURCE_URL_FILE, RESOURCE_PARTITION, RESOURCE_REGULAR_FILE) &&
+ !IN_SET(t->target.type, RESOURCE_PARTITION, RESOURCE_REGULAR_FILE)) ||
+ (IN_SET(t->source.type, RESOURCE_URL_TAR, RESOURCE_TAR, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME) &&
+ !IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME)))
+ return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+ "Target type '%s' is incompatible with source type '%s', refusing.",
+ resource_type_to_string(t->source.type), resource_type_to_string(t->target.type));
+
+ if (!t->source.path && !t->source.path_auto)
+ return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+ "Source specification lacks Path=.");
+
+ if (t->source.path) {
+ if (RESOURCE_IS_FILESYSTEM(t->source.type) || t->source.type == RESOURCE_PARTITION)
+ if (!path_is_absolute(t->source.path) || !path_is_normalized(t->source.path))
+ return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+ "Source path is not a normalized, absolute path: %s", t->source.path);
+
+ /* We unofficially support file:// in addition to http:// and https:// for url
+ * sources. That's mostly for testing, since it relieves us from having to set up a HTTP
+ * server, and CURL abstracts this away from us thankfully. */
+ if (RESOURCE_IS_URL(t->source.type))
+ if (!http_url_is_valid(t->source.path) && !file_url_is_valid(t->source.path))
+ return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+ "Source path is not a valid HTTP or HTTPS URL: %s", t->source.path);
+ }
+
+ if (strv_isempty(t->source.patterns))
+ return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+ "Source specification lacks MatchPattern=.");
+
+ if (!t->target.path && !t->target.path_auto)
+ return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+ "Target specification lacks Path= field.");
+
+ if (t->target.path &&
+ (!path_is_absolute(t->target.path) || !path_is_normalized(t->target.path)))
+ return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+ "Target path is not a normalized, absolute path: %s", t->target.path);
+
+ if (strv_isempty(t->target.patterns)) {
+ strv_free(t->target.patterns);
+ t->target.patterns = strv_copy(t->source.patterns);
+ if (!t->target.patterns)
+ return log_oom();
+ }
+
+ if (t->current_symlink && !RESOURCE_IS_FILESYSTEM(t->target.type) && !path_is_absolute(t->current_symlink))
+ return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+ "Current symlink must be absolute path if target is partition: %s", t->current_symlink);
+
+ /* When no instance limit is set, use all available partition slots in case of partitions, or 3 in case of fs objects */
+ if (t->instances_max == 0)
+ t->instances_max = t->target.type == RESOURCE_PARTITION ? UINT64_MAX : DEFAULT_FILE_INSTANCES_MAX;
+
+ return 0;
+}
+
+int transfer_resolve_paths(
+ Transfer *t,
+ const char *root,
+ const char *node) {
+
+ int r;
+
+ /* If Path=auto is used in [Source] or [Target] sections, let's automatically detect the path of the
+ * block device to use. Moreover, if this path points to a directory but we need a block device,
+ * automatically determine the backing block device, so that users can reference block devices by
+ * mount point. */
+
+ assert(t);
+
+ r = resource_resolve_path(&t->source, root, node);
+ if (r < 0)
+ return r;
+
+ r = resource_resolve_path(&t->target, root, node);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static void transfer_remove_temporary(Transfer *t) {
+ _cleanup_(closedirp) DIR *d = NULL;
+ int r;
+
+ assert(t);
+
+ if (!t->remove_temporary)
+ return;
+
+ if (!IN_SET(t->target.type, RESOURCE_REGULAR_FILE, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME))
+ return;
+
+ /* Removes all temporary files/dirs from previous runs in the target directory, i.e. all those starting with '.#' */
+
+ d = opendir(t->target.path);
+ if (!d) {
+ if (errno == ENOENT)
+ return;
+
+ log_debug_errno(errno, "Failed to open target directory '%s', ignoring: %m", t->target.path);
+ return;
+ }
+
+ for (;;) {
+ struct dirent *de;
+
+ errno = 0;
+ de = readdir_no_dot(d);
+ if (!de) {
+ if (errno != 0)
+ log_debug_errno(errno, "Failed to read target directory '%s', ignoring: %m", t->target.path);
+ break;
+ }
+
+ if (!startswith(de->d_name, ".#"))
+ continue;
+
+ r = rm_rf_child(dirfd(d), de->d_name, REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_CHMOD);
+ if (r == -ENOENT)
+ continue;
+ if (r < 0) {
+ log_warning_errno(r, "Failed to remove temporary resource instance '%s/%s', ignoring: %m", t->target.path, de->d_name);
+ continue;
+ }
+
+ log_debug("Removed temporary resource instance '%s/%s'.", t->target.path, de->d_name);
+ }
+}
+
+int transfer_vacuum(
+ Transfer *t,
+ uint64_t space,
+ const char *extra_protected_version) {
+
+ uint64_t instances_max, limit;
+ int r, count = 0;
+
+ assert(t);
+
+ transfer_remove_temporary(t);
+
+ /* First, calculate how many instances to keep, based on the instance limit — but keep at least one */
+
+ instances_max = arg_instances_max != UINT64_MAX ? arg_instances_max : t->instances_max;
+ assert(instances_max >= 1);
+ if (instances_max == UINT64_MAX) /* Keep infinite instances? */
+ limit = UINT64_MAX;
+ else if (space > instances_max)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOSPC),
+ "Asked to delete more instances than total maximum allowed number of instances, refusing.");
+ else if (space == instances_max)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOSPC),
+ "Asked to delete all possible instances, can't allow that. One instance must always remain.");
+ else
+ limit = instances_max - space;
+
+ if (t->target.type == RESOURCE_PARTITION) {
+ uint64_t rm, remain;
+
+ /* If we are looking at a partition table, we also have to take into account how many
+ * partition slots of the right type are available */
+
+ if (t->target.n_empty + t->target.n_instances < 2)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOSPC),
+ "Partition table has less than two partition slots of the right type " SD_ID128_UUID_FORMAT_STR " (%s), refusing.",
+ SD_ID128_FORMAT_VAL(t->target.partition_type),
+ gpt_partition_type_uuid_to_string(t->target.partition_type));
+ if (space > t->target.n_empty + t->target.n_instances)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOSPC),
+ "Partition table does not have enough partition slots of right type " SD_ID128_UUID_FORMAT_STR " (%s) for operation.",
+ SD_ID128_FORMAT_VAL(t->target.partition_type),
+ gpt_partition_type_uuid_to_string(t->target.partition_type));
+ if (space == t->target.n_empty + t->target.n_instances)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOSPC),
+ "Asked to empty all partition table slots of the right type " SD_ID128_UUID_FORMAT_STR " (%s), can't allow that. One instance must always remain.",
+ SD_ID128_FORMAT_VAL(t->target.partition_type),
+ gpt_partition_type_uuid_to_string(t->target.partition_type));
+
+ rm = LESS_BY(space, t->target.n_empty);
+ remain = LESS_BY(t->target.n_instances, rm);
+ limit = MIN(limit, remain);
+ }
+
+ while (t->target.n_instances > limit) {
+ Instance *oldest;
+ size_t p = t->target.n_instances - 1;
+
+ for (;;) {
+ oldest = t->target.instances[p];
+ assert(oldest);
+
+ /* If this is listed among the protected versions, then let's not remove it */
+ if (!strv_contains(t->protected_versions, oldest->metadata.version) &&
+ (!extra_protected_version || !streq(extra_protected_version, oldest->metadata.version)))
+ break;
+
+ log_debug("Version '%s' is protected, not removing.", oldest->metadata.version);
+ if (p == 0) {
+ oldest = NULL;
+ break;
+ }
+
+ p--;
+ }
+
+ if (!oldest) /* Nothing more to remove */
+ break;
+
+ assert(oldest->resource);
+
+ log_info("%s Removing old '%s' (%s).", special_glyph(SPECIAL_GLYPH_RECYCLING), oldest->path, resource_type_to_string(oldest->resource->type));
+
+ switch (t->target.type) {
+
+ case RESOURCE_REGULAR_FILE:
+ case RESOURCE_DIRECTORY:
+ case RESOURCE_SUBVOLUME:
+ r = rm_rf(oldest->path, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_MISSING_OK|REMOVE_CHMOD);
+ if (r < 0 && r != -ENOENT)
+ return log_error_errno(r, "Failed to make room, deleting '%s' failed: %m", oldest->path);
+
+ break;
+
+ case RESOURCE_PARTITION: {
+ PartitionInfo pinfo = oldest->partition_info;
+
+ /* label "_empty" means "no contents" for our purposes */
+ pinfo.label = (char*) "_empty";
+
+ r = patch_partition(t->target.path, &pinfo, PARTITION_LABEL);
+ if (r < 0)
+ return r;
+
+ t->target.n_empty++;
+ break;
+ }
+
+ default:
+ assert_not_reached();
+ break;
+ }
+
+ instance_free(oldest);
+ memmove(t->target.instances + p, t->target.instances + p + 1, (t->target.n_instances - p - 1) * sizeof(Instance*));
+ t->target.n_instances--;
+
+ count++;
+ }
+
+ return count;
+}
+
+static void compile_pattern_fields(
+ const Transfer *t,
+ const Instance *i,
+ InstanceMetadata *ret) {
+
+ assert(t);
+ assert(i);
+ assert(ret);
+
+ *ret = (InstanceMetadata) {
+ .version = i->metadata.version,
+
+ /* We generally prefer explicitly configured values for the transfer over those automatically
+ * derived from the source instance. Also, if the source is a tar archive, then let's not
+ * patch mtime/mode and use the one embedded in the tar file */
+ .partition_uuid = t->partition_uuid_set ? t->partition_uuid : i->metadata.partition_uuid,
+ .partition_uuid_set = t->partition_uuid_set || i->metadata.partition_uuid_set,
+ .partition_flags = t->partition_flags_set ? t->partition_flags : i->metadata.partition_flags,
+ .partition_flags_set = t->partition_flags_set || i->metadata.partition_flags_set,
+ .mtime = RESOURCE_IS_TAR(i->resource->type) ? USEC_INFINITY : i->metadata.mtime,
+ .mode = t->mode != MODE_INVALID ? t->mode : (RESOURCE_IS_TAR(i->resource->type) ? MODE_INVALID : i->metadata.mode),
+ .size = i->metadata.size,
+ .tries_done = t->tries_done != UINT64_MAX ? t->tries_done :
+ i->metadata.tries_done != UINT64_MAX ? i->metadata.tries_done : 0,
+ .tries_left = t->tries_left != UINT64_MAX ? t->tries_left :
+ i->metadata.tries_left != UINT64_MAX ? i->metadata.tries_left : 3,
+ .no_auto = t->no_auto >= 0 ? t->no_auto : i->metadata.no_auto,
+ .read_only = t->read_only >= 0 ? t->read_only : i->metadata.read_only,
+ .growfs = t->growfs >= 0 ? t->growfs : i->metadata.growfs,
+ .sha256sum_set = i->metadata.sha256sum_set,
+ };
+
+ memcpy(ret->sha256sum, i->metadata.sha256sum, sizeof(ret->sha256sum));
+}
+
+static int run_helper(
+ const char *name,
+ const char *path,
+ const char * const cmdline[]) {
+
+ int r;
+
+ assert(name);
+ assert(path);
+ assert(cmdline);
+
+ r = safe_fork(name, FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_LOG|FORK_WAIT, NULL);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ /* Child */
+
+ (void) unsetenv("NOTIFY_SOCKET");
+ execv(path, (char *const*) cmdline);
+ log_error_errno(errno, "Failed to execute %s tool: %m", path);
+ _exit(EXIT_FAILURE);
+ }
+
+ return 0;
+}
+
+int transfer_acquire_instance(Transfer *t, Instance *i) {
+ _cleanup_free_ char *formatted_pattern = NULL, *digest = NULL;
+ char offset[DECIMAL_STR_MAX(uint64_t)+1], max_size[DECIMAL_STR_MAX(uint64_t)+1];
+ const char *where = NULL;
+ InstanceMetadata f;
+ Instance *existing;
+ int r;
+
+ assert(t);
+ assert(i);
+ assert(i->resource);
+ assert(t == container_of(i->resource, Transfer, source));
+
+ /* Does this instance already exist in the target? Then we don't need to acquire anything */
+ existing = resource_find_instance(&t->target, i->metadata.version);
+ if (existing) {
+ log_info("No need to acquire '%s', already installed.", i->path);
+ return 0;
+ }
+
+ assert(!t->final_path);
+ assert(!t->temporary_path);
+ assert(!strv_isempty(t->target.patterns));
+
+ /* Format the target name using the first pattern specified */
+ compile_pattern_fields(t, i, &f);
+ r = pattern_format(t->target.patterns[0], &f, &formatted_pattern);
+ if (r < 0)
+ return log_error_errno(r, "Failed to format target pattern: %m");
+
+ if (RESOURCE_IS_FILESYSTEM(t->target.type)) {
+
+ if (!filename_is_valid(formatted_pattern))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as file name, refusing: %s", formatted_pattern);
+
+ t->final_path = path_join(t->target.path, formatted_pattern);
+ if (!t->final_path)
+ return log_oom();
+
+ r = tempfn_random(t->final_path, "sysupdate", &t->temporary_path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to generate temporary target path: %m");
+
+ where = t->final_path;
+ }
+
+ if (t->target.type == RESOURCE_PARTITION) {
+ r = gpt_partition_label_valid(formatted_pattern);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine if formatted pattern is suitable as GPT partition label: %s", formatted_pattern);
+ if (!r)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as GPT partition label, refusing: %s", formatted_pattern);
+
+ r = find_suitable_partition(
+ t->target.path,
+ i->metadata.size,
+ t->target.partition_type_set ? &t->target.partition_type : NULL,
+ &t->partition_info);
+ if (r < 0)
+ return r;
+
+ xsprintf(offset, "%" PRIu64, t->partition_info.start);
+ xsprintf(max_size, "%" PRIu64, t->partition_info.size);
+
+ where = t->partition_info.device;
+ }
+
+ assert(where);
+
+ log_info("%s Acquiring %s %s %s...", special_glyph(SPECIAL_GLYPH_DOWNLOAD), i->path, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), where);
+
+ if (RESOURCE_IS_URL(i->resource->type)) {
+ /* For URL sources we require the SHA256 sum to be known so that we can validate the
+ * download. */
+
+ if (!i->metadata.sha256sum_set)
+ return log_error_errno(r, "SHA256 checksum not known for download '%s', refusing.", i->path);
+
+ digest = hexmem(i->metadata.sha256sum, sizeof(i->metadata.sha256sum));
+ if (!digest)
+ return log_oom();
+ }
+
+ switch (i->resource->type) { /* Source */
+
+ case RESOURCE_REGULAR_FILE:
+
+ switch (t->target.type) { /* Target */
+
+ case RESOURCE_REGULAR_FILE:
+
+ /* regular file → regular file (why fork off systemd-import for such a simple file
+ * copy case? implicit decompression mostly, and thus also sandboxing. Also, the
+ * importer has some tricks up its sleeve, such as sparse file generation, which we
+ * want to take benefit of, too.) */
+
+ r = run_helper("(sd-import-raw)",
+ import_binary_path(),
+ (const char* const[]) {
+ "systemd-import",
+ "raw",
+ "--direct", /* just copy/unpack the specified file, don't do anything else */
+ arg_sync ? "--sync=yes" : "--sync=no",
+ i->path,
+ t->temporary_path,
+ NULL
+ });
+ break;
+
+ case RESOURCE_PARTITION:
+
+ /* regular file → partition */
+
+ r = run_helper("(sd-import-raw)",
+ import_binary_path(),
+ (const char* const[]) {
+ "systemd-import",
+ "raw",
+ "--direct", /* just copy/unpack the specified file, don't do anything else */
+ "--offset", offset,
+ "--size-max", max_size,
+ arg_sync ? "--sync=yes" : "--sync=no",
+ i->path,
+ t->target.path,
+ NULL
+ });
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ break;
+
+ case RESOURCE_DIRECTORY:
+ case RESOURCE_SUBVOLUME:
+ assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME));
+
+ /* directory/subvolume → directory/subvolume */
+
+ r = run_helper("(sd-import-fs)",
+ import_fs_binary_path(),
+ (const char* const[]) {
+ "systemd-import-fs",
+ "run",
+ "--direct", /* just untar the specified file, don't do anything else */
+ arg_sync ? "--sync=yes" : "--sync=no",
+ t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no",
+ i->path,
+ t->temporary_path,
+ NULL
+ });
+ break;
+
+ case RESOURCE_TAR:
+ assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME));
+
+ /* tar → directory/subvolume */
+
+ r = run_helper("(sd-import-tar)",
+ import_binary_path(),
+ (const char* const[]) {
+ "systemd-import",
+ "tar",
+ "--direct", /* just untar the specified file, don't do anything else */
+ arg_sync ? "--sync=yes" : "--sync=no",
+ t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no",
+ i->path,
+ t->temporary_path,
+ NULL
+ });
+ break;
+
+ case RESOURCE_URL_FILE:
+
+ switch (t->target.type) {
+
+ case RESOURCE_REGULAR_FILE:
+
+ /* url file → regular file */
+
+ r = run_helper("(sd-pull-raw)",
+ pull_binary_path(),
+ (const char* const[]) {
+ "systemd-pull",
+ "raw",
+ "--direct", /* just download the specified URL, don't download anything else */
+ "--verify", digest, /* validate by explicit SHA256 sum */
+ arg_sync ? "--sync=yes" : "--sync=no",
+ i->path,
+ t->temporary_path,
+ NULL
+ });
+ break;
+
+ case RESOURCE_PARTITION:
+
+ /* url file → partition */
+
+ r = run_helper("(sd-pull-raw)",
+ pull_binary_path(),
+ (const char* const[]) {
+ "systemd-pull",
+ "raw",
+ "--direct", /* just download the specified URL, don't download anything else */
+ "--verify", digest, /* validate by explicit SHA256 sum */
+ "--offset", offset,
+ "--size-max", max_size,
+ arg_sync ? "--sync=yes" : "--sync=no",
+ i->path,
+ t->target.path,
+ NULL
+ });
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ break;
+
+ case RESOURCE_URL_TAR:
+ assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME));
+
+ r = run_helper("(sd-pull-tar)",
+ pull_binary_path(),
+ (const char*const[]) {
+ "systemd-pull",
+ "tar",
+ "--direct", /* just download the specified URL, don't download anything else */
+ "--verify", digest, /* validate by explicit SHA256 sum */
+ t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no",
+ arg_sync ? "--sync=yes" : "--sync=no",
+ i->path,
+ t->temporary_path,
+ NULL
+ });
+ break;
+
+ default:
+ assert_not_reached();
+ }
+ if (r < 0)
+ return r;
+
+ if (RESOURCE_IS_FILESYSTEM(t->target.type)) {
+ bool need_sync = false;
+ assert(t->temporary_path);
+
+ /* Apply file attributes if set */
+ if (f.mtime != USEC_INFINITY) {
+ struct timespec ts;
+
+ 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);
+
+ need_sync = true;
+ }
+
+ if (f.mode != MODE_INVALID) {
+ /* 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);
+
+ need_sync = true;
+ }
+
+ /* Synchronize */
+ if (arg_sync && need_sync) {
+ if (t->target.type == RESOURCE_REGULAR_FILE)
+ r = fsync_path_and_parent_at(AT_FDCWD, t->temporary_path);
+ else {
+ assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME));
+ r = syncfs_path(AT_FDCWD, t->temporary_path);
+ }
+ if (r < 0)
+ return log_error_errno(r, "Failed to synchronize file system backing '%s': %m", t->temporary_path);
+ }
+
+ t->install_read_only = f.read_only;
+ }
+
+ if (t->target.type == RESOURCE_PARTITION) {
+ free_and_replace(t->partition_info.label, formatted_pattern);
+ t->partition_change = PARTITION_LABEL;
+
+ if (f.partition_uuid_set) {
+ t->partition_info.uuid = f.partition_uuid;
+ t->partition_change |= PARTITION_UUID;
+ }
+
+ if (f.partition_flags_set) {
+ t->partition_info.flags = f.partition_flags;
+ t->partition_change |= PARTITION_FLAGS;
+ }
+
+ if (f.no_auto >= 0) {
+ t->partition_info.no_auto = f.no_auto;
+ t->partition_change |= PARTITION_NO_AUTO;
+ }
+
+ if (f.read_only >= 0) {
+ t->partition_info.read_only = f.read_only;
+ t->partition_change |= PARTITION_READ_ONLY;
+ }
+
+ if (f.growfs >= 0) {
+ t->partition_info.growfs = f.growfs;
+ t->partition_change |= PARTITION_GROWFS;
+ }
+ }
+
+ /* For regular file cases the only step left is to install the file in place, which install_file()
+ * will do via rename(). For partition cases the only step left is to update the partition table,
+ * which is done at the same place. */
+
+ log_info("Successfully acquired '%s'.", i->path);
+ return 0;
+}
+
+int transfer_install_instance(
+ Transfer *t,
+ Instance *i,
+ const char *root) {
+
+ int r;
+
+ assert(t);
+ assert(i);
+ assert(i->resource);
+ assert(t == container_of(i->resource, Transfer, source));
+
+ if (t->temporary_path) {
+ assert(RESOURCE_IS_FILESYSTEM(t->target.type));
+ assert(t->final_path);
+
+ r = install_file(AT_FDCWD, t->temporary_path,
+ AT_FDCWD, t->final_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 place: %m", t->final_path);
+
+ log_info("Successfully installed '%s' (%s) as '%s' (%s).",
+ i->path,
+ resource_type_to_string(i->resource->type),
+ t->final_path,
+ resource_type_to_string(t->target.type));
+
+ t->temporary_path = mfree(t->temporary_path);
+ }
+
+ if (t->partition_change != 0) {
+ assert(t->target.type == RESOURCE_PARTITION);
+
+ r = patch_partition(
+ t->target.path,
+ &t->partition_info,
+ t->partition_change);
+ if (r < 0)
+ return r;
+
+ log_info("Successfully installed '%s' (%s) as '%s' (%s).",
+ i->path,
+ resource_type_to_string(i->resource->type),
+ t->partition_info.device,
+ resource_type_to_string(t->target.type));
+ }
+
+ if (t->current_symlink) {
+ _cleanup_free_ char *buf = NULL, *parent = NULL, *relative = NULL, *resolved = NULL;
+ const char *link_path, *link_target;
+ bool resolve_link_path = false;
+
+ if (RESOURCE_IS_FILESYSTEM(t->target.type)) {
+
+ assert(t->target.path);
+
+ if (path_is_absolute(t->current_symlink)) {
+ link_path = t->current_symlink;
+ resolve_link_path = true;
+ } else {
+ buf = path_make_absolute(t->current_symlink, t->target.path);
+ if (!buf)
+ return log_oom();
+
+ link_path = buf;
+ }
+
+ link_target = t->final_path;
+
+ } else if (t->target.type == RESOURCE_PARTITION) {
+
+ assert(path_is_absolute(t->current_symlink));
+
+ link_path = t->current_symlink;
+ link_target = t->partition_info.device;
+
+ resolve_link_path = true;
+ } else
+ assert_not_reached();
+
+ if (resolve_link_path && root) {
+ r = chase_symlinks(link_path, root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &resolved, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to resolve current symlink path '%s': %m", link_path);
+
+ link_path = resolved;
+ }
+
+ if (link_target) {
+ r = path_extract_directory(link_path, &parent);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract directory of target path '%s': %m", link_path);
+
+ r = path_make_relative(parent, link_target, &relative);
+ if (r < 0)
+ return log_error_errno(r, "Failed to make symlink path '%s' relative to '%s': %m", link_target, parent);
+
+ r = symlink_atomic(relative, link_path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update current symlink '%s' → '%s': %m", link_path, relative);
+
+ log_info("Updated symlink '%s' → '%s'.", link_path, relative);
+ }
+ }
+
+ return 0;
+}
diff --git a/src/sysupdate/sysupdate-transfer.h b/src/sysupdate/sysupdate-transfer.h
new file mode 100644
index 0000000000..b0c2a6e455
--- /dev/null
+++ b/src/sysupdate/sysupdate-transfer.h
@@ -0,0 +1,62 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+#include <stdbool.h>
+#include <sys/types.h>
+
+#include "sd-id128.h"
+
+/* Forward declare this type so that the headers below can use it */
+typedef struct Transfer Transfer;
+
+#include "sysupdate-partition.h"
+#include "sysupdate-resource.h"
+
+struct Transfer {
+ char *definition_path;
+ char *min_version;
+ char **protected_versions;
+ char *current_symlink;
+ bool verify;
+
+ Resource source, target;
+
+ uint64_t instances_max;
+ bool remove_temporary;
+
+ /* When creating a new partition/file, optionally override these attributes explicitly */
+ sd_id128_t partition_uuid;
+ bool partition_uuid_set;
+ uint64_t partition_flags;
+ bool partition_flags_set;
+ mode_t mode;
+ uint64_t tries_left, tries_done;
+ int no_auto;
+ int read_only;
+ 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 *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;
+};
+
+Transfer *transfer_new(void);
+
+Transfer *transfer_free(Transfer *t);
+DEFINE_TRIVIAL_CLEANUP_FUNC(Transfer*, transfer_free);
+
+int transfer_read_definition(Transfer *t, const char *path);
+
+int transfer_resolve_paths(Transfer *t, const char *root, const char *node);
+
+int transfer_vacuum(Transfer *t, uint64_t space, const char *extra_protected_version);
+
+int transfer_acquire_instance(Transfer *t, Instance *i);
+
+int transfer_install_instance(Transfer *t, Instance *i, const char *root);
diff --git a/src/sysupdate/sysupdate-update-set.c b/src/sysupdate/sysupdate-update-set.c
new file mode 100644
index 0000000000..6d6051d15a
--- /dev/null
+++ b/src/sysupdate/sysupdate-update-set.c
@@ -0,0 +1,63 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "glyph-util.h"
+#include "string-util.h"
+#include "sysupdate-update-set.h"
+#include "terminal-util.h"
+
+UpdateSet *update_set_free(UpdateSet *us) {
+ if (!us)
+ return NULL;
+
+ free(us->version);
+ free(us->instances); /* The objects referenced by this array are freed via resource_free(), not us */
+
+ return mfree(us);
+}
+
+int update_set_cmp(UpdateSet *const*a, UpdateSet *const*b) {
+ assert(a);
+ assert(b);
+ assert(*a);
+ assert(*b);
+ assert((*a)->version);
+ assert((*b)->version);
+
+ /* Newest version at the beginning */
+ return -strverscmp_improved((*a)->version, (*b)->version);
+}
+
+const char *update_set_flags_to_color(UpdateSetFlags flags) {
+
+ if (flags == 0 || (flags & UPDATE_OBSOLETE))
+ return (flags & UPDATE_NEWEST) ? ansi_highlight_grey() : ansi_grey();
+
+ if (FLAGS_SET(flags, UPDATE_INSTALLED|UPDATE_NEWEST))
+ return ansi_highlight();
+
+ if (FLAGS_SET(flags, UPDATE_INSTALLED|UPDATE_PROTECTED))
+ return ansi_highlight_magenta();
+
+ if ((flags & (UPDATE_AVAILABLE|UPDATE_INSTALLED|UPDATE_NEWEST|UPDATE_OBSOLETE)) == (UPDATE_AVAILABLE|UPDATE_NEWEST))
+ return ansi_highlight_green();
+
+ return NULL;
+}
+
+const char *update_set_flags_to_glyph(UpdateSetFlags flags) {
+
+ if (flags == 0 || (flags & UPDATE_OBSOLETE))
+ return special_glyph(SPECIAL_GLYPH_MULTIPLICATION_SIGN);
+
+ if (FLAGS_SET(flags, UPDATE_INSTALLED|UPDATE_NEWEST))
+ return special_glyph(SPECIAL_GLYPH_BLACK_CIRCLE);
+
+ if (FLAGS_SET(flags, UPDATE_INSTALLED|UPDATE_PROTECTED))
+ return special_glyph(SPECIAL_GLYPH_WHITE_CIRCLE);
+
+ if ((flags & (UPDATE_AVAILABLE|UPDATE_INSTALLED|UPDATE_NEWEST|UPDATE_OBSOLETE)) == (UPDATE_AVAILABLE|UPDATE_NEWEST))
+ return special_glyph(SPECIAL_GLYPH_CIRCLE_ARROW);
+
+ return " ";
+}
diff --git a/src/sysupdate/sysupdate-update-set.h b/src/sysupdate/sysupdate-update-set.h
new file mode 100644
index 0000000000..5dd94bce41
--- /dev/null
+++ b/src/sysupdate/sysupdate-update-set.h
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+#include <stdbool.h>
+#include <sys/types.h>
+
+typedef struct UpdateSet UpdateSet;
+
+#include "sysupdate-instance.h"
+
+typedef enum UpdateSetFlags {
+ UPDATE_NEWEST = 1 << 0,
+ UPDATE_AVAILABLE = 1 << 1,
+ UPDATE_INSTALLED = 1 << 2,
+ UPDATE_OBSOLETE = 1 << 3,
+ UPDATE_PROTECTED = 1 << 4,
+} UpdateSetFlags;
+
+struct UpdateSet {
+ UpdateSetFlags flags;
+ char *version;
+ Instance **instances;
+ size_t n_instances;
+};
+
+UpdateSet *update_set_free(UpdateSet *us);
+
+int update_set_cmp(UpdateSet *const*a, UpdateSet *const*b);
+
+const char *update_set_flags_to_color(UpdateSetFlags flags);
+const char *update_set_flags_to_glyph(UpdateSetFlags flags);
diff --git a/src/sysupdate/sysupdate-util.c b/src/sysupdate/sysupdate-util.c
new file mode 100644
index 0000000000..c7a23015ce
--- /dev/null
+++ b/src/sysupdate/sysupdate-util.c
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "path-util.h"
+#include "sysupdate-util.h"
+
+bool version_is_valid(const char *s) {
+ if (isempty(s))
+ return false;
+
+ if (!filename_is_valid(s))
+ return false;
+
+ if (!in_charset(s, ALPHANUMERICAL ".,_-+"))
+ return false;
+
+ return true;
+}
diff --git a/src/sysupdate/sysupdate-util.h b/src/sysupdate/sysupdate-util.h
new file mode 100644
index 0000000000..afa3a9d498
--- /dev/null
+++ b/src/sysupdate/sysupdate-util.h
@@ -0,0 +1,6 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <stdbool.h>
+
+bool version_is_valid(const char *s);
diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c
new file mode 100644
index 0000000000..76c5bae051
--- /dev/null
+++ b/src/sysupdate/sysupdate.c
@@ -0,0 +1,1411 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+#include <unistd.h>
+
+#include "bus-error.h"
+#include "bus-locator.h"
+#include "chase-symlinks.h"
+#include "conf-files.h"
+#include "def.h"
+#include "dirent-util.h"
+#include "dissect-image.h"
+#include "fd-util.h"
+#include "format-table.h"
+#include "glyph-util.h"
+#include "hexdecoct.h"
+#include "login-util.h"
+#include "main-func.h"
+#include "mount-util.h"
+#include "os-util.h"
+#include "pager.h"
+#include "parse-argument.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "pretty-print.h"
+#include "set.h"
+#include "sort-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "sysupdate-transfer.h"
+#include "sysupdate-update-set.h"
+#include "sysupdate.h"
+#include "terminal-util.h"
+#include "utf8.h"
+#include "verbs.h"
+
+static char *arg_definitions = NULL;
+bool arg_sync = true;
+uint64_t arg_instances_max = UINT64_MAX;
+static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF;
+static PagerFlags arg_pager_flags = 0;
+static bool arg_legend = true;
+char *arg_root = NULL;
+static char *arg_image = NULL;
+static bool arg_reboot = false;
+static char *arg_component = NULL;
+static int arg_verify = -1;
+
+STATIC_DESTRUCTOR_REGISTER(arg_definitions, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_component, freep);
+
+typedef struct Context {
+ Transfer **transfers;
+ size_t n_transfers;
+
+ UpdateSet **update_sets;
+ size_t n_update_sets;
+
+ UpdateSet *newest_installed, *candidate;
+
+ Hashmap *web_cache; /* Cache for downloaded resources, keyed by URL */
+} Context;
+
+static Context *context_free(Context *c) {
+ if (!c)
+ return NULL;
+
+ for (size_t i = 0; i < c->n_transfers; i++)
+ transfer_free(c->transfers[i]);
+ free(c->transfers);
+
+ for (size_t i = 0; i < c->n_update_sets; i++)
+ update_set_free(c->update_sets[i]);
+ free(c->update_sets);
+
+ hashmap_free(c->web_cache);
+
+ return mfree(c);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Context*, context_free);
+
+static Context *context_new(void) {
+ /* For now, no fields to initialize non-zero */
+ return new0(Context, 1);
+}
+
+static int context_read_definitions(
+ Context *c,
+ const char *directory,
+ const char *component,
+ const char *root,
+ const char *node) {
+
+ _cleanup_strv_free_ char **files = NULL;
+ int r;
+
+ assert(c);
+
+ if (directory)
+ r = conf_files_list_strv(&files, ".conf", NULL, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char**) STRV_MAKE(directory));
+ else if (component) {
+ _cleanup_strv_free_ char **n = NULL;
+ char **l = CONF_PATHS_STRV("");
+ size_t k = 0;
+
+ n = new0(char*, strv_length(l) + 1);
+ if (!n)
+ return log_oom();
+
+ STRV_FOREACH(i, l) {
+ char *j;
+
+ j = strjoin(*i, "sysupdate.", component, ".d");
+ if (!j)
+ return log_oom();
+
+ n[k++] = j;
+ }
+
+ r = conf_files_list_strv(&files, ".conf", root, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char**) n);
+ } else
+ r = conf_files_list_strv(&files, ".conf", root, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char**) CONF_PATHS_STRV("sysupdate.d"));
+ if (r < 0)
+ return log_error_errno(r, "Failed to enumerate *.conf files: %m");
+
+ STRV_FOREACH(f, files) {
+ _cleanup_(transfer_freep) Transfer *t = NULL;
+
+ if (!GREEDY_REALLOC(c->transfers, c->n_transfers + 1))
+ return log_oom();
+
+ t = transfer_new();
+ if (!t)
+ return log_oom();
+
+ t->definition_path = strdup(*f);
+ if (!t->definition_path)
+ return log_oom();
+
+ r = transfer_read_definition(t, *f);
+ if (r < 0)
+ return r;
+
+ c->transfers[c->n_transfers++] = TAKE_PTR(t);
+ }
+
+ if (c->n_transfers == 0) {
+ if (arg_component)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
+ "No transfer definitions for component '%s' found.", arg_component);
+
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
+ "No transfer definitions found.");
+ }
+
+ for (size_t i = 0; i < c->n_transfers; i++) {
+ r = transfer_resolve_paths(c->transfers[i], root, node);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int context_load_installed_instances(Context *c) {
+ int r;
+
+ assert(c);
+
+ log_info("Discovering installed instances…");
+
+ for (size_t i = 0; i < c->n_transfers; i++) {
+ r = resource_load_instances(
+ &c->transfers[i]->target,
+ arg_verify >= 0 ? arg_verify : c->transfers[i]->verify,
+ &c->web_cache);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int context_load_available_instances(Context *c) {
+ int r;
+
+ assert(c);
+
+ log_info("Discovering available instances…");
+
+ for (size_t i = 0; i < c->n_transfers; i++) {
+ assert(c->transfers[i]);
+
+ r = resource_load_instances(
+ &c->transfers[i]->source,
+ arg_verify >= 0 ? arg_verify : c->transfers[i]->verify,
+ &c->web_cache);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags) {
+ _cleanup_free_ Instance **cursor_instances = NULL;
+ _cleanup_free_ char *boundary = NULL;
+ bool newest_found = false;
+ int r;
+
+ assert(c);
+ assert(IN_SET(flags, UPDATE_AVAILABLE, UPDATE_INSTALLED));
+
+ for (;;) {
+ bool incomplete = false, exists = false;
+ UpdateSetFlags extra_flags = 0;
+ _cleanup_free_ char *cursor = NULL;
+ UpdateSet *us = NULL;
+
+ for (size_t k = 0; k < c->n_transfers; k++) {
+ Transfer *t = c->transfers[k];
+ bool cursor_found = false;
+ Resource *rr;
+
+ assert(t);
+
+ if (flags == UPDATE_AVAILABLE)
+ rr = &t->source;
+ else {
+ assert(flags == UPDATE_INSTALLED);
+ rr = &t->target;
+ }
+
+ for (size_t j = 0; j < rr->n_instances; j++) {
+ Instance *i = rr->instances[j];
+
+ assert(i);
+
+ /* Is the instance we are looking at equal or newer than the boundary? If so, we
+ * already checked this version, and it wasn't complete, let's ignore it. */
+ if (boundary && strverscmp_improved(i->metadata.version, boundary) >= 0)
+ continue;
+
+ if (cursor) {
+ if (strverscmp_improved(i->metadata.version, cursor) != 0)
+ continue;
+ } else {
+ cursor = strdup(i->metadata.version);
+ if (!cursor)
+ return log_oom();
+ }
+
+ cursor_found = true;
+
+ if (!cursor_instances) {
+ cursor_instances = new(Instance*, c->n_transfers);
+ if (!cursor_instances)
+ return -ENOMEM;
+ }
+ cursor_instances[k] = i;
+ break;
+ }
+
+ if (!cursor) /* No suitable instance beyond the boundary found? Then we are done! */
+ break;
+
+ if (!cursor_found) {
+ /* Hmm, we didn't find the version indicated by 'cursor' among the instances
+ * of this transfer, let's skip it. */
+ incomplete = true;
+ break;
+ }
+
+ if (t->min_version && strverscmp_improved(t->min_version, cursor) > 0)
+ extra_flags |= UPDATE_OBSOLETE;
+
+ if (strv_contains(t->protected_versions, cursor))
+ extra_flags |= UPDATE_PROTECTED;
+ }
+
+ if (!cursor) /* EOL */
+ break;
+
+ r = free_and_strdup_warn(&boundary, cursor);
+ if (r < 0)
+ return r;
+
+ if (incomplete) /* One transfer was missing this version, ignore the whole thing */
+ continue;
+
+ /* See if we already have this update set in our table */
+ for (size_t i = 0; i < c->n_update_sets; i++) {
+ if (strverscmp_improved(c->update_sets[i]->version, cursor) != 0)
+ continue;
+
+ /* We only store the instances we found first, but we remember we also found it again */
+ c->update_sets[i]->flags |= flags | extra_flags;
+ exists = true;
+ newest_found = true;
+ break;
+ }
+
+ if (exists)
+ continue;
+
+ /* Doesn't exist yet, let's add it */
+ if (!GREEDY_REALLOC(c->update_sets, c->n_update_sets + 1))
+ return log_oom();
+
+ us = new(UpdateSet, 1);
+ if (!us)
+ return log_oom();
+
+ *us = (UpdateSet) {
+ .flags = flags | (newest_found ? 0 : UPDATE_NEWEST) | extra_flags,
+ .version = TAKE_PTR(cursor),
+ .instances = TAKE_PTR(cursor_instances),
+ .n_instances = c->n_transfers,
+ };
+
+ c->update_sets[c->n_update_sets++] = us;
+
+ newest_found = true;
+
+ /* Remember which one is the newest installed */
+ if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED)) == (UPDATE_NEWEST|UPDATE_INSTALLED))
+ c->newest_installed = us;
+
+ /* Remember which is the newest non-obsolete, available (and not installed) version, which we declare the "candidate" */
+ if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE)) == (UPDATE_NEWEST|UPDATE_AVAILABLE))
+ c->candidate = us;
+ }
+
+ /* Newest installed is newer than or equal to candidate? Then suppress the candidate */
+ if (c->newest_installed && c->candidate && strverscmp_improved(c->newest_installed->version, c->candidate->version) >= 0)
+ c->candidate = NULL;
+
+ return 0;
+}
+
+static int context_discover_update_sets(Context *c) {
+ int r;
+
+ assert(c);
+
+ log_info("Determining installed update sets…");
+
+ r = context_discover_update_sets_by_flag(c, UPDATE_INSTALLED);
+ if (r < 0)
+ return r;
+
+ log_info("Determining available update sets…");
+
+ r = context_discover_update_sets_by_flag(c, UPDATE_AVAILABLE);
+ if (r < 0)
+ return r;
+
+ typesafe_qsort(c->update_sets, c->n_update_sets, update_set_cmp);
+ return 0;
+}
+
+static const char *update_set_flags_to_string(UpdateSetFlags flags) {
+
+ switch ((unsigned) flags) {
+
+ case 0:
+ return "n/a";
+
+ case UPDATE_INSTALLED|UPDATE_NEWEST:
+ case UPDATE_INSTALLED|UPDATE_NEWEST|UPDATE_PROTECTED:
+ case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST:
+ case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST|UPDATE_PROTECTED:
+ return "current";
+
+ case UPDATE_AVAILABLE|UPDATE_NEWEST:
+ case UPDATE_AVAILABLE|UPDATE_NEWEST|UPDATE_PROTECTED:
+ return "candidate";
+
+ case UPDATE_INSTALLED:
+ case UPDATE_INSTALLED|UPDATE_AVAILABLE:
+ return "installed";
+
+ case UPDATE_INSTALLED|UPDATE_PROTECTED:
+ case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_PROTECTED:
+ return "protected";
+
+ case UPDATE_AVAILABLE:
+ case UPDATE_AVAILABLE|UPDATE_PROTECTED:
+ return "available";
+
+ case UPDATE_INSTALLED|UPDATE_OBSOLETE|UPDATE_NEWEST:
+ case UPDATE_INSTALLED|UPDATE_OBSOLETE|UPDATE_NEWEST|UPDATE_PROTECTED:
+ case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST:
+ case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST|UPDATE_PROTECTED:
+ return "current+obsolete";
+
+ case UPDATE_INSTALLED|UPDATE_OBSOLETE:
+ case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE:
+ return "installed+obsolete";
+
+ case UPDATE_INSTALLED|UPDATE_OBSOLETE|UPDATE_PROTECTED:
+ case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_PROTECTED:
+ return "protected+obsolete";
+
+ case UPDATE_AVAILABLE|UPDATE_OBSOLETE:
+ case UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_PROTECTED:
+ case UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST:
+ case UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST|UPDATE_PROTECTED:
+ return "available+obsolete";
+
+ default:
+ assert_not_reached();
+ }
+}
+
+
+static int context_show_table(Context *c) {
+ _cleanup_(table_unrefp) Table *t = NULL;
+ int r;
+
+ assert(c);
+
+ t = table_new("", "version", "installed", "available", "assessment");
+ if (!t)
+ return log_oom();
+
+ (void) table_set_align_percent(t, table_get_cell(t, 0, 0), 100);
+ (void) table_set_align_percent(t, table_get_cell(t, 0, 2), 50);
+ (void) table_set_align_percent(t, table_get_cell(t, 0, 3), 50);
+
+ for (size_t i = 0; i < c->n_update_sets; i++) {
+ UpdateSet *us = c->update_sets[i];
+ const char *color;
+
+ color = update_set_flags_to_color(us->flags);
+
+ r = table_add_many(t,
+ TABLE_STRING, update_set_flags_to_glyph(us->flags),
+ TABLE_SET_COLOR, color,
+ TABLE_STRING, us->version,
+ TABLE_SET_COLOR, color,
+ TABLE_STRING, special_glyph_check_mark_space(FLAGS_SET(us->flags, UPDATE_INSTALLED)),
+ TABLE_SET_COLOR, color,
+ TABLE_STRING, special_glyph_check_mark_space(FLAGS_SET(us->flags, UPDATE_AVAILABLE)),
+ TABLE_SET_COLOR, color,
+ TABLE_STRING, update_set_flags_to_string(us->flags),
+ TABLE_SET_COLOR, color);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
+}
+
+static UpdateSet *context_update_set_by_version(Context *c, const char *version) {
+ assert(c);
+ assert(version);
+
+ for (size_t i = 0; i < c->n_update_sets; i++)
+ if (streq(c->update_sets[i]->version, version))
+ return c->update_sets[i];
+
+ return NULL;
+}
+
+static int context_show_version(Context *c, const char *version) {
+ bool show_fs_columns = false, show_partition_columns = false,
+ have_fs_attributes = false, have_partition_attributes = false,
+ have_size = false, have_tries = false, have_no_auto = false,
+ have_read_only = false, have_growfs = false, have_sha256 = false;
+ _cleanup_(table_unrefp) Table *t = NULL;
+ UpdateSet *us;
+ int r;
+
+ assert(c);
+ assert(version);
+
+ us = context_update_set_by_version(c, version);
+ if (!us)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
+
+ if (arg_json_format_flags & (JSON_FORMAT_OFF|JSON_FORMAT_PRETTY|JSON_FORMAT_PRETTY_AUTO))
+ (void) pager_open(arg_pager_flags);
+
+ if (FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF))
+ printf("%s%s%s Version: %s\n"
+ " State: %s%s%s\n"
+ "Installed: %s%s\n"
+ "Available: %s%s\n"
+ "Protected: %s%s%s\n"
+ " Obsolete: %s%s%s\n\n",
+ strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_glyph(us->flags), ansi_normal(), us->version,
+ strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_string(us->flags), ansi_normal(),
+ yes_no(us->flags & UPDATE_INSTALLED), FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_NEWEST) ? " (newest)" : "",
+ yes_no(us->flags & UPDATE_AVAILABLE), (us->flags & (UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST)) == (UPDATE_AVAILABLE|UPDATE_NEWEST) ? " (newest)" : "",
+ FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED) ? ansi_highlight() : "", yes_no(FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED)), ansi_normal(),
+ us->flags & UPDATE_OBSOLETE ? ansi_highlight_red() : "", yes_no(us->flags & UPDATE_OBSOLETE), ansi_normal());
+
+
+ t = table_new("type", "path", "ptuuid", "ptflags", "mtime", "mode", "size", "tries-done", "tries-left", "noauto", "ro", "growfs", "sha256");
+ if (!t)
+ return log_oom();
+
+ (void) table_set_align_percent(t, table_get_cell(t, 0, 3), 100);
+ (void) table_set_align_percent(t, table_get_cell(t, 0, 4), 100);
+ (void) table_set_align_percent(t, table_get_cell(t, 0, 5), 100);
+ (void) table_set_align_percent(t, table_get_cell(t, 0, 6), 100);
+ (void) table_set_align_percent(t, table_get_cell(t, 0, 7), 100);
+ (void) table_set_align_percent(t, table_get_cell(t, 0, 8), 100);
+ (void) table_set_empty_string(t, "-");
+
+ /* Determine if the target will make use of partition/fs attributes for any of the transfers */
+ for (size_t n = 0; n < c->n_transfers; n++) {
+ Transfer *tr = c->transfers[n];
+
+ if (tr->target.type == RESOURCE_PARTITION)
+ show_partition_columns = true;
+ if (RESOURCE_IS_FILESYSTEM(tr->target.type))
+ show_fs_columns = true;
+ }
+
+ for (size_t n = 0; n < us->n_instances; n++) {
+ Instance *i = us->instances[n];
+
+ r = table_add_many(t,
+ TABLE_STRING, resource_type_to_string(i->resource->type),
+ TABLE_PATH, i->path);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (i->metadata.partition_uuid_set) {
+ have_partition_attributes = true;
+ r = table_add_cell(t, NULL, TABLE_UUID, &i->metadata.partition_uuid);
+ } else
+ r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (i->metadata.partition_flags_set) {
+ have_partition_attributes = true;
+ r = table_add_cell(t, NULL, TABLE_UINT64_HEX, &i->metadata.partition_flags);
+ } else
+ r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (i->metadata.mtime != USEC_INFINITY) {
+ have_fs_attributes = true;
+ r = table_add_cell(t, NULL, TABLE_TIMESTAMP, &i->metadata.mtime);
+ } else
+ r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (i->metadata.mode != MODE_INVALID) {
+ have_fs_attributes = true;
+ r = table_add_cell(t, NULL, TABLE_MODE, &i->metadata.mode);
+ } else
+ r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (i->metadata.size != UINT64_MAX) {
+ have_size = true;
+ r = table_add_cell(t, NULL, TABLE_SIZE, &i->metadata.size);
+ } else
+ r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (i->metadata.tries_done != UINT64_MAX) {
+ have_tries = true;
+ r = table_add_cell(t, NULL, TABLE_UINT64, &i->metadata.tries_done);
+ } else
+ r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (i->metadata.tries_left != UINT64_MAX) {
+ have_tries = true;
+ r = table_add_cell(t, NULL, TABLE_UINT64, &i->metadata.tries_left);
+ } else
+ r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (i->metadata.no_auto >= 0) {
+ bool b;
+
+ have_no_auto = true;
+ b = i->metadata.no_auto;
+ r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
+ } else
+ r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+ if (i->metadata.read_only >= 0) {
+ bool b;
+
+ have_read_only = true;
+ b = i->metadata.read_only;
+ r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
+ } else
+ r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (i->metadata.growfs >= 0) {
+ bool b;
+
+ have_growfs = true;
+ b = i->metadata.growfs;
+ r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
+ } else
+ r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (i->metadata.sha256sum_set) {
+ _cleanup_free_ char *formatted = NULL;
+
+ have_sha256 = true;
+
+ formatted = hexmem(i->metadata.sha256sum, sizeof(i->metadata.sha256sum));
+ if (!formatted)
+ return log_oom();
+
+ r = table_add_cell(t, NULL, TABLE_STRING, formatted);
+ } else
+ r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ /* Hide the fs/partition columns if we don't have any data to show there */
+ if (!have_fs_attributes)
+ show_fs_columns = false;
+ if (!have_partition_attributes)
+ show_partition_columns = false;
+
+ if (!show_partition_columns)
+ (void) table_hide_column_from_display(t, 2, 3);
+ if (!show_fs_columns)
+ (void) table_hide_column_from_display(t, 4, 5);
+ if (!have_size)
+ (void) table_hide_column_from_display(t, 6);
+ if (!have_tries)
+ (void) table_hide_column_from_display(t, 7, 8);
+ if (!have_no_auto)
+ (void) table_hide_column_from_display(t, 9);
+ if (!have_read_only)
+ (void) table_hide_column_from_display(t, 10);
+ if (!have_growfs)
+ (void) table_hide_column_from_display(t, 11);
+ if (!have_sha256)
+ (void) table_hide_column_from_display(t, 12);
+
+ return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
+}
+
+static int context_vacuum(
+ Context *c,
+ uint64_t space,
+ const char *extra_protected_version) {
+
+ int r, count = 0;
+
+ assert(c);
+
+ if (space == 0)
+ log_info("Making room…");
+ else
+ log_info("Making room for %" PRIu64 " updates…", space);
+
+ for (size_t i = 0; i < c->n_transfers; i++) {
+ r = transfer_vacuum(c->transfers[i], space, extra_protected_version);
+ if (r < 0)
+ return r;
+
+ count = MAX(count, r);
+ }
+
+ if (count > 0)
+ log_info("Removed %i instances.", count);
+ else
+ log_info("Removed no instances.");
+
+ return 0;
+}
+
+static int context_make_offline(Context **ret, const char *node) {
+ _cleanup_(context_freep) Context* context = NULL;
+ int r;
+
+ assert(ret);
+
+ /* Allocates a context object and initializes everything we can initialize offline, i.e. without
+ * checking on the update source (i.e. the Internet) what versions are available */
+
+ context = context_new();
+ if (!context)
+ return log_oom();
+
+ r = context_read_definitions(context, arg_definitions, arg_component, arg_root, node);
+ if (r < 0)
+ return r;
+
+ r = context_load_installed_instances(context);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(context);
+ return 0;
+}
+
+static int context_make_online(Context **ret, const char *node) {
+ _cleanup_(context_freep) Context* context = NULL;
+ int r;
+
+ assert(ret);
+
+ /* Like context_make_offline(), but also communicates with the update source looking for new
+ * versions. */
+
+ r = context_make_offline(&context, node);
+ if (r < 0)
+ return r;
+
+ r = context_load_available_instances(context);
+ if (r < 0)
+ return r;
+
+ r = context_discover_update_sets(context);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(context);
+ return 0;
+}
+
+static int context_apply(
+ Context *c,
+ const char *version,
+ UpdateSet **ret_applied) {
+
+ UpdateSet *us = NULL;
+ int r;
+
+ assert(c);
+
+ if (version) {
+ us = context_update_set_by_version(c, version);
+ if (!us)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
+ } else {
+ if (!c->candidate) {
+ log_info("No update needed.");
+
+ if (ret_applied)
+ *ret_applied = NULL;
+
+ return 0;
+ }
+
+ us = c->candidate;
+ }
+
+ if (FLAGS_SET(us->flags, UPDATE_INSTALLED)) {
+ log_info("Selected update '%s' is already installed. Skipping update.", us->version);
+
+ if (ret_applied)
+ *ret_applied = NULL;
+
+ return 0;
+ }
+ if (!FLAGS_SET(us->flags, UPDATE_AVAILABLE))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is not available, refusing.", us->version);
+ if (FLAGS_SET(us->flags, UPDATE_OBSOLETE))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is obsolete, refusing.", us->version);
+
+ assert((us->flags & (UPDATE_AVAILABLE|UPDATE_INSTALLED|UPDATE_OBSOLETE)) == UPDATE_AVAILABLE);
+
+ if (!FLAGS_SET(us->flags, UPDATE_NEWEST))
+ log_notice("Selected update '%s' is not the newest, proceeding anyway.", us->version);
+ if (c->newest_installed && strverscmp_improved(c->newest_installed->version, us->version) > 0)
+ log_notice("Selected update '%s' is older than newest installed version, proceeding anyway.", us->version);
+
+ log_info("Selected update '%s' for install.", us->version);
+
+ (void) sd_notifyf(false,
+ "STATUS=Making room for '%s'.", us->version);
+
+ /* Let's make some room. We make sure for each transfer we have one free space to fill. While
+ * removing stuff we'll protect the version we are trying to acquire. Why that? Maybe an earlier
+ * download succeeded already, in which case we shouldn't remove it just to acquire it again */
+ r = context_vacuum(
+ c,
+ /* space = */ 1,
+ /* extra_protected_version = */ us->version);
+ if (r < 0)
+ return r;
+
+ if (arg_sync)
+ sync();
+
+ (void) sd_notifyf(false,
+ "STATUS=Updating to '%s'.\n", us->version);
+
+ /* There should now be one instance picked for each transfer, and the order is the same */
+ assert(us->n_instances == c->n_transfers);
+
+ for (size_t i = 0; i < c->n_transfers; i++) {
+ r = transfer_acquire_instance(c->transfers[i], us->instances[i]);
+ if (r < 0)
+ return r;
+ }
+
+ if (arg_sync)
+ sync();
+
+ for (size_t i = 0; i < c->n_transfers; i++) {
+ r = transfer_install_instance(c->transfers[i], us->instances[i], arg_root);
+ if (r < 0)
+ return r;
+ }
+
+ log_info("%s Successfully installed update '%s'.", special_glyph(SPECIAL_GLYPH_SPARKLES), us->version);
+
+ if (ret_applied)
+ *ret_applied = us;
+
+ return 1;
+}
+
+static int reboot_now(void) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_close_unrefp) sd_bus *bus = NULL;
+ int r;
+
+ r = sd_bus_open_system(&bus);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open bus connection: %m");
+
+ r = bus_call_method(bus, bus_login_mgr, "RebootWithFlags", &error, NULL, "t",
+ (uint64_t) SD_LOGIND_ROOT_CHECK_INHIBITORS);
+ if (r < 0)
+ return log_error_errno(r, "Failed to issue reboot request: %s", bus_error_message(&error, r));
+
+ return 0;
+}
+
+static int process_image(
+ bool ro,
+ char **ret_mounted_dir,
+ LoopDevice **ret_loop_device,
+ DecryptedImage **ret_decrypted_image) {
+
+ _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
+ _cleanup_(decrypted_image_unrefp) DecryptedImage *decrypted_image = NULL;
+ _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
+ int r;
+
+ assert(ret_mounted_dir);
+ assert(ret_loop_device);
+ assert(ret_decrypted_image);
+
+ if (!arg_image)
+ return 0;
+
+ assert(!arg_root);
+
+ r = mount_image_privately_interactively(
+ arg_image,
+ (ro ? DISSECT_IMAGE_READ_ONLY : 0) |
+ DISSECT_IMAGE_FSCK |
+ DISSECT_IMAGE_MKDIR |
+ DISSECT_IMAGE_GROWFS |
+ DISSECT_IMAGE_RELAX_VAR_CHECK |
+ DISSECT_IMAGE_USR_NO_ROOT |
+ DISSECT_IMAGE_GENERIC_ROOT |
+ DISSECT_IMAGE_REQUIRE_ROOT,
+ &mounted_dir,
+ &loop_device,
+ &decrypted_image);
+ if (r < 0)
+ return r;
+
+ arg_root = strdup(mounted_dir);
+ if (!arg_root)
+ return log_oom();
+
+ *ret_mounted_dir = TAKE_PTR(mounted_dir);
+ *ret_loop_device = TAKE_PTR(loop_device);
+ *ret_decrypted_image = TAKE_PTR(decrypted_image);
+
+ return 0;
+}
+
+static int verb_list(int argc, char **argv, void *userdata) {
+ _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
+ _cleanup_(decrypted_image_unrefp) DecryptedImage *decrypted_image = NULL;
+ _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
+ _cleanup_(context_freep) Context* context = NULL;
+ const char *version;
+ int r;
+
+ assert(argc <= 2);
+ version = argc >= 2 ? argv[1] : NULL;
+
+ r = process_image(/* ro= */ true, &mounted_dir, &loop_device, &decrypted_image);
+ if (r < 0)
+ return r;
+
+ r = context_make_online(&context, loop_device ? loop_device->node : NULL);
+ if (r < 0)
+ return r;
+
+ if (version)
+ return context_show_version(context, version);
+ else
+ return context_show_table(context);
+}
+
+static int verb_check_new(int argc, char **argv, void *userdata) {
+ _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
+ _cleanup_(decrypted_image_unrefp) DecryptedImage *decrypted_image = NULL;
+ _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
+ _cleanup_(context_freep) Context* context = NULL;
+ int r;
+
+ assert(argc <= 1);
+
+ r = process_image(/* ro= */ true, &mounted_dir, &loop_device, &decrypted_image);
+ if (r < 0)
+ return r;
+
+ r = context_make_online(&context, loop_device ? loop_device->node : NULL);
+ if (r < 0)
+ return r;
+
+ if (!context->candidate) {
+ log_debug("No candidate found.");
+ return EXIT_FAILURE;
+ }
+
+ puts(context->candidate->version);
+ return EXIT_SUCCESS;
+}
+
+static int verb_vacuum(int argc, char **argv, void *userdata) {
+ _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
+ _cleanup_(decrypted_image_unrefp) DecryptedImage *decrypted_image = NULL;
+ _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
+ _cleanup_(context_freep) Context* context = NULL;
+ int r;
+
+ assert(argc <= 1);
+
+ r = process_image(/* ro= */ false, &mounted_dir, &loop_device, &decrypted_image);
+ if (r < 0)
+ return r;
+
+ r = context_make_offline(&context, loop_device ? loop_device->node : NULL);
+ if (r < 0)
+ return r;
+
+ return context_vacuum(context, 0, NULL);
+}
+
+static int verb_update(int argc, char **argv, void *userdata) {
+ _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
+ _cleanup_(decrypted_image_unrefp) DecryptedImage *decrypted_image = NULL;
+ _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
+ _cleanup_(context_freep) Context* context = NULL;
+ _cleanup_free_ char *booted_version = NULL;
+ UpdateSet *applied = NULL;
+ const char *version;
+ int r;
+
+ assert(argc <= 2);
+ version = argc >= 2 ? argv[1] : NULL;
+
+ if (arg_reboot) {
+ /* If automatic reboot on completion is requested, let's first determine the currently booted image */
+
+ r = parse_os_release(arg_root, "IMAGE_VERSION", &booted_version);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse /etc/os-release: %m");
+ if (!booted_version)
+ return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "/etc/os-release lacks IMAGE_VERSION field.");
+ }
+
+ r = process_image(/* ro= */ false, &mounted_dir, &loop_device, &decrypted_image);
+ if (r < 0)
+ return r;
+
+ r = context_make_online(&context, loop_device ? loop_device->node : NULL);
+ if (r < 0)
+ return r;
+
+ r = context_apply(context, version, &applied);
+ if (r < 0)
+ return r;
+
+ if (r > 0 && arg_reboot) {
+ assert(applied);
+ assert(booted_version);
+
+ if (strverscmp_improved(applied->version, booted_version) > 0) {
+ log_notice("Newly installed version is newer than booted version, rebooting.");
+ return reboot_now();
+ }
+
+ log_info("Booted version is newer or identical to newly installed version, not rebooting.");
+ }
+
+ return 0;
+}
+
+static int verb_pending_or_reboot(int argc, char **argv, void *userdata) {
+ _cleanup_(context_freep) Context* context = NULL;
+ _cleanup_free_ char *booted_version = NULL;
+ int r;
+
+ assert(argc == 1);
+
+ if (arg_image || arg_root)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "The --root=/--image switches may not be combined with the '%s' operation.", argv[0]);
+
+ r = context_make_offline(&context, NULL);
+ if (r < 0)
+ return r;
+
+ log_info("Determining installed update sets…");
+
+ r = context_discover_update_sets_by_flag(context, UPDATE_INSTALLED);
+ if (r < 0)
+ return r;
+ if (!context->newest_installed)
+ return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "Couldn't find any suitable installed versions.");
+
+ r = parse_os_release(arg_root, "IMAGE_VERSION", &booted_version);
+ if (r < 0) /* yes, arg_root is NULL here, but we have to pass something, and it's a lot more readable
+ * if we see what the first argument is about */
+ return log_error_errno(r, "Failed to parse /etc/os-release: %m");
+ if (!booted_version)
+ return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "/etc/os-release lacks IMAGE_VERSION= field.");
+
+ r = strverscmp_improved(context->newest_installed->version, booted_version);
+ if (r > 0) {
+ log_notice("Newest installed version '%s' is newer than booted version '%s'.%s",
+ context->newest_installed->version, booted_version,
+ streq(argv[0], "pending") ? " Reboot recommended." : "");
+
+ if (streq(argv[0], "reboot"))
+ return reboot_now();
+
+ return EXIT_SUCCESS;
+ } else if (r == 0)
+ log_info("Newest installed version '%s' matches booted version '%s'.",
+ context->newest_installed->version, booted_version);
+ else
+ log_warning("Newest installed version '%s' is older than booted version '%s'.",
+ context->newest_installed->version, booted_version);
+
+ if (streq(argv[0], "pending")) /* When called as 'pending' tell the caller via failure exit code that there's nothing newer installed */
+ return EXIT_FAILURE;
+
+ return EXIT_SUCCESS;
+}
+
+static int component_name_valid(const char *c) {
+ _cleanup_free_ char *j = NULL;
+
+ /* See if the specified string enclosed in the directory prefix+suffix would be a valid file name */
+
+ if (isempty(c))
+ return false;
+
+ if (string_has_cc(c, NULL))
+ return false;
+
+ if (!utf8_is_valid(c))
+ return false;
+
+ j = strjoin("sysupdate.", c, ".d");
+ if (!j)
+ return -ENOMEM;
+
+ return filename_is_valid(j);
+}
+
+static int verb_components(int argc, char **argv, void *userdata) {
+ _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
+ _cleanup_(decrypted_image_unrefp) DecryptedImage *decrypted_image = NULL;
+ _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
+ _cleanup_(set_freep) Set *names = NULL;
+ _cleanup_free_ char **z = NULL; /* We use simple free() rather than strv_free() here, since set_free() will free the strings for us */
+ char **l = CONF_PATHS_STRV("");
+ bool has_default_component = false;
+ int r;
+
+ assert(argc <= 1);
+
+ r = process_image(/* ro= */ false, &mounted_dir, &loop_device, &decrypted_image);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(i, l) {
+ _cleanup_closedir_ DIR *d = NULL;
+ _cleanup_free_ char *p = NULL;
+
+ r = chase_symlinks_and_opendir(*i, arg_root, CHASE_PREFIX_ROOT, &p, &d);
+ if (r == -ENOENT)
+ continue;
+ if (r < 0)
+ return log_error_errno(r, "Failed to open directory '%s': %m", *i);
+
+ for (;;) {
+ _cleanup_free_ char *n = NULL;
+ struct dirent *de;
+ const char *e, *a;
+
+ de = readdir_ensure_type(d);
+ if (!de) {
+ if (errno != 0)
+ return log_error_errno(errno, "Failed to enumerate directory '%s': %m", p);
+
+ break;
+ }
+
+ if (de->d_type != DT_DIR)
+ continue;
+
+ if (dot_or_dot_dot(de->d_name))
+ continue;
+
+ if (streq(de->d_name, "sysupdate.d")) {
+ has_default_component = true;
+ continue;
+ }
+
+ e = startswith(de->d_name, "sysupdate.");
+ if (!e)
+ continue;
+
+ a = endswith(e, ".d");
+ if (!a)
+ continue;
+
+ n = strndup(e, a - e);
+ if (!n)
+ return log_oom();
+
+ r = component_name_valid(n);
+ if (r < 0)
+ return log_error_errno(r, "Unable to validate component name: %m");
+ if (r == 0)
+ continue;
+
+ r = set_ensure_consume(&names, &string_hash_ops_free, TAKE_PTR(n));
+ if (r < 0 && r != -EEXIST)
+ return log_error_errno(r, "Failed to add component to set: %m");
+ }
+ }
+
+ if (!has_default_component && set_isempty(names)) {
+ log_info("No components defined.");
+ return 0;
+ }
+
+ z = set_get_strv(names);
+ if (!z)
+ return log_oom();
+
+ strv_sort(z);
+
+ if (has_default_component)
+ printf("%s<default>%s\n",
+ ansi_highlight(), ansi_normal());
+
+ STRV_FOREACH(i, z)
+ puts(*i);
+
+ return 0;
+}
+
+static int verb_help(int argc, char **argv, void *userdata) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ r = terminal_urlify_man("systemd-sysupdate", "1", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%1$s [OPTIONS...] [VERSION]\n"
+ "\n%5$sUpdate OS images.%6$s\n"
+ "\n%3$sCommands:%4$s\n"
+ " list [VERSION] Show installed and available versions\n"
+ " check-new Check if there's a new version available\n"
+ " update [VERSION] Install new version now\n"
+ " vacuum Make room, by deleting old versions\n"
+ " pending Report whether a newer version is installed than\n"
+ " currently booted\n"
+ " reboot Reboot if a newer version is installed than booted\n"
+ " components Show list of components\n"
+ " -h --help Show this help\n"
+ " --version Show package version\n"
+ "\n%3$sOptions:%4$s\n"
+ " -C --component=NAME Select component to update\n"
+ " --definitions=DIR Find transfer definitions in specified directory\n"
+ " --root=PATH Operate relative to root path\n"
+ " --image=PATH Operate relative to image file\n"
+ " -m --instances-max=INT How many instances to maintain\n"
+ " --sync=BOOL Controls whether to sync data to disk\n"
+ " --verify=BOOL Force signature verification on or off\n"
+ " --reboot Reboot after updating to newer version\n"
+ " --no-pager Do not pipe output into a pager\n"
+ " --no-legend Do not show the headers and footers\n"
+ " --json=pretty|short|off\n"
+ " Generate JSON output\n"
+ "\nSee the %2$s for details.\n"
+ , program_invocation_short_name
+ , link
+ , ansi_underline(), ansi_normal()
+ , ansi_highlight(), ansi_normal()
+ );
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_NO_PAGER,
+ ARG_NO_LEGEND,
+ ARG_SYNC,
+ ARG_DEFINITIONS,
+ ARG_JSON,
+ ARG_ROOT,
+ ARG_IMAGE,
+ ARG_REBOOT,
+ ARG_VERIFY,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "no-pager", no_argument, NULL, ARG_NO_PAGER },
+ { "no-legend", no_argument, NULL, ARG_NO_LEGEND },
+ { "definitions", required_argument, NULL, ARG_DEFINITIONS },
+ { "instances-max", required_argument, NULL, 'm' },
+ { "sync", required_argument, NULL, ARG_SYNC },
+ { "json", required_argument, NULL, ARG_JSON },
+ { "root", required_argument, NULL, ARG_ROOT },
+ { "image", required_argument, NULL, ARG_IMAGE },
+ { "reboot", no_argument, NULL, ARG_REBOOT },
+ { "component", required_argument, NULL, 'C' },
+ { "verify", required_argument, NULL, ARG_VERIFY },
+ {}
+ };
+
+ int c, r;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "hm:C:", options, NULL)) >= 0) {
+
+ switch (c) {
+
+ case 'h':
+ return verb_help(0, NULL, NULL);
+
+ case ARG_VERSION:
+ return version();
+
+ case ARG_NO_PAGER:
+ arg_pager_flags |= PAGER_DISABLE;
+ break;
+
+ case ARG_NO_LEGEND:
+ arg_legend = false;
+ break;
+
+ case 'm':
+ r = safe_atou64(optarg, &arg_instances_max);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --instances-max= parameter: %s", optarg);
+
+ break;
+
+ case ARG_SYNC:
+ r = parse_boolean_argument("--sync=", optarg, &arg_sync);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_DEFINITIONS:
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_definitions);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_JSON:
+ r = parse_json_argument(optarg, &arg_json_format_flags);
+ if (r <= 0)
+ return r;
+
+ break;
+
+ case ARG_ROOT:
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_root);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_IMAGE:
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_REBOOT:
+ arg_reboot = true;
+ break;
+
+ case 'C':
+ if (isempty(optarg)) {
+ arg_component = mfree(arg_component);
+ break;
+ }
+
+ r = component_name_valid(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine if component name is valid: %m");
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Component name invalid: %s", optarg);
+
+ r = free_and_strdup_warn(&arg_component, optarg);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case ARG_VERIFY: {
+ bool b;
+
+ r = parse_boolean_argument("--verify=", optarg, &b);
+ if (r < 0)
+ return r;
+
+ arg_verify = b;
+ break;
+ }
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+ }
+
+ if (arg_image && arg_root)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Please specify either --root= or --image=, the combination of both is not supported.");
+
+ if ((arg_image || arg_root) && arg_reboot)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --reboot switch may not be combined with --root= or --image=.");
+
+ if (arg_definitions && arg_component)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= and --component= switches may not be combined.");
+
+ return 1;
+}
+
+static int sysupdate_main(int argc, char *argv[]) {
+
+ static const Verb verbs[] = {
+ { "list", VERB_ANY, 2, VERB_DEFAULT, verb_list },
+ { "components", VERB_ANY, 1, 0, verb_components },
+ { "check-new", VERB_ANY, 1, 0, verb_check_new },
+ { "update", VERB_ANY, 2, 0, verb_update },
+ { "vacuum", VERB_ANY, 1, 0, verb_vacuum },
+ { "reboot", 1, 1, 0, verb_pending_or_reboot },
+ { "pending", 1, 1, 0, verb_pending_or_reboot },
+ { "help", VERB_ANY, 1, 0, verb_help },
+ {}
+ };
+
+ return dispatch_verb(argc, argv, verbs, NULL);
+}
+
+static int run(int argc, char *argv[]) {
+ int r;
+
+ log_setup();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ return sysupdate_main(argc, argv);
+}
+
+DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);
diff --git a/src/sysupdate/sysupdate.h b/src/sysupdate/sysupdate.h
new file mode 100644
index 0000000000..6d387b7a5d
--- /dev/null
+++ b/src/sysupdate/sysupdate.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+#include <stdbool.h>
+
+extern bool arg_sync;
+extern uint64_t arg_instances_max;
+extern char *arg_root;
+
+static inline const char* import_binary_path(void) {
+ return secure_getenv("SYSTEMD_IMPORT_PATH") ?: SYSTEMD_IMPORT_PATH;
+}
+
+static inline const char* import_fs_binary_path(void) {
+ return secure_getenv("SYSTEMD_IMPORT_FS_PATH") ?: SYSTEMD_IMPORT_FS_PATH;
+}
+
+static inline const char *pull_binary_path(void) {
+ return secure_getenv("SYSTEMD_PULL_PATH") ?: SYSTEMD_PULL_PATH;
+}
diff --git a/src/test/test-strv.c b/src/test/test-strv.c
index ae3e2798c2..edb782eb0d 100644
--- a/src/test/test-strv.c
+++ b/src/test/test-strv.c
@@ -639,8 +639,13 @@ TEST(strv_foreach_backwards) {
STRV_FOREACH_BACKWARDS(check, (char**) NULL)
assert_not_reached();
- STRV_FOREACH_BACKWARDS(check, (char**) { NULL })
+ STRV_FOREACH_BACKWARDS(check, STRV_MAKE_EMPTY)
assert_not_reached();
+
+ unsigned count = 0;
+ STRV_FOREACH_BACKWARDS(check, STRV_MAKE("ONE"))
+ count++;
+ assert_se(count == 1);
}
TEST(strv_foreach_pair) {
diff --git a/test/TEST-72-SYSUPDATE/Makefile b/test/TEST-72-SYSUPDATE/Makefile
new file mode 120000
index 0000000000..e9f93b1104
--- /dev/null
+++ b/test/TEST-72-SYSUPDATE/Makefile
@@ -0,0 +1 @@
+../TEST-01-BASIC/Makefile \ No newline at end of file
diff --git a/test/TEST-72-SYSUPDATE/test.sh b/test/TEST-72-SYSUPDATE/test.sh
new file mode 100755
index 0000000000..471b02e9a3
--- /dev/null
+++ b/test/TEST-72-SYSUPDATE/test.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*-
+# ex: ts=8 sw=4 sts=4 et filetype=sh
+set -e
+
+TEST_DESCRIPTION="test sysupdate"
+
+# shellcheck source=test/test-functions
+. "${TEST_BASE_DIR:?}/test-functions"
+
+test_append_files() {
+ inst_binary sha256sum
+}
+
+do_test "$@"
diff --git a/test/units/testsuite-72.service b/test/units/testsuite-72.service
new file mode 100644
index 0000000000..1640350578
--- /dev/null
+++ b/test/units/testsuite-72.service
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+[Unit]
+Description=TEST-72-SYSUPDATE
+
+[Service]
+ExecStartPre=rm -f /failed /testok
+ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh
+Type=oneshot
diff --git a/test/units/testsuite-72.sh b/test/units/testsuite-72.sh
new file mode 100755
index 0000000000..9effc982ba
--- /dev/null
+++ b/test/units/testsuite-72.sh
@@ -0,0 +1,170 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*-
+# ex: ts=8 sw=4 sts=4 et filetype=sh
+set -eux
+set -o pipefail
+
+SYSUPDATE=/lib/systemd/systemd-sysupdate
+
+if ! test -x "$SYSUPDATE"; then
+ echo "no systemd-sysupdate" >/skipped
+ exit 0
+fi
+
+export SYSTEMD_PAGER=cat
+export SYSTEMD_LOG_LEVEL=debug
+
+rm -f /var/tmp/72-joined.raw
+truncate -s 10M /var/tmp/72-joined.raw
+
+sfdisk /var/tmp/72-joined.raw <<EOF
+label: gpt
+unit: sectors
+sector-size: 512
+
+size=2048, type=4f68bce3-e8cd-4db1-96e7-fbcaf984b709, name=_empty
+size=2048, type=4f68bce3-e8cd-4db1-96e7-fbcaf984b709, name=_empty
+size=2048, type=2c7357ed-ebd2-46d9-aec1-23d437ec2bf5, name=_empty
+size=2048, type=2c7357ed-ebd2-46d9-aec1-23d437ec2bf5, name=_empty
+EOF
+
+rm -rf /var/tmp/72-dirs
+
+rm -rf /var/tmp/72-defs
+mkdir -p /var/tmp/72-defs
+
+cat >/var/tmp/72-defs/01-first.conf <<"EOF"
+[Source]
+Type=regular-file
+Path=/var/tmp/72-source
+MatchPattern=part1-@v.raw
+
+[Target]
+Type=partition
+Path=/var/tmp/72-joined.raw
+MatchPattern=part1-@v
+MatchPartitionType=root-x86-64
+EOF
+
+cat >/var/tmp/72-defs/02-second.conf <<"EOF"
+[Source]
+Type=regular-file
+Path=/var/tmp/72-source
+MatchPattern=part2-@v.raw.gz
+
+[Target]
+Type=partition
+Path=/var/tmp/72-joined.raw
+MatchPattern=part2-@v
+MatchPartitionType=root-x86-64-verity
+EOF
+
+cat >/var/tmp/72-defs/03-third.conf <<"EOF"
+[Source]
+Type=directory
+Path=/var/tmp/72-source
+MatchPattern=dir-@v
+
+[Target]
+Type=directory
+Path=/var/tmp/72-dirs
+CurrentSymlink=/var/tmp/72-dirs/current
+MatchPattern=dir-@v
+InstancesMax=3
+EOF
+
+rm -rf /var/tmp/72-source
+mkdir -p /var/tmp/72-source
+
+new_version() {
+ # Create a pair of random partition payloads, and compress one
+ dd if=/dev/urandom of="/var/tmp/72-source/part1-$1.raw" bs=1024 count=1024
+ 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"
+
+ 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 )
+}
+
+update_now() {
+ # 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
+
+ "$SYSUPDATE" --definitions=/var/tmp/72-defs --verify=no check-new
+ "$SYSUPDATE" --definitions=/var/tmp/72-defs --verify=no update
+ ( ! "$SYSUPDATE" --definitions=/var/tmp/72-defs --verify=no check-new )
+}
+
+verify_version() {
+ # Expects: version ID + sector offset of both partitions to compare
+ 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"
+ 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
+}
+
+# Install initial version and verify
+new_version v1
+update_now
+verify_version v1 1024 3072
+
+# Create second version, update and verify that it is added
+new_version v2
+update_now
+verify_version v2 2048 4096
+
+# Create third version, update and verify it replaced the first version
+new_version v3
+update_now
+verify_version v3 1024 3072
+
+# 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
+# abstracted in curl for us, and since our main goal is to test our own code
+# (and not curl) this test should be quite good even if not comprehensive. This
+# will test the SHA256SUMS logic at least (we turn off GPG validation though,
+# see above)
+new_version v4
+
+cat >/var/tmp/72-defs/02-second.conf <<"EOF"
+[Source]
+Type=url-file
+Path=file:///var/tmp/72-source
+MatchPattern=part2-@v.raw.gz
+
+[Target]
+Type=partition
+Path=/var/tmp/72-joined.raw
+MatchPattern=part2-@v
+MatchPartitionType=root-x86-64-verity
+EOF
+
+cat >/var/tmp/72-defs/03-third.conf <<"EOF"
+[Source]
+Type=url-tar
+Path=file:///var/tmp/72-source
+MatchPattern=dir-@v.tar.gz
+
+[Target]
+Type=directory
+Path=/var/tmp/72-dirs
+CurrentSymlink=/var/tmp/72-dirs/current
+MatchPattern=dir-@v
+InstancesMax=3
+EOF
+
+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
+
+echo OK >/testok
+
+exit 0
diff --git a/units/meson.build b/units/meson.build
index 2bb0a8e845..8a3bd0da51 100644
--- a/units/meson.build
+++ b/units/meson.build
@@ -140,6 +140,8 @@ units = [
['systemd-reboot.service', ''],
['systemd-rfkill.socket', 'ENABLE_RFKILL'],
['systemd-sysext.service', 'ENABLE_SYSEXT'],
+ ['systemd-sysupdate.timer', 'ENABLE_SYSUPDATE'],
+ ['systemd-sysupdate-reboot.timer', 'ENABLE_SYSUPDATE'],
['systemd-sysusers.service', 'ENABLE_SYSUSERS',
'sysinit.target.wants/'],
['systemd-tmpfiles-clean.service', 'ENABLE_TMPFILES'],
@@ -236,6 +238,8 @@ in_units = [
['systemd-suspend.service', ''],
['systemd-sysctl.service', '',
'sysinit.target.wants/'],
+ ['systemd-sysupdate.service', 'ENABLE_SYSUPDATE'],
+ ['systemd-sysupdate-reboot.service', 'ENABLE_SYSUPDATE'],
['systemd-timedated.service', 'ENABLE_TIMEDATED',
'dbus-org.freedesktop.timedate1.service'],
['systemd-timesyncd.service', 'ENABLE_TIMESYNCD'],
diff --git a/units/systemd-sysupdate-reboot.service.in b/units/systemd-sysupdate-reboot.service.in
new file mode 100644
index 0000000000..9d7b7d1657
--- /dev/null
+++ b/units/systemd-sysupdate-reboot.service.in
@@ -0,0 +1,20 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Reboot Automatically After System Update
+Documentation=man:systemd-sysupdate-reboot.service(8)
+ConditionVirtualization=!container
+
+[Service]
+Type=oneshot
+ExecStart={{ROOTLIBEXECDIR}}/systemd-sysupdate reboot
+
+[Install]
+Also=systemd-sysupdate-reboot.timer
diff --git a/units/systemd-sysupdate-reboot.timer b/units/systemd-sysupdate-reboot.timer
new file mode 100644
index 0000000000..95a44bfea7
--- /dev/null
+++ b/units/systemd-sysupdate-reboot.timer
@@ -0,0 +1,20 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Reboot Automatically After System Update
+Documentation=man:systemd-sysupdate-reboot.service(8)
+ConditionVirtualization=!container
+
+[Timer]
+OnCalendar=4:10
+RandomizedDelaySec=30min
+
+[Install]
+WantedBy=timers.target
diff --git a/units/systemd-sysupdate.service.in b/units/systemd-sysupdate.service.in
new file mode 100644
index 0000000000..085a9c4a22
--- /dev/null
+++ b/units/systemd-sysupdate.service.in
@@ -0,0 +1,34 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Automatic System Update
+Documentation=man:systemd-sysupdate.service(8)
+Wants=network-online.target
+After=network-online.target
+ConditionVirtualization=!container
+
+[Service]
+Type=simple
+NotifyAccess=main
+ExecStart={{ROOTLIBEXECDIR}}/systemd-sysupdate update
+CapabilityBoundingSet=CAP_CHOWN CAP_FOWNER CAP_FSETID CAP_MKNOD CAP_SETFCAP CAP_SYS_ADMIN CAP_SETPCAP CAP_DAC_OVERRIDE CAP_LINUX_IMMUTABLE
+NoNewPrivileges=yes
+MemoryDenyWriteExecute=yes
+ProtectHostname=yes
+RestrictRealtime=yes
+RestrictNamespaces=net
+RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
+SystemCallFilter=@system-service @mount
+SystemCallErrorNumber=EPERM
+SystemCallArchitectures=native
+LockPersonality=yes
+
+[Install]
+Also=systemd-sysupdate.timer
diff --git a/units/systemd-sysupdate.timer b/units/systemd-sysupdate.timer
new file mode 100644
index 0000000000..6ecd98d13e
--- /dev/null
+++ b/units/systemd-sysupdate.timer
@@ -0,0 +1,30 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Automatic System Update
+Documentation=man:systemd-sysupdate.service(8)
+
+# For containers we assume that the manager will handle updates. And we likely
+# can't even access our backing block device anyway.
+ConditionVirtualization=!container
+
+[Timer]
+# Trigger the update 15min after boot, and then – on average – every 6h, but
+# randomly distributed in a 2h…6h interval. In addition trigger things
+# persistently once on each Saturday, to ensure that even on systems that are
+# never booted up for long we have a chance to to do the update.
+OnBootSec=15min
+OnUnitActiveSec=2h
+OnCalendar=Sat
+RandomizedDelaySec=4h
+Persistent=yes
+
+[Install]
+WantedBy=timers.target