diff --git a/TODO.md b/TODO.md
index 23edf2519e8..d789f86ea61 100644
--- a/TODO.md
+++ b/TODO.md
@@ -156,8 +156,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
- make vpick assessment counting work
-- sysext: condition confext/sysext on machine tags, too
-
- sysupdate: add health check logic, revert updates if fail
- confext/sysext: add policy file concept: json files that encode for relevant
@@ -1229,9 +1227,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
- complete varlink interface
- download images into .v/ dirs
-- importd: support image signature verification with PKCS#7 + OpenBSD signify
- logic, as alternative to crummy gpg
-
- In .socket units, add ConnectStream=, ConnectDatagram=,
ConnectSequentialPacket= that create a socket, and then *connect to* rather than
listen on some socket. Then, add a new setting WriteData= that takes some
diff --git a/man/sysupdate.d.xml b/man/sysupdate.d.xml
index ee4d0de04dd..8e899837e92 100644
--- a/man/sysupdate.d.xml
+++ b/man/sysupdate.d.xml
@@ -140,7 +140,10 @@
For HTTP/HTTPS sources a simple scheme is used: a manifest file
SHA256SUMS, following the format defined by sha256sum1,
- lists file names and their SHA256 hashes.
+ lists file names and their SHA256 hashes. Alternatively, a Uapi16Manifest file
+ following the UAPI.16 File
+ Manifest specification may be used, see ManifestFormat=
+ below.
Transfers are done in the alphabetical order of the .transfer file names they are
@@ -521,6 +524,24 @@
keyring in /etc/systemd/import-pubring.pgp (or, if that does not
exist, /etc/systemd/import-pubring.gpg).
+ For UAPI.16 manifests (see ManifestFormat=) the signature is embedded in
+ the manifest itself, as a signature object. Only PKCS#7 signatures are supported, which must have
+ been made with the private key of one of the X.509 certificates found in the File
+ Hierarchy for the Verification of OS Artifacts (VOA), i.e. in
+ OS/manifest/sysupdate/x509/NAME-certificate.pem
+ below the VOA load paths /etc/voa/, /run/voa/,
+ /usr/local/share/voa/ and /usr/share/voa/, where
+ OS is derived from
+ os-release5 of
+ the system (or of the root directory or image specified with or
+ ). Only artifact verifiers are supported, i.e. certificates are not checked
+ against trust anchors, they are trusted as they are. If no certificates are found the transfer
+ fails. Such signatures may be generated with systemd-manifest make --private-key=,
+ and checked against the same certificates with systemd-manifest verify
+ --voa-context=sysupdate, see
+ systemd-manifest1.
+
This option is essential to provide integrity guarantees for downloaded resources and thus
should be left enabled, outside of test environments.
@@ -634,11 +655,12 @@
Specifies where to find source versions of this resource.If the source type is selected as url-file or
- url-tar this must be a HTTP/HTTPS URL. The URL is suffixed with
- /SHA256SUMS to acquire the manifest file, with
- /SHA256SUMS.gpg 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.
+ url-tar this must be a HTTP/HTTPS URL. Unless ManifestPath=
+ is specified, the URL is suffixed with /SHA256SUMS (and
+ /SHA256SUMS.gpg for the detached signature file) or with
+ /Uapi16Manifest (which carries its signature inline), depending on
+ ManifestFormat=, to acquire the manifest file. It is suffixed with the file names
+ listed in the manifest file in case an update is executed and a resource shall be downloaded.
Alternatively, for these two source types a URL of the form
provider:[socket]/resource
@@ -698,8 +720,49 @@
url-file or url-tar source. Takes
sha256sums, which selects a SHA256SUMS file as generated by
sha256sum1,
- accompanied by a detached SHA256SUMS.gpg signature, see above. Defaults to
- sha256sums. This option has no effect for other source resource types.
+ accompanied by a detached SHA256SUMS.gpg signature, see above. Or
+ uapi16, which selects a Uapi16Manifest file following the
+ UAPI.16 File
+ Manifest specification, i.e. a JSON-SEQ stream with one object per file, carrying the file
+ name, size and SHA256 digest of each file, as generated by
+ systemd-manifest1
+ or systemd-dissect --uapi16. Only regular files listed in the manifest are
+ considered, and entries whose GPT partition type does not match the target's
+ MatchPartitionType= are ignored. The signatures embedded in UAPI.16 manifests are
+ verified against the certificates found in the VOA hierarchy, see
+ Verify= above. Defaults to sha256sums. This option has no
+ effect for other source resource types.
+
+ If a file object of a UAPI.16 manifest carries a contents array, the file
+ is acquired from the sources listed there, instead of by its name relative to
+ Path=. Each entry is an alternative location of the same data, tried in the listed
+ order until one works: entries with a file field are downloaded from that
+ (relative) file name resolved against Path=, entries with a
+ url field from that URL, and entries with a literal field carry
+ the data inline. Entries with none of these three fields refer to a file of the object's name,
+ resolved against Path=. Relative file names are resolved against
+ Path= even if ManifestPath= is set, as the latter only
+ redirects the acquisition of the manifest itself. The encoding field
+ may be one of gzip, xz, zstd,
+ bzip2 or lz4, in which case the data is decompressed on
+ acquisition. The declared encoding is binding: the data is
+ strictly decoded with that algorithm (or taken as is, if none is declared), no detection of the
+ compression format from the data takes place. A source may carry more than the file, in which case
+ the range of the decoded data starting at offset and extending over the file's
+ size is used. Downloaded sources are verified against their
+ encodedSha256 digest if they carry one, except for sources that are not encoded
+ but carry more than the file: of those only the range that makes up the file is downloaded (via an
+ HTTP range request), and their encodedSha256 digest is not checked. If the source
+ is encoded, carries more than the file, or has no digest of its own, the decoded file is checked
+ against the file's sha256 digest. Downloaded sources of url-tar
+ resources, as well as inline data that is encoded, must consist of the file and nothing else, other
+ such sources are ignored.
+
+ The extended attributes in the user namespace listed in the
+ extendedAttributes field of a file object of a UAPI.16 manifest are applied to the
+ file or directory created when installing it. This is done on a best-effort basis: if the file system
+ does not support extended attributes, or they cannot be set for other reasons, the instance is
+ installed without them.
@@ -724,6 +787,136 @@
+
+
+ MatchXAttr=
+
+ Takes a boolean argument, defaults to on. If enabled, source instances may be
+ restricted to specific systems via extended attributes in the user namespace. Each
+ of the following extended attributes is checked against a property of the system that is being
+ updated (i.e. the system specified with /, if
+ used):
+
+
+ Extended attributes checked
+
+
+
+
+
+ Extended attribute
+ Matched against
+
+
+
+
+ user.uapi.match_hostname
+ The hostname, as configured in /etc/hostname if operating on a
+ root directory or image, the current hostname otherwise.
+
+
+ user.uapi.match_machine_id
+ The machine ID, see
+ machine-id5,
+ formatted as 32 lowercase hexadecimal characters.
+
+
+ user.uapi.match_machine_tags
+ The machine tags, see TAGS= in
+ machine-info5.
+
+
+ user.uapi.match_image_id
+ IMAGE_ID= from
+ os-release5.
+
+
+ user.uapi.match_image_version
+ IMAGE_VERSION= from
+ os-release5.
+
+
+ user.uapi.match_os_id
+ ID= from
+ os-release5.
+
+
+ user.uapi.match_os_version
+ VERSION_ID= from
+ os-release5.
+
+
+
+
+
+ Each extended attribute contains a list of entries, separated by NUL bytes. A value without
+ any NUL byte is a list with a single entry. For all extended attributes but the two version ones,
+ each entry is a shell-style glob pattern (see
+ fnmatch3),
+ and the instance matches if any of the entries matches the system property.
+ For user.uapi.match_machine_tags the instance matches if any entry matches any of
+ the machine's tags. For user.uapi.match_image_version and
+ user.uapi.match_os_version each entry is a version comparison expression, i.e.
+ a version prefixed with one of <, <=,
+ =, !=, >=, >
+ (or a glob pattern, optionally prefixed with $= or !$=), the
+ same syntax ConditionVersion= in
+ systemd.unit5
+ accepts, and the instance matches if all of the expressions hold.
+
+ An instance that does not carry the extended attribute is not restricted by it. If the system
+ property is not set (for example, if /etc/os-release carries no
+ IMAGE_ID=), an instance that carries the extended attribute does not match. An
+ instance whose extended attribute cannot be parsed does not match either. If multiple of these
+ extended attributes are set, instances must match all of them. Instances that do not
+ match are ignored, as if they did not exist. The system properties are only read once an instance
+ actually carries one of these extended attributes.
+
+ The extended attributes are taken from the extendedAttributes field of the
+ file objects in UAPI.16 manifests (see ManifestFormat=), as well as from the files
+ and directories in the file system, for local sources. When an instance is installed from a source
+ with a UAPI.16 manifest, the user extended attributes listed in the manifest are
+ applied to the file or directory created (see ManifestFormat= above), hence the
+ restrictions are retained. Installed instances are checked only if MatchXAttr= is
+ enabled in the [Target] section too, see below.
+
+ This setting is intended to make specific versions unavailable to certain systems
+ altogether, for example to offer a build only to certain machines, or only to systems of a certain
+ image type. Instances that do not match are simply not candidates for the system: the newest version
+ with matching instances is picked instead. It is not intended as a mechanism to make systems pass
+ through specific intermediary versions while updating, nor to offer different builds of the same
+ version to different systems.
+
+ Note that a version is only offered as update if all transfers have a matching instance of
+ it. If no instance of a version matches for one transfer, the whole version is considered incomplete
+ and is not offered at all. Hence, the extended attributes should typically be applied to the
+ instances of all transfers of a version alike. This also means that this setting cannot be used to
+ make a transfer optional. To make parts of an update optional, use Features=
+ instead, see above.
+
+ Example: the extended attribute of an instance suitable for any system running Fedora 41 or
+ newer (but older than 50) may be set with:
+
+ # setfattr -n user.uapi.match_os_version -v '0x3e3d3431003c353000' foo_7.raw
+
+ (This sets the NUL separated list >=41, <50.)
+ Combined with user.uapi.match_os_id set to fedora, only Fedora
+ systems of a suitable version will consider the instance (as long as
+ MatchXAttr= is not turned off in the [Source] section of the transfer
+ file).
+
+ Example: in order to offer version 8 to machines tagged canary before
+ everybody else, set the extended attribute on its instance with:
+
+ # setfattr -n user.uapi.match_machine_tags -v canary foo_8.raw
+
+ Machines that carry the canary tag (see TAGS= in
+ machine-info5)
+ will update to version 8, while all others will skip it and stay at (or update to) version 7. Once
+ version 8 shall be offered to everybody, the extended attribute is simply removed again.
+
+
+
@@ -851,6 +1044,20 @@
+
+ MatchXAttr=
+
+ Takes a boolean argument, defaults to off (unlike the setting of the same name in
+ the [Source] section, which defaults to on). Works like that (see above), but applies to installed
+ instances, whose extended attributes are read from the installed files and directories. Installed instances that do not match
+ are ignored, as if they did not exist: they are neither considered when determining the currently
+ installed version, nor removed to make room for new instances (see
+ InstancesMax=). Not supported for partition targets, as
+ partitions do not carry extended attributes.
+
+
+
+
PartitionUUID=PartitionFlags=
diff --git a/src/shared/data-fd-util.c b/src/shared/data-fd-util.c
index 6b1041e0f3c..e146a8424fe 100644
--- a/src/shared/data-fd-util.c
+++ b/src/shared/data-fd-util.c
@@ -18,6 +18,21 @@
/* If memfd didn't work out, then let's use a file in /tmp up to a size of 1M. If it's large than that use /var/tmp/ instead. */
#define DATA_FD_TMP_LIMIT (1U * U64_MB)
+static int memfd_seal_and_reopen(int fd, int mode) {
+ int r;
+
+ assert(fd >= 0);
+
+ /* Seals the memfd and returns a new fd to it with the specified access mode, positioned at the
+ * beginning of the file. */
+
+ r = memfd_set_sealed(fd);
+ if (r < 0)
+ return r;
+
+ return fd_reopen(fd, mode);
+}
+
int copy_data_fd_full(int fd, uint64_t max_size) {
_cleanup_close_ int copy_fd = -EBADF, tmp_fd = -EBADF;
const char *td;
@@ -67,20 +82,16 @@ int copy_data_fd_full(int fd, uint64_t max_size) {
if (remaining != UINT64_MAX)
remaining -= n;
+ if (r == 0)
+ /* Did it fit into the limit? If so, we are done. (Reopening positions the new fd
+ * at the beginning of the file, no need to seek.) */
+ return memfd_seal_and_reopen(copy_fd, O_RDONLY);
+
off_t f = lseek(copy_fd, 0, SEEK_SET);
if (f < 0)
return -errno;
if (f != 0)
return -EIO;
-
- if (r == 0) {
- /* Did it fit into the limit? If so, we are done. */
- r = memfd_set_sealed(copy_fd);
- if (r < 0)
- return r;
-
- return TAKE_FD(copy_fd);
- }
}
/* If we have reason to believe this will fit fine in /tmp, then use that as first fallback. */
@@ -223,13 +234,8 @@ int memfd_clone_fd(int fd, const char *name, int mode) {
if (r < 0)
return r;
- if (ro) {
- r = memfd_set_sealed(mfd);
- if (r < 0)
- return r;
-
- return fd_reopen(mfd, mode);
- }
+ if (ro)
+ return memfd_seal_and_reopen(mfd, mode);
off_t f = lseek(mfd, 0, SEEK_SET);
if (f < 0)
@@ -237,3 +243,20 @@ int memfd_clone_fd(int fd, const char *name, int mode) {
return TAKE_FD(mfd);
}
+
+int data_fd_from_iovec(const char *name, const struct iovec *iov) {
+ _cleanup_close_ int fd = -EBADF;
+
+ /* Places the specified data in a sealed memfd and returns a read-only fd to it, positioned at the
+ * beginning of the file. This is useful for passing literal data to other processes, for example as
+ * standard input. Unlike copy_data_fd() this never falls back to temporary files, the data is in
+ * memory already after all. */
+
+ assert(iov);
+
+ fd = memfd_new_and_seal(name, iov->iov_base, iov->iov_len);
+ if (fd < 0)
+ return fd;
+
+ return fd_reopen(fd, O_RDONLY);
+}
diff --git a/src/shared/data-fd-util.h b/src/shared/data-fd-util.h
index 9a9f0c8ed46..570f8b7c9a8 100644
--- a/src/shared/data-fd-util.h
+++ b/src/shared/data-fd-util.h
@@ -10,3 +10,4 @@ static inline int copy_data_fd(int fd) {
int seekable_data_fd(int fd, uint64_t max_size);
int open_seekable_data_fd(int dir_fd, const char *path, uint64_t max_size);
int memfd_clone_fd(int fd, const char *name, int mode);
+int data_fd_from_iovec(const char *name, const struct iovec *iov);
diff --git a/src/shared/forward.h b/src/shared/forward.h
index d8b7b186dd9..3d9a1c03d6c 100644
--- a/src/shared/forward.h
+++ b/src/shared/forward.h
@@ -95,7 +95,9 @@ typedef struct Table Table;
typedef struct Tpm2Context Tpm2Context;
typedef struct Tpm2Handle Tpm2Handle;
typedef struct Tpm2PCRValue Tpm2PCRValue;
+typedef struct Uapi16Contents Uapi16Contents;
typedef struct Uapi16Object Uapi16Object;
+typedef struct Uapi16Xattr Uapi16Xattr;
typedef struct UnitInfo UnitInfo;
typedef struct UnitResult UnitResult;
typedef struct UserRecord UserRecord;
diff --git a/src/shared/uapi16-manifest.c b/src/shared/uapi16-manifest.c
index bff48145756..974fd7e1753 100644
--- a/src/shared/uapi16-manifest.c
+++ b/src/shared/uapi16-manifest.c
@@ -536,7 +536,7 @@ static int json_dispatch_contents(const char *name, sd_json_variant *variant, sd
if (r < 0)
return r;
- if (!!c.file + !!c.url + !!iovec_is_set(&c.literal) > 1)
+ if (!!c.file + !!c.url + uapi16_contents_has_literal(&c) > 1)
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Only one of 'file', 'url', 'literal' may be set for '%s'.", strna(name));
if (c.encoding) {
diff --git a/src/shared/uapi16-manifest.h b/src/shared/uapi16-manifest.h
index 55d6a7b1dcd..38746e17661 100644
--- a/src/shared/uapi16-manifest.h
+++ b/src/shared/uapi16-manifest.h
@@ -26,6 +26,13 @@ typedef struct Uapi16Contents {
.original_size = UINT64_MAX, \
}
+static inline bool uapi16_contents_has_literal(const Uapi16Contents *c) {
+ /* An empty "literal" field is valid (it encodes empty data), hence check whether the field was set,
+ * not whether it carries data: when decoding Base64 we always allocate a buffer, even for empty
+ * data. */
+ return c->literal.iov_base;
+}
+
/* An extended attribute in the "user" namespace */
typedef struct Uapi16Xattr {
char *name; /* The attribute name, without the "user." prefix */
diff --git a/src/shared/varlink-io.systemd.SysUpdate.Provider.c b/src/shared/varlink-io.systemd.SysUpdate.Provider.c
index e1b41750f45..d0d56f5f028 100644
--- a/src/shared/varlink-io.systemd.SysUpdate.Provider.c
+++ b/src/shared/varlink-io.systemd.SysUpdate.Provider.c
@@ -53,7 +53,9 @@ static SD_VARLINK_DEFINE_ENUM_TYPE(
static SD_VARLINK_DEFINE_ENUM_TYPE(
ManifestFormat,
SD_VARLINK_FIELD_COMMENT("A SHA256SUMS file, see ManifestFormat=sha256sums in sysupdate.d(5)."),
- SD_VARLINK_DEFINE_ENUM_VALUE(sha256sums));
+ SD_VARLINK_DEFINE_ENUM_VALUE(sha256sums),
+ SD_VARLINK_FIELD_COMMENT("A UAPI.16 manifest, see ManifestFormat=uapi16 in sysupdate.d(5)."),
+ SD_VARLINK_DEFINE_ENUM_VALUE(uapi16));
static SD_VARLINK_DEFINE_STRUCT_TYPE(
TransferSource,
@@ -68,7 +70,9 @@ static SD_VARLINK_DEFINE_STRUCT_TYPE(
SD_VARLINK_FIELD_COMMENT("The format of the manifest file listing the available source files, see ManifestFormat= in the [Source] section of sysupdate.d(5). If unset, defaults to 'sha256sums'."),
SD_VARLINK_DEFINE_FIELD_BY_TYPE(manifestFormat, ManifestFormat, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("The URL of the manifest file, see ManifestPath= in the [Source] section of sysupdate.d(5). If unset, the manifest is acquired from the source URL, under the file name appropriate for the manifest format. No specifier expansion is applied."),
- SD_VARLINK_DEFINE_FIELD(manifestPath, SD_VARLINK_STRING, SD_VARLINK_NULLABLE));
+ SD_VARLINK_DEFINE_FIELD(manifestPath, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("Whether to match the user.uapi.match_* extended attributes of instances against the local system, see MatchXAttr= in the [Source] section of sysupdate.d(5). If unset, defaults to true."),
+ SD_VARLINK_DEFINE_FIELD(matchXAttr, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE));
static SD_VARLINK_DEFINE_STRUCT_TYPE(
TransferTarget,
@@ -105,7 +109,9 @@ static SD_VARLINK_DEFINE_STRUCT_TYPE(
SD_VARLINK_FIELD_COMMENT("Whether to remove temporary files from previous runs, see RemoveTemporary= in the [Target] section of sysupdate.d(5)."),
SD_VARLINK_DEFINE_FIELD(removeTemporary, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("The path of a symlink to maintain pointing to the newest instance, see CurrentSymlink= in the [Target] section of sysupdate.d(5). No specifier expansion is applied."),
- SD_VARLINK_DEFINE_FIELD(currentSymlink, SD_VARLINK_STRING, SD_VARLINK_NULLABLE));
+ SD_VARLINK_DEFINE_FIELD(currentSymlink, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("Whether to match the user.uapi.match_* extended attributes of instances against the local system, see MatchXAttr= in the [Target] section of sysupdate.d(5). If unset, defaults to false."),
+ SD_VARLINK_DEFINE_FIELD(matchXAttr, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE));
static SD_VARLINK_DEFINE_STRUCT_TYPE(
Transfer,
diff --git a/src/sysupdate/sysupdate-instance.c b/src/sysupdate/sysupdate-instance.c
index 91b5d988d99..ef769bac103 100644
--- a/src/sysupdate/sysupdate-instance.c
+++ b/src/sysupdate/sysupdate-instance.c
@@ -3,6 +3,7 @@
#include "alloc-util.h"
#include "log.h"
#include "sysupdate-instance.h"
+#include "uapi16-manifest.h"
void instance_metadata_destroy(InstanceMetadata *m) {
assert(m);
@@ -58,5 +59,11 @@ Instance *instance_free(Instance *i) {
free(i->path);
partition_info_destroy(&i->partition_info);
+ FOREACH_ARRAY(s, i->sources, i->n_sources)
+ uapi16_contents_done(s);
+ free(i->sources);
+
+ uapi16_xattr_array_free(i->user_xattrs, i->n_user_xattrs);
+
return mfree(i);
}
diff --git a/src/sysupdate/sysupdate-instance.h b/src/sysupdate/sysupdate-instance.h
index b482ab3b2cd..584937159e4 100644
--- a/src/sysupdate/sysupdate-instance.h
+++ b/src/sysupdate/sysupdate-instance.h
@@ -8,8 +8,8 @@
#include "sysupdate-partition.h"
typedef 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 */
+ /* Various bits of metadata for each instance, that is either derived from the filename/GPT label,
+ * from metadata of the file/partition itself or from the manifest */
char *version;
sd_id128_t partition_uuid;
bool partition_uuid_set;
@@ -49,6 +49,16 @@ struct Instance {
char *path; /* includes the `.sysupdate.partial.` (etc.) prefix, if applicable */
PartitionInfo partition_info;
+ /* The sources we can acquire the resource from (for web backed sources) */
+ Uapi16Contents *sources;
+ size_t n_sources;
+
+ /* The "user" extended attributes listed in the manifest (for web backed sources), which we apply
+ * to the inode we create when installing the instance */
+ Uapi16Xattr *user_xattrs;
+ size_t n_user_xattrs;
+
+ /* The current state of this instance */
bool is_partial;
bool is_pending;
};
diff --git a/src/sysupdate/sysupdate-resource.c b/src/sysupdate/sysupdate-resource.c
index c75f9fdd10f..1744b113822 100644
--- a/src/sysupdate/sysupdate-resource.c
+++ b/src/sysupdate/sysupdate-resource.c
@@ -8,6 +8,7 @@
#include "blockdev-util.h"
#include "build-path.h"
#include "chase.h"
+#include "compress.h"
#include "device-util.h"
#include "dirent-util.h"
#include "env-util.h"
@@ -37,6 +38,9 @@
#include "sysupdate-pattern.h"
#include "sysupdate-resource.h"
#include "time-util.h"
+#include "uapi16-manifest.h"
+#include "web-util.h"
+#include "xattr-match.h"
void resource_destroy(Resource *rr) {
assert(rr);
@@ -62,6 +66,12 @@ bool resource_has_glob_directory_pattern(Resource *rr) {
return false;
}
+unsigned resource_xattr_match_mask(const Resource *rr) {
+ assert(rr);
+
+ return rr->match_xattr ? XATTR_MATCH_MASK_ALL : 0;
+}
+
static int resource_add_instance(
Resource *rr,
const char *path,
@@ -98,7 +108,8 @@ static int resource_load_from_directory_recursive(
const char* relpath_for_matching,
mode_t m,
bool ancestor_is_partial,
- bool ancestor_is_pending) {
+ bool ancestor_is_pending,
+ const XAttrMatchFilter *filter) {
int r;
for (;;) {
@@ -203,7 +214,7 @@ static int resource_load_from_directory_recursive(
return log_error_errno(errno, "Failed to open directory %s/%s: %m", rr->path, rel_joined);
}
- r = resource_load_from_directory_recursive(rr, subdir, rel_joined, rel_joined_for_matching, m, is_partial, is_pending);
+ r = resource_load_from_directory_recursive(rr, subdir, rel_joined, rel_joined_for_matching, m, is_partial, is_pending, filter);
if (r < 0)
return r;
continue;
@@ -212,6 +223,13 @@ static int resource_load_from_directory_recursive(
if (S_ISDIR(st.st_mode) && m != S_IFDIR)
continue;
+ /* Skip instances restricted to other systems via extended attributes */
+ r = xattr_match_filter_test_inode(filter, dirfd(d), de->d_name);
+ if (r < 0)
+ return log_error_errno(r, "Failed to check extended attributes of '%s': %m", de->d_name);
+ if (r == 0)
+ continue;
+
joined = path_join(rr->path, rel_joined);
if (!joined)
return log_oom();
@@ -239,7 +257,8 @@ static int resource_load_from_directory_recursive(
static int resource_load_from_directory(
Resource *rr,
- mode_t m) {
+ mode_t m,
+ const XAttrMatchFilter *filter) {
_cleanup_closedir_ DIR *d = NULL;
assert(rr);
@@ -256,7 +275,15 @@ static int resource_load_from_directory(
return log_error_errno(errno, "Failed to open directory '%s': %m", rr->path);
}
- return resource_load_from_directory_recursive(rr, d, /* relpath= */ NULL, /* relpath_for_matching= */ NULL, m, /* ancestor_is_partial= */ false, /* ancestor_is_pending= */ false);
+ return resource_load_from_directory_recursive(
+ rr,
+ d,
+ /* relpath= */ NULL,
+ /* relpath_for_matching= */ NULL,
+ m,
+ /* ancestor_is_partial= */ false,
+ /* ancestor_is_pending= */ false,
+ filter);
}
static int resource_load_from_blockdev(Resource *rr) {
@@ -361,11 +388,22 @@ static int resource_load_from_blockdev(Resource *rr) {
return 0;
}
+static const char* const resource_manifest_filename_table[_MANIFEST_FORMAT_MAX] = {
+ [MANIFEST_FORMAT_SHA256SUMS] = "SHA256SUMS",
+ [MANIFEST_FORMAT_UAPI16] = UAPI16_MANIFEST_FILENAME,
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(resource_manifest_filename, ManifestFormat);
+
+static bool resource_manifest_has_inline_signatures(ManifestFormat style) {
+ return style == MANIFEST_FORMAT_UAPI16;
+}
+
static int resource_manifest_url(Resource *rr, char **ret) {
int r;
assert(rr);
- assert(rr->manifest_format == MANIFEST_FORMAT_SHA256SUMS);
+ assert(IN_SET(rr->manifest_format, MANIFEST_FORMAT_SHA256SUMS, MANIFEST_FORMAT_UAPI16));
assert(ret);
/* Returns the URL to acquire the manifest from: either the one explicitly configured, or the
@@ -379,9 +417,10 @@ static int resource_manifest_url(Resource *rr, char **ret) {
return 0;
}
- r = import_url_append_component(rr->path, "SHA256SUMS", ret);
+ const char *fn = resource_manifest_filename_to_string(rr->manifest_format);
+ r = import_url_append_component(rr->path, fn, ret);
if (r < 0)
- return log_error_errno(r, "Failed to append SHA256SUMS to URL: %m");
+ return log_error_errno(r, "Failed to append '%s' to URL '%s': %m", fn, rr->path);
return 0;
}
@@ -394,11 +433,11 @@ static int download_manifest(
int r;
- assert(manifest_format == MANIFEST_FORMAT_SHA256SUMS);
+ assert(IN_SET(manifest_format, MANIFEST_FORMAT_SHA256SUMS, MANIFEST_FORMAT_UAPI16));
assert(manifest_url);
assert(ret);
- /* Download a SHA256SUMS file as manifest */
+ /* Download a SHA256SUMS/Uapi16Manifest file as manifest */
_cleanup_close_pair_ int pfd[2] = EBADF_PAIR;
if (pipe2(pfd, O_CLOEXEC) < 0)
@@ -424,7 +463,7 @@ static int download_manifest(
"raw",
"--direct", /* just download the specified URL, don't download anything else */
"--checksum-file=yes", /* whatever its name, look for .gpg/.asc signature */
- "--verify", verify_signature ? "signature" : "no", /* verify the manifest file */
+ "--verify", verify_signature && !resource_manifest_has_inline_signatures(manifest_format) ? "signature" : "no", /* verify the manifest file */
manifest_url,
"-", /* write to stdout */
NULL
@@ -626,10 +665,318 @@ static int sha256sums_process(
}
}
+static int instance_copy_uapi16(Instance *instance, const Uapi16Object *object) {
+ assert(instance);
+ assert(object);
+
+ /* We verified earlier that these fields are definitely set in the UAPI 16 objects */
+ assert(object->type == S_IFREG);
+ assert(iovec_is_set(&object->sha256));
+ assert(object->size != UINT64_MAX);
+
+ if (instance->metadata.sha256sum_set) {
+ if (memcmp_nn(instance->metadata.sha256sum, SHA256_DIGEST_SIZE, object->sha256.iov_base, object->sha256.iov_len) != 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "SHA256 sum parsed from filename and manifest don't match, refusing.");
+ } else {
+ assert(object->sha256.iov_len == SHA256_DIGEST_SIZE);
+ memcpy(instance->metadata.sha256sum, object->sha256.iov_base, SHA256_DIGEST_SIZE);
+ instance->metadata.sha256sum_set = true;
+ }
+
+ if (instance->metadata.size != UINT64_MAX) {
+ if (instance->metadata.size != object->size)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Size parsed from filename and manifest don't match, refusing.");
+ } else
+ instance->metadata.size = object->size;
+
+ if (instance->metadata.mode == MODE_INVALID)
+ instance->metadata.mode = object->mode;
+ if (instance->metadata.mtime == USEC_INFINITY && object->mtime_nsec < (NSEC_INFINITY / NSEC_PER_USEC) * NSEC_PER_USEC)
+ instance->metadata.mtime = object->mtime_nsec / NSEC_PER_USEC;
+ if (!instance->metadata.partition_uuid_set && object->gpt_uuid_set) {
+ instance->metadata.partition_uuid = object->gpt_uuid;
+ instance->metadata.partition_uuid_set = true;
+ }
+
+ if (!instance->metadata.partition_flags_set && object->gpt_flags_set) {
+ instance->metadata.partition_flags = object->gpt_flags;
+ instance->metadata.partition_flags_set = true;
+ }
+
+ if (instance->metadata.no_auto < 0 && object->gpt_no_auto >= 0)
+ instance->metadata.no_auto = object->gpt_no_auto;
+ if (instance->metadata.growfs < 0 && object->gpt_growfs >= 0)
+ instance->metadata.growfs = object->gpt_growfs;
+ if (instance->metadata.read_only < 0 && object->read_only >= 0)
+ instance->metadata.read_only = object->read_only;
+
+ return 0;
+}
+
+static bool uapi16_encoding_is_supported(const char *encoding) {
+ /* The compression algorithms systemd-pull and systemd-import can decode, by their canonical names,
+ * which is what we pass on to them via --compression= */
+ Compression c = compression_from_string(encoding);
+ return c >= 0 && c != COMPRESSION_NONE && compression_supported(c);
+}
+
+static int uapi16_object_resolve_sources(Resource *rr, Uapi16Object *o) {
+ size_t n = 0;
+ int r;
+
+ assert(rr);
+ assert(o);
+
+ /* Validates the "contents" array of a file object, i.e. the list of alternative sources the file may
+ * be acquired from. Resolves relative "file" references against Path= into absolute URLs, and drops
+ * entries we cannot make use of. Returns > 0 if the object is usable (i.e. at least one source
+ * remains, or none was listed to begin with, in which case the file is acquired by its name), and 0
+ * if it shall be skipped. */
+
+ for (size_t k = 0; k < o->n_contents; k++) {
+ Uapi16Contents *c = o->contents + k;
+ bool usable = true;
+
+ /* An entry with neither "file", "url" nor "literal" set refers to a file of the object's
+ * name, see the UAPI.16 specification. */
+ if (!c->file && !c->url && !uapi16_contents_has_literal(c)) {
+ c->file = strdup(o->name);
+ if (!c->file)
+ return log_oom();
+ }
+
+ if (c->file) {
+ /* Refuse "%" for the same reason as in sha256sums_line() */
+ if (!uapi16_name_is_valid(c->file) || strchr(c->file, '%')) {
+ log_warning("File '%s' refers to invalid file name '%s' in its contents, ignoring source.", o->name, c->file);
+ usable = false;
+ } else {
+ /* "file" references are relative to Path=, even if the manifest itself is
+ * acquired from elsewhere via ManifestPath=, the same way as files that list
+ * no "contents" at all */
+ _cleanup_free_ char *url = NULL;
+ r = import_url_append_component(rr->path, c->file, &url);
+ if (r < 0)
+ return log_error_errno(r, "Failed to build URL for '%s': %m", c->file);
+
+ free_and_replace(c->url, url);
+ c->file = mfree(c->file);
+ }
+
+ } else if (c->url) {
+ if (!http_url_is_valid(c->url) && !file_url_is_valid(c->url)) {
+ log_warning("File '%s' refers to invalid URL '%s' in its contents, ignoring source.", o->name, c->url);
+ usable = false;
+ }
+ }
+
+ if (usable && c->encoding && !uapi16_encoding_is_supported(c->encoding)) {
+ log_warning("File '%s' has a source with unsupported encoding '%s', ignoring source.", o->name, c->encoding);
+ usable = false;
+ }
+
+ /* Whether the decoded source consists of the file and nothing else */
+ bool whole = c->offset == 0 && (c->original_size == UINT64_MAX || c->original_size == o->size);
+
+ if (usable && !whole) {
+ if (!c->url && c->encoding) {
+ /* We cannot cut into encoded inline data, and leave decoding to systemd-import */
+ log_warning("File '%s' has encoded inline data that carries more than the file, ignoring source.", o->name);
+ usable = false;
+ } else if (c->url && rr->type == RESOURCE_URL_TAR) {
+ /* systemd-pull can only acquire a range of a source for raw images */
+ log_warning("File '%s' has a source that carries more than the file, which is not supported for tarballs, ignoring source.", o->name);
+ usable = false;
+ }
+ }
+
+ if (usable && c->url && !iovec_is_set(&c->encoded_sha256)) {
+ /* Without the digest of the downloaded bytes we cannot verify the download as a
+ * whole. If the source is not encoded and consists of the file and nothing else, it
+ * is identical to the file though, and we can use the file's digest. Otherwise we
+ * can still verify the file once it has been decoded and cut out of the source, as
+ * long as its digest is known. */
+ if (!c->encoding && whole) {
+ if (!iovec_memdup(&o->sha256, &c->encoded_sha256))
+ return log_oom();
+ } else if (!iovec_is_set(&o->sha256)) {
+ log_warning("File '%s' has a source without SHA256 digest that does not consist of the file alone, and no SHA256 digest of its own, ignoring source.", o->name);
+ usable = false;
+ }
+ }
+
+ if (!usable) {
+ uapi16_contents_done(c);
+ continue;
+ }
+
+ /* Keep the array compact */
+ if (n != k) {
+ o->contents[n] = *c;
+ *c = UAPI16_CONTENTS_NULL;
+ }
+ n++;
+ }
+
+ if (o->n_contents > 0 && n == 0) {
+ log_warning("File '%s' has no usable source, skipping.", o->name);
+ o->n_contents = 0;
+ return 0;
+ }
+
+ o->n_contents = n;
+ return 1;
+}
+
+typedef struct Uapi16ProcessContext {
+ Resource *resource;
+ const XAttrMatchFilter *filter;
+ const GptPartitionType *match_partition_type;
+} Uapi16ProcessContext;
+
+static int uapi16_process_object(Uapi16Object *object, size_t index, void *userdata) {
+ Uapi16ProcessContext *pc = ASSERT_PTR(userdata);
+ Resource *rr = ASSERT_PTR(pc->resource);
+ int r;
+
+ assert(object);
+
+ /* Honour validity properties, but otherwise ignore intro object */
+ if (index == 0) {
+ r = uapi16_object_check_validity(object);
+ if (r < 0) {
+ if (verify_freshness())
+ return log_error_errno(r, "Manifest file marked invalid: %m");
+
+ log_warning_errno(r, "Manifest file marked invalid, but freshness checks disabled, proceeding.");
+ }
+
+ return 0;
+ }
+
+ /* Ignore non-regular files */
+ if (!S_ISREG(object->type)) {
+ log_debug("Skipping file '%s' as it is not a regular file.", object->name);
+ return 0;
+ }
+
+ /* Ignore files whose partition type does not match the one of the partitions we'd write them to */
+ if (object->gpt_type_uuid_set && pc->match_partition_type &&
+ !sd_id128_equal(object->gpt_type_uuid, pc->match_partition_type->uuid)) {
+ log_debug("Skipping file '%s' as its partition type does not match the target's.", object->name);
+ return 0;
+ }
+
+ r = uapi16_object_check_validity(object);
+ if (r < 0) {
+ if (verify_freshness()) {
+ log_warning_errno(r, "File '%s' marked invalid, skipping: %m", object->name);
+ return 0;
+ }
+
+ log_warning_errno(r, "File '%s' marked invalid, but freshness checks disabled, proceeding.", object->name);
+ }
+
+ if (object->size == UINT64_MAX) {
+ log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "File '%s' lacks a size, skipping.", object->name);
+ return 0;
+ }
+
+ if (!iovec_is_set(&object->sha256)) {
+ log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "File '%s' lacks a SHA256 digest, skipping.", object->name);
+ return 0;
+ }
+
+ /* Refuse "%" for the same reason as in sha256sums_line() */
+ if (strchr(object->name, '%')) {
+ log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "File '%s' contains a '%%' character, skipping.", object->name);
+ return 0;
+ }
+
+ r = uapi16_object_resolve_sources(rr, object);
+ if (r <= 0)
+ return r;
+
+ _cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL;
+ r = pattern_match_many(rr->patterns, object->name, &extracted_fields);
+ if (r < 0)
+ return log_error_errno(r, "Failed to match pattern: %m");
+ if (!IN_SET(r, PATTERN_MATCH_YES, PATTERN_MATCH_YES_AND_RETRY))
+ return 0;
+
+ /* Skip instances restricted to other systems via extended attributes */
+ r = xattr_match_filter_test_uapi16_object(pc->filter, object);
+ if (r < 0)
+ return log_error_errno(r, "Failed to check extended attributes of '%s' listed in manifest: %m", object->name);
+ if (r == 0)
+ return 0;
+
+ _cleanup_free_ char *path = NULL;
+ r = import_url_append_component(rr->path, object->name, &path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to build instance URL: %m");
+
+ Instance *instance;
+ r = resource_add_instance(rr, path, &extracted_fields, &instance);
+ if (r < 0)
+ return r;
+
+ /* Now copy the fields from the manifest into our instances logic */
+ r = instance_copy_uapi16(instance, object);
+ if (r < 0)
+ return r;
+
+ /* And take possession of the list of sources, we'll need it when acquiring the file */
+ instance->sources = TAKE_PTR(object->contents);
+ instance->n_sources = object->n_contents;
+ object->n_contents = 0;
+
+ /* Similar, for the extended attributes to apply when installing */
+ instance->user_xattrs = TAKE_PTR(object->user_xattrs);
+ instance->n_user_xattrs = object->n_user_xattrs;
+ object->n_user_xattrs = 0;
+
+ return 0;
+}
+
+static int uapi16_process(
+ Resource *rr,
+ const struct iovec *manifest,
+ X509 *const *certificates,
+ size_t n_certificates,
+ const XAttrMatchFilter *filter,
+ const GptPartitionType *match_partition_type) {
+
+ assert(rr);
+ assert(manifest);
+
+ if (!iovec_is_set(manifest))
+ return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Manifest is empty, refusing.");
+
+ _cleanup_fclose_ FILE *f = fmemopen_unlocked(manifest->iov_base, manifest->iov_len, "r");
+ if (!f)
+ return log_error_errno(errno, "Failed to open manifest buffer as stream: %m");
+
+ return uapi16_manifest_read(
+ f,
+ certificates,
+ n_certificates,
+ uapi16_process_object,
+ &(Uapi16ProcessContext) {
+ .resource = rr,
+ .filter = filter,
+ .match_partition_type = match_partition_type,
+ });
+}
+
static int resource_load_from_web(
Resource *rr,
bool verify,
- Hashmap **web_cache) {
+ X509 *const *certificates,
+ size_t n_certificates,
+ Hashmap **web_cache,
+ const XAttrMatchFilter *filter,
+ const GptPartitionType *match_partition_type) {
_cleanup_(iovec_done) struct iovec buf = {};
struct iovec manifest = {};
@@ -639,7 +986,7 @@ static int resource_load_from_web(
POINTER_MAY_BE_NULL(web_cache);
/* Key the cache by the URL of the manifest itself, not the one of the resource, as the former
- * depends on ManifestPath= too. */
+ * depends on ManifestFormat= and ManifestPath= too. */
_cleanup_free_ char *manifest_url = NULL;
r = resource_manifest_url(rr, &manifest_url);
if (r < 0)
@@ -660,7 +1007,27 @@ static int resource_load_from_web(
manifest = buf;
}
- r = sha256sums_process(rr, &manifest);
+ switch (rr->manifest_format) {
+
+ case MANIFEST_FORMAT_SHA256SUMS:
+ r = sha256sums_process(rr, &manifest);
+ break;
+
+ case MANIFEST_FORMAT_UAPI16:
+ /* UAPI.16 manifests carry their signatures inline, hence systemd-pull didn't verify
+ * anything, and we do it here. */
+ r = uapi16_process(
+ rr,
+ &manifest,
+ verify ? certificates : NULL,
+ verify ? n_certificates : 0,
+ filter,
+ match_partition_type);
+ break;
+
+ default:
+ assert_not_reached();
+ }
if (r < 0)
return r;
@@ -696,21 +1063,32 @@ static int instance_cmp(Instance *const*a, Instance *const*b) {
return path_compare((*a)->path, (*b)->path);
}
-int resource_load_instances(Resource *rr, bool verify, Hashmap **web_cache) {
+int resource_load_instances(
+ Resource *rr,
+ bool verify,
+ X509 *const *certificates,
+ size_t n_certificates,
+ Hashmap **web_cache,
+ const XAttrMatchFilter *filter,
+ const GptPartitionType *match_partition_type) {
int r;
assert(rr);
+ /* If match_partition_type is specified, files listed in UAPI.16 manifests that declare a different
+ * partition type are skipped. This is used to filter the sources by the partition type of the
+ * target the files would be written to. */
+
switch (rr->type) {
case RESOURCE_TAR:
case RESOURCE_REGULAR_FILE:
- r = resource_load_from_directory(rr, S_IFREG);
+ r = resource_load_from_directory(rr, S_IFREG, filter);
break;
case RESOURCE_DIRECTORY:
case RESOURCE_SUBVOLUME:
- r = resource_load_from_directory(rr, S_IFDIR);
+ r = resource_load_from_directory(rr, S_IFDIR, filter);
break;
case RESOURCE_PARTITION:
@@ -719,7 +1097,7 @@ int resource_load_instances(Resource *rr, bool verify, Hashmap **web_cache) {
case RESOURCE_URL_FILE:
case RESOURCE_URL_TAR:
- r = resource_load_from_web(rr, verify, web_cache);
+ r = resource_load_from_web(rr, verify, certificates, n_certificates, web_cache, filter, match_partition_type);
break;
default:
diff --git a/src/sysupdate/sysupdate-resource.h b/src/sysupdate/sysupdate-resource.h
index 2465ad92c4f..0a9119d43aa 100644
--- a/src/sysupdate/sysupdate-resource.h
+++ b/src/sysupdate/sysupdate-resource.h
@@ -4,6 +4,7 @@
#include "gpt.h"
#include "manifest-util.h"
#include "sysupdate-forward.h"
+#include "xattr-match.h"
typedef enum ResourceType {
RESOURCE_URL_FILE,
@@ -79,8 +80,9 @@ typedef struct Resource {
char **patterns;
GptPartitionType partition_type;
bool partition_type_set;
- ManifestFormat manifest_format; /* Only MANIFEST_FORMAT_SHA256SUMS for now */
+ ManifestFormat manifest_format; /* MANIFEST_FORMAT_SHA256SUMS or MANIFEST_FORMAT_UAPI16 */
char *manifest_path;
+ bool match_xattr; /* MatchXAttr= */
/* All instances of this resource we found */
Instance **instances;
@@ -94,7 +96,16 @@ void resource_destroy(Resource *rr);
bool resource_has_glob_directory_pattern(Resource *rr);
-int resource_load_instances(Resource *rr, bool verify, Hashmap **web_cache);
+unsigned resource_xattr_match_mask(const Resource *rr);
+
+int resource_load_instances(
+ Resource *rr,
+ bool verify,
+ X509 *const *certificates,
+ size_t n_certificates,
+ Hashmap **web_cache,
+ const XAttrMatchFilter *filter,
+ const GptPartitionType *match_partition_type);
Instance* resource_find_instance(Resource *rr, const char *version);
diff --git a/src/sysupdate/sysupdate-transfer.c b/src/sysupdate/sysupdate-transfer.c
index 8d098666751..bc895a5320f 100644
--- a/src/sysupdate/sysupdate-transfer.c
+++ b/src/sysupdate/sysupdate-transfer.c
@@ -11,6 +11,7 @@
#include "build-path.h"
#include "chase.h"
#include "conf-parser.h"
+#include "data-fd-util.h"
#include "dirent-util.h"
#include "errno-util.h"
#include "event-util.h"
@@ -23,6 +24,8 @@
#include "hexdecoct.h"
#include "install-file.h"
#include "json-util.h"
+#include "iovec-util.h"
+#include "memory-util.h"
#include "mkdir.h"
#include "notify-recv.h"
#include "parse-helpers.h"
@@ -32,6 +35,7 @@
#include "pidref.h"
#include "process-util.h"
#include "rm-rf.h"
+#include "sha256.h"
#include "signal-util.h"
#include "specifier.h"
#include "stdio-util.h"
@@ -47,6 +51,7 @@
#include "sysupdate-transfer.h"
#include "time-util.h"
#include "web-util.h"
+#include "xattr-util.h"
/* Default value for InstancesMax= for fs object targets */
#define DEFAULT_FILE_INSTANCES_MAX 3
@@ -120,6 +125,9 @@ Transfer* transfer_new(Context *ctx) {
.context = ctx,
};
+ /* Available instances are matched against the system by default, installed ones are not */
+ t->source.match_xattr = true;
+
return t;
}
@@ -611,6 +619,11 @@ static int transfer_finalize(Transfer *t, const char *origin, unsigned line, Has
"Target type '%s' is incompatible with source type '%s', refusing.",
resource_type_to_string(t->target.type), resource_type_to_string(t->source.type));
+ /* Partitions carry no extended attributes */
+ if (t->target.type == RESOURCE_PARTITION && resource_xattr_match_mask(&t->target) != 0)
+ return log_syntax(NULL, LOG_ERR, origin, line, SYNTHETIC_ERRNO(EINVAL),
+ "MatchXAttr= in the [Target] section is not supported for partition targets, refusing.");
+
if (!t->source.path && !t->source.path_auto)
return log_syntax(NULL, LOG_ERR, origin, line, SYNTHETIC_ERRNO(EINVAL),
"Source specification lacks Path=.");
@@ -638,7 +651,7 @@ static int transfer_finalize(Transfer *t, const char *origin, unsigned line, Has
"Source path is not a valid HTTP, HTTPS, file, or provider URL: %s", t->source.path);
}
- if (t->source.manifest_format != MANIFEST_FORMAT_SHA256SUMS)
+ if (!IN_SET(t->source.manifest_format, MANIFEST_FORMAT_SHA256SUMS, MANIFEST_FORMAT_UAPI16))
return log_syntax(NULL, LOG_ERR, origin, line, SYNTHETIC_ERRNO(EINVAL),
"Manifest format '%s' not supported for source specifications, refusing.",
manifest_format_to_string(t->source.manifest_format));
@@ -721,11 +734,13 @@ int transfer_read_definition(Transfer *t, const char *path, const char **dirs, H
{ "Source", "MatchPattern", config_parse_resource_pattern, MATCH_PATTERN_SOURCE, &t->source.patterns },
{ "Source", "ManifestFormat", config_parse_resource_manifest_format, 0, &t->source.manifest_format },
{ "Source", "ManifestPath", config_parse_resource_manifest_path, 0, &t->source.manifest_path },
+ { "Source", "MatchXAttr", config_parse_bool, 0, &t->source.match_xattr },
{ "Target", "Type", config_parse_resource_type, 0, &t->target.type },
{ "Target", "Path", config_parse_resource_path, 0, &t->target },
{ "Target", "PathRelativeTo", config_parse_resource_path_relto, 0, &t->target.path_relative_to },
{ "Target", "MatchPattern", config_parse_resource_pattern, MATCH_PATTERN_TARGET, &t->target.patterns },
{ "Target", "MatchPartitionType", config_parse_resource_ptype, 0, &t->target },
+ { "Target", "MatchXAttr", config_parse_bool, 0, &t->target.match_xattr },
{ "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 },
@@ -930,16 +945,29 @@ static int dispatch_current_symlink(const char *name, sd_json_variant *variant,
return free_and_replace(*current_symlink, p);
}
+static int dispatch_source_match(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
+ bool *b = ASSERT_PTR(userdata);
+
+ /* MatchXAttr= defaults to on in the [Source] section, hence map null to true */
+ if (sd_json_variant_is_null(variant)) {
+ *b = true;
+ return 0;
+ }
+
+ return sd_json_dispatch_stdbool(name, variant, flags, userdata);
+}
+
static int dispatch_transfer_source(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
Transfer *t = ASSERT_PTR(userdata);
static const sd_json_dispatch_field dispatch_table[] = {
- { "type", SD_JSON_VARIANT_STRING, dispatch_resource_type, offsetof(Transfer, source.type), SD_JSON_MANDATORY },
- { "path", SD_JSON_VARIANT_STRING, dispatch_resource_path, offsetof(Transfer, source), SD_JSON_MANDATORY },
- { "pathRelativeTo", SD_JSON_VARIANT_STRING, dispatch_path_relative_to, offsetof(Transfer, source.path_relative_to), SD_JSON_NULLABLE },
- { "matchPattern", SD_JSON_VARIANT_ARRAY, dispatch_source_patterns, offsetof(Transfer, source), SD_JSON_MANDATORY },
- { "manifestFormat", SD_JSON_VARIANT_STRING, dispatch_manifest_format, offsetof(Transfer, source.manifest_format), SD_JSON_NULLABLE },
- { "manifestPath", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(Transfer, source.manifest_path), SD_JSON_NULLABLE },
+ { "type", SD_JSON_VARIANT_STRING, dispatch_resource_type, offsetof(Transfer, source.type), SD_JSON_MANDATORY },
+ { "path", SD_JSON_VARIANT_STRING, dispatch_resource_path, offsetof(Transfer, source), SD_JSON_MANDATORY },
+ { "pathRelativeTo", SD_JSON_VARIANT_STRING, dispatch_path_relative_to, offsetof(Transfer, source.path_relative_to), SD_JSON_NULLABLE },
+ { "matchPattern", SD_JSON_VARIANT_ARRAY, dispatch_source_patterns, offsetof(Transfer, source), SD_JSON_MANDATORY },
+ { "manifestFormat", SD_JSON_VARIANT_STRING, dispatch_manifest_format, offsetof(Transfer, source.manifest_format), SD_JSON_NULLABLE },
+ { "manifestPath", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(Transfer, source.manifest_path), SD_JSON_NULLABLE },
+ { "matchXAttr", SD_JSON_VARIANT_BOOLEAN, dispatch_source_match, offsetof(Transfer, source.match_xattr), SD_JSON_NULLABLE },
{},
};
@@ -969,6 +997,7 @@ static int dispatch_transfer_target(const char *name, sd_json_variant *variant,
{ "instancesMax", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_instances_max, offsetof(Transfer, instances_max), 0 },
{ "removeTemporary", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(Transfer, remove_temporary), SD_JSON_NULLABLE },
{ "currentSymlink", SD_JSON_VARIANT_STRING, dispatch_current_symlink, offsetof(Transfer, current_symlink), SD_JSON_NULLABLE },
+ { "matchXAttr", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(Transfer, target.match_xattr), SD_JSON_NULLABLE },
{},
};
@@ -989,17 +1018,17 @@ int transfer_from_json(Transfer *t, sd_json_variant *v, const char *origin, Hash
* from, for logging purposes. */
static const sd_json_dispatch_field dispatch_table[] = {
- { "id", SD_JSON_VARIANT_STRING, json_dispatch_filename, offsetof(Transfer, id), SD_JSON_MANDATORY|SD_JSON_STRICT },
- { "minVersion", SD_JSON_VARIANT_STRING, json_dispatch_version, offsetof(Transfer, min_version), SD_JSON_NULLABLE },
- { "maxVersion", SD_JSON_VARIANT_STRING, json_dispatch_version, offsetof(Transfer, max_version), SD_JSON_NULLABLE },
- { "protectVersion", SD_JSON_VARIANT_ARRAY, json_dispatch_versions, offsetof(Transfer, protected_versions), SD_JSON_NULLABLE },
- { "verify", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(Transfer, verify), SD_JSON_NULLABLE },
- { "changeLog", SD_JSON_VARIANT_ARRAY, json_dispatch_http_urls, offsetof(Transfer, changelog), SD_JSON_NULLABLE },
- { "appStream", SD_JSON_VARIANT_ARRAY, json_dispatch_http_urls, offsetof(Transfer, appstream), SD_JSON_NULLABLE },
- { "features", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(Transfer, features), SD_JSON_NULLABLE },
- { "requisiteFeatures", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(Transfer, requisite_features), SD_JSON_NULLABLE },
- { "source", SD_JSON_VARIANT_OBJECT, dispatch_transfer_source, 0, SD_JSON_MANDATORY },
- { "target", SD_JSON_VARIANT_OBJECT, dispatch_transfer_target, 0, SD_JSON_MANDATORY },
+ { "id", SD_JSON_VARIANT_STRING, json_dispatch_filename, offsetof(Transfer, id), SD_JSON_MANDATORY|SD_JSON_STRICT },
+ { "minVersion", SD_JSON_VARIANT_STRING, json_dispatch_version, offsetof(Transfer, min_version), SD_JSON_NULLABLE },
+ { "maxVersion", SD_JSON_VARIANT_STRING, json_dispatch_version, offsetof(Transfer, max_version), SD_JSON_NULLABLE },
+ { "protectVersion", SD_JSON_VARIANT_ARRAY, json_dispatch_versions, offsetof(Transfer, protected_versions), SD_JSON_NULLABLE },
+ { "verify", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(Transfer, verify), SD_JSON_NULLABLE },
+ { "changeLog", SD_JSON_VARIANT_ARRAY, json_dispatch_http_urls, offsetof(Transfer, changelog), SD_JSON_NULLABLE },
+ { "appStream", SD_JSON_VARIANT_ARRAY, json_dispatch_http_urls, offsetof(Transfer, appstream), SD_JSON_NULLABLE },
+ { "features", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(Transfer, features), SD_JSON_NULLABLE },
+ { "requisiteFeatures", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(Transfer, requisite_features), SD_JSON_NULLABLE },
+ { "source", SD_JSON_VARIANT_OBJECT, dispatch_transfer_source, 0, SD_JSON_MANDATORY },
+ { "target", SD_JSON_VARIANT_OBJECT, dispatch_transfer_target, 0, SD_JSON_MANDATORY },
{},
};
@@ -1468,6 +1497,7 @@ static int helper_on_notify(sd_event_source *s, int fd, uint32_t revents, void *
static int run_callout(
const char *name,
char *cmdline[],
+ int stdin_fd,
const Transfer *transfer,
const Instance *instance,
TransferProgress callback,
@@ -1507,7 +1537,13 @@ static int run_callout(
if (r < 0)
return log_error_errno(r, "Failed to prepare notify socket: %m");
- r = pidref_safe_fork(ctx->name, FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_LOG, &ctx->pid);
+ /* Optionally connect the helper's stdin to the specified fd, otherwise it inherits ours */
+ r = pidref_safe_fork_full(
+ ctx->name,
+ stdin_fd >= 0 ? (int[]) { stdin_fd, STDOUT_FILENO, STDERR_FILENO } : NULL,
+ /* except_fds= */ NULL, /* n_except_fds= */ 0,
+ FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_LOG|(stdin_fd >= 0 ? FORK_REARRANGE_STDIO : 0),
+ &ctx->pid);
if (r < 0)
return log_error_errno(r, "Failed to fork process %s: %m", ctx->name);
if (r == 0) {
@@ -1633,8 +1669,247 @@ static bool transfer_zero_fill(const Transfer *t) {
return partition_designator_is_verity_sig(t->target.partition_type.designator);
}
-int transfer_acquire_instance(Transfer *t, Instance *i, InstanceMetadata *f, TransferProgress cb, void *userdata) {
+static int transfer_acquire_source(
+ Transfer *t,
+ Instance *i,
+ const Uapi16Contents *s,
+ const char *compression,
+ TransferProgress cb,
+ void *userdata) {
+
+ _cleanup_strv_free_ char **cmdline = NULL;
_cleanup_free_ char *digest = NULL;
+ _cleanup_close_ int literal_fd = -EBADF;
+ const char *from, *to;
+ int r;
+
+ assert(t);
+ assert(i);
+ assert(s);
+ assert(compression);
+ assert(RESOURCE_IS_URL(i->resource->type));
+
+ /* Acquires a URL backed resource instance from one of its sources: either downloads it from the URL,
+ * or takes the literal data the manifest carries. We run everything through systemd-pull or
+ * systemd-import, so that decompression, sparse file handling and sandboxing are the same regardless
+ * of where the data comes from. The compression algorithm is passed down explicitly, so that the
+ * tools strictly decode with what the manifest declares, instead of guessing from the data. */
+
+ bool tar = i->resource->type == RESOURCE_URL_TAR;
+
+ /* The file may be only a part of the decoded source, i.e. the range [offset, offset+size) of it. If
+ * the size of the source is not known it is assumed to consist of the file only. */
+ bool range = s->offset != 0 ||
+ (s->original_size != UINT64_MAX && i->metadata.size != UINT64_MAX && s->original_size != i->metadata.size);
+
+ if (s->url) {
+ from = s->url;
+ r = strv_extend_many(
+ &cmdline,
+ SYSTEMD_PULL_PATH,
+ tar ? "tar" : "raw",
+ "--direct"); /* just download the specified URL, don't download anything else */
+ if (r < 0)
+ return log_oom();
+
+ /* If the file is cut out of a source that is not encoded, verifying the file's own digest
+ * (see below) is sufficient, and allows systemd-pull to acquire only the range of the source
+ * that makes up the file, via an HTTP range request, instead of the whole source. */
+ if (iovec_is_set(&s->encoded_sha256) && !(range && !s->encoding && i->metadata.sha256sum_set)) {
+ digest = hexmem(s->encoded_sha256.iov_base, s->encoded_sha256.iov_len);
+ if (!digest)
+ return log_oom();
+
+ r = strv_extend_many(&cmdline, "--verify", digest); /* validate by explicit SHA256 sum */
+ } else {
+ /* Without the digest of the download as a whole, we require the file's digest to be
+ * known, so that we can validate the decoded file instead, see below. */
+ if (!i->metadata.sha256sum_set)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "SHA256 checksum not known for download '%s', refusing.", s->url);
+
+ r = strv_extend_many(&cmdline, "--verify", "no");
+ }
+ if (r < 0)
+ return log_oom();
+
+ /* systemd-pull supports ranges for raw images only. Other sources have been dropped when
+ * the manifest was processed already. */
+ assert(!range || !tar);
+
+ if (s->offset != 0) {
+ r = strv_extendf(&cmdline, "--source-offset=%" PRIu64, s->offset);
+ if (r < 0)
+ return log_oom();
+ }
+
+ if (range && i->metadata.size != UINT64_MAX) {
+ r = strv_extendf(&cmdline, "--source-size=%" PRIu64, i->metadata.size);
+ if (r < 0)
+ return log_oom();
+ }
+ } else {
+ struct iovec data = s->literal;
+
+ /* All other sources have been turned into URLs when the manifest was processed. Note that
+ * the inline data may be empty, for empty files. */
+ assert(uapi16_contents_has_literal(s));
+
+ /* Literal data: check it against its digest right here, cut out the part that makes up the
+ * file, and pass it to systemd-import via a memory file descriptor as standard input. */
+
+ if (iovec_is_set(&s->encoded_sha256) &&
+ memcmp_nn(SHA256_DIRECT(data.iov_base, data.iov_len), SHA256_DIGEST_SIZE,
+ s->encoded_sha256.iov_base, s->encoded_sha256.iov_len) != 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Inline data of '%s' does not match its SHA256 digest, refusing.", i->path);
+
+ /* Encoded data we cannot cut into, and leave decoding to the importer. Encoded sources that
+ * carry more than the file have been dropped when the manifest was processed already. */
+ assert(!range || !s->encoding);
+
+ if (!s->encoding) {
+ if (s->offset > data.iov_len)
+ return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Inline data of '%s' is shorter than the offset of the file in it, refusing.", i->path);
+
+ data = IOVEC_MAKE((uint8_t*) data.iov_base + s->offset, data.iov_len - s->offset);
+
+ if (i->metadata.size != UINT64_MAX) {
+ if (data.iov_len < i->metadata.size)
+ return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Inline data of '%s' is shorter than the file, refusing.", i->path);
+
+ data.iov_len = i->metadata.size;
+ }
+
+ if (i->metadata.sha256sum_set &&
+ memcmp_nn(SHA256_DIRECT(data.iov_base, data.iov_len), SHA256_DIGEST_SIZE,
+ i->metadata.sha256sum, sizeof(i->metadata.sha256sum)) != 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Inline data of '%s' does not match the file's SHA256 digest, refusing.", i->path);
+ }
+
+ /* Place the inline data in a sealed memory file, which we hand to systemd-import as stdin */
+ literal_fd = data_fd_from_iovec("sysupdate-literal", &data);
+ if (literal_fd < 0)
+ return log_error_errno(literal_fd, "Failed to allocate memory file for inline data: %m");
+
+ from = "-"; /* i.e. read from stdin */
+ r = strv_extend_many(
+ &cmdline,
+ SYSTEMD_IMPORT_PATH,
+ tar ? "tar" : "raw",
+ "--direct"); /* just copy/unpack the specified file, don't do anything else */
+ }
+ if (r < 0)
+ return log_oom();
+
+ r = strv_extendf(&cmdline, "--compression=%s", compression);
+ if (r < 0)
+ return log_oom();
+
+ if (i->metadata.sha256sum_set && (s->encoding || (s->url && (range || !iovec_is_set(&s->encoded_sha256))))) {
+ /* The digests checked so far cover the encoded data or the whole source only (if any), hence
+ * let the tool also check the decoded data cut down to the file against the file's digest */
+ _cleanup_free_ char *decoded_digest = hexmem(i->metadata.sha256sum, sizeof(i->metadata.sha256sum));
+ if (!decoded_digest)
+ return log_oom();
+
+ r = strv_extendf(&cmdline, "--verify-decoded=%s", decoded_digest);
+ if (r < 0)
+ return log_oom();
+ }
+
+ r = strv_extend(&cmdline, t->context->sync ? "--sync=yes" : "--sync=no");
+ if (r < 0)
+ return log_oom();
+
+ if (tar) {
+ assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME));
+
+ r = strv_extend(&cmdline, t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no");
+ if (r < 0)
+ return log_oom();
+
+ to = t->temporary_partial_path;
+
+ } else if (t->target.type == RESOURCE_PARTITION) {
+ r = strv_extendf(&cmdline, "--offset=%" PRIu64, t->partition_info.start);
+ if (r < 0)
+ return log_oom();
+
+ r = strv_extendf(&cmdline, "--size-max=%" PRIu64, t->partition_info.size);
+ if (r < 0)
+ return log_oom();
+
+ r = strv_extend(&cmdline, transfer_zero_fill(t) ? "--zero-fill=yes" : "--zero-fill=no");
+ if (r < 0)
+ return log_oom();
+
+ to = t->target.path;
+
+ } else {
+ assert(t->target.type == RESOURCE_REGULAR_FILE);
+ to = t->temporary_partial_path;
+ }
+
+ r = strv_extend_many(&cmdline, from, to);
+ if (r < 0)
+ return log_oom();
+
+ return run_callout(
+ s->url ? (tar ? "(sd-pull-tar)" : "(sd-pull-raw)") : (tar ? "(sd-import-tar)" : "(sd-import-raw)"),
+ cmdline,
+ literal_fd,
+ t, i, cb, userdata);
+}
+
+static int instance_apply_user_xattrs(Instance *i, const char *path) {
+ int r;
+
+ assert(i);
+ assert(path);
+
+ /* Applies the user extended attributes listed in the manifest to the installed inode, so that they
+ * are retained on the target. This in particular matters for the ones MatchXAttr= looks at. Failing
+ * to do so is not fatal, the instance is fully usable without them. Returns > 0 if any attribute
+ * was applied. */
+
+ if (i->n_user_xattrs == 0)
+ return 0;
+
+ _cleanup_close_ int fd = xopenat(AT_FDCWD, path, O_PATH|O_NOFOLLOW);
+ if (fd < 0) {
+ log_warning_errno(fd, "Failed to open '%s' to apply extended attributes, ignoring: %m", path);
+ return 0;
+ }
+
+ bool applied = false;
+ FOREACH_ARRAY(x, i->user_xattrs, i->n_user_xattrs) {
+ _cleanup_free_ char *n = strjoin("user.", x->name);
+ if (!n)
+ return log_oom();
+
+ r = xsetxattr_full(
+ fd,
+ /* path= */ NULL,
+ AT_EMPTY_PATH,
+ n,
+ x->value.iov_base,
+ x->value.iov_len,
+ /* xattr_flags= */ 0);
+ if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) {
+ log_notice_errno(r, "File system backing '%s' does not support extended attributes, not applying them: %m", path);
+ break;
+ }
+ if (r < 0) {
+ log_warning_errno(r, "Failed to set extended attribute '%s' on '%s', ignoring: %m", n, path);
+ continue;
+ }
+
+ applied = true;
+ }
+
+ return applied;
+}
+
+int transfer_acquire_instance(Transfer *t, Instance *i, InstanceMetadata *f, TransferProgress cb, void *userdata) {
char offset[DECIMAL_STR_MAX(uint64_t)+1], max_size[DECIMAL_STR_MAX(uint64_t)+1];
const char *where = NULL;
Instance *existing;
@@ -1709,18 +1984,6 @@ int transfer_acquire_instance(Transfer *t, Instance *i, InstanceMetadata *f, Tra
log_info("%s Acquiring %s %s %s...", glyph(GLYPH_DOWNLOAD), i->path, glyph(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(SYNTHETIC_ERRNO(EINVAL), "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:
@@ -1742,7 +2005,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, InstanceMetadata *f, Tra
t->context->sync ? "--sync=yes" : "--sync=no",
i->path,
t->temporary_partial_path),
- t, i, cb, userdata);
+ /* stdin_fd= */ -EBADF, t, i, cb, userdata);
break;
case RESOURCE_PARTITION:
@@ -1760,7 +2023,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, InstanceMetadata *f, Tra
t->context->sync ? "--sync=yes" : "--sync=no",
i->path,
t->target.path),
- t, i, cb, userdata);
+ /* stdin_fd= */ -EBADF, t, i, cb, userdata);
break;
default:
@@ -1784,7 +2047,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, InstanceMetadata *f, Tra
t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no",
i->path,
t->temporary_partial_path),
- t, i, cb, userdata);
+ /* stdin_fd= */ -EBADF, t, i, cb, userdata);
break;
case RESOURCE_TAR:
@@ -1801,68 +2064,54 @@ int transfer_acquire_instance(Transfer *t, Instance *i, InstanceMetadata *f, Tra
t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no",
i->path,
t->temporary_partial_path),
- t, i, cb, userdata);
+ /* stdin_fd= */ -EBADF, t, i, cb, userdata);
break;
case RESOURCE_URL_FILE:
-
- switch (t->target.type) {
-
- case RESOURCE_REGULAR_FILE:
-
- /* url file → regular file */
-
- r = run_callout("(sd-pull-raw)",
- STRV_MAKE(
- SYSTEMD_PULL_PATH,
- "raw",
- "--direct", /* just download the specified URL, don't download anything else */
- "--verify", digest, /* validate by explicit SHA256 sum */
- t->context->sync ? "--sync=yes" : "--sync=no",
- i->path,
- t->temporary_partial_path),
- t, i, cb, userdata);
- break;
-
- case RESOURCE_PARTITION:
-
- /* url file → partition */
-
- r = run_callout("(sd-pull-raw)",
- STRV_MAKE(
- SYSTEMD_PULL_PATH,
- "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,
- transfer_zero_fill(t) ? "--zero-fill=yes" : "--zero-fill=no",
- t->context->sync ? "--sync=yes" : "--sync=no",
- i->path,
- t->target.path),
- t, i, cb, userdata);
- break;
-
- default:
- assert_not_reached();
- }
-
- break;
-
case RESOURCE_URL_TAR:
- assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME));
- r = run_callout("(sd-pull-tar)",
- STRV_MAKE(
- SYSTEMD_PULL_PATH,
- "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",
- t->context->sync ? "--sync=yes" : "--sync=no",
- i->path,
- t->temporary_partial_path),
- t, i, cb, userdata);
+ if (i->n_sources == 0) {
+ /* The manifest lists no sources for the file, hence acquire it by its name relative
+ * to the source URL. */
+ Uapi16Contents fallback = UAPI16_CONTENTS_NULL;
+
+ fallback.url = i->path;
+ fallback.offset = 0;
+ if (i->metadata.sha256sum_set)
+ fallback.encoded_sha256 = IOVEC_MAKE(i->metadata.sha256sum, sizeof(i->metadata.sha256sum));
+
+ /* For SHA256SUMS manifests we know nothing about the compression, hence let the tool
+ * figure it out, and decompress implicitly. UAPI.16 manifests declare encodings
+ * explicitly, via "contents", hence a file without it is to be taken as is: its
+ * digest is the one of the file as stored. */
+ r = transfer_acquire_source(
+ t, i, &fallback,
+ i->resource->manifest_format == MANIFEST_FORMAT_UAPI16 ? "none" : "auto",
+ cb, userdata);
+ } else
+ /* The manifest lists one or more alternative sources for the file, try them in order
+ * until one works out. The manifest declares the encoding of each source, hence
+ * insist on it (or on none) and don't let the tools guess. */
+ FOREACH_ARRAY(s, i->sources, i->n_sources) {
+ r = transfer_acquire_source(t, i, s, s->encoding ?: "none", cb, userdata);
+ if (r >= 0 || r == -ECANCELED)
+ break;
+
+ if (s + 1 >= i->sources + i->n_sources)
+ break;
+
+ log_warning_errno(r, "Failed to acquire '%s', trying alternative source: %m", i->path);
+
+ /* In --direct mode the tools leave whatever they wrote in place when they
+ * fail, hence remove the partial, unverified leftovers before trying the next
+ * source, so that they are not combined with what that one provides. */
+ if (RESOURCE_IS_FILESYSTEM(t->target.type)) {
+ int k = rm_rf(t->temporary_partial_path, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_MISSING_OK|REMOVE_CHMOD);
+ if (k < 0)
+ return log_error_errno(k, "Failed to remove partially acquired '%s': %m", t->temporary_partial_path);
+ }
+ }
+
break;
default:
@@ -1899,6 +2148,12 @@ int transfer_acquire_instance(Transfer *t, Instance *i, InstanceMetadata *f, Tra
need_sync = true;
}
+ r = instance_apply_user_xattrs(i, t->temporary_partial_path);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ need_sync = true;
+
/* Synchronize */
if (t->context->sync && need_sync) {
if (t->target.type == RESOURCE_REGULAR_FILE)
diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c
index dc0844aece2..2d716dcf439 100644
--- a/src/sysupdate/sysupdate.c
+++ b/src/sysupdate/sysupdate.c
@@ -1,5 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#include
#include
#include "sd-bus.h"
@@ -19,6 +20,7 @@
#include "errno-util.h"
#include "fd-util.h"
#include "format-table.h"
+#include "fs-util.h"
#include "glyph-util.h"
#include "hashmap.h"
#include "hexdecoct.h"
@@ -50,9 +52,11 @@
#include "sysupdate-transfer.h"
#include "sysupdate-update-set.h"
#include "sysupdate-util.h"
+#include "uapi16-manifest.h"
#include "varlink-io.systemd.SysUpdate.h"
#include "varlink-util.h"
#include "verbs.h"
+#include "xattr-match.h"
static char *arg_definitions = NULL;
static bool arg_sync = true;
@@ -128,6 +132,12 @@ void context_done(Context *c) {
c->web_cache = hashmap_free(c->web_cache);
+ host_properties_done(&c->host_properties);
+
+ uapi16_certificates_free(c->manifest_certificates, c->n_manifest_certificates);
+ c->manifest_certificates = NULL;
+ c->n_manifest_certificates = 0;
+
c->installdb_fd = safe_close(c->installdb_fd);
c->definitions = mfree(c->definitions);
@@ -572,7 +582,15 @@ static int context_load_installed_instances(Context *c) {
r = resource_load_instances(
&t->target,
c->verify >= 0 ? c->verify : t->verify != 0,
- &c->web_cache);
+ /* certificates= */ NULL,
+ /* n_certificates= */ 0,
+ &c->web_cache,
+ &(const XAttrMatchFilter) {
+ .mask = resource_xattr_match_mask(&t->target),
+ .host = &c->host_properties,
+ .root = c->root,
+ },
+ /* match_partition_type= */ NULL);
if (r < 0)
return r;
}
@@ -583,7 +601,15 @@ static int context_load_installed_instances(Context *c) {
r = resource_load_instances(
&t->target,
c->verify >= 0 ? c->verify : t->verify != 0,
- &c->web_cache);
+ /* certificates= */ NULL,
+ /* n_certificates= */ 0,
+ &c->web_cache,
+ &(const XAttrMatchFilter) {
+ .mask = resource_xattr_match_mask(&t->target),
+ .host = &c->host_properties,
+ .root = c->root,
+ },
+ /* match_partition_type= */ NULL);
if (r < 0)
return r;
}
@@ -591,6 +617,35 @@ static int context_load_installed_instances(Context *c) {
return 0;
}
+static int context_load_manifest_certificates(Context *c) {
+ int r;
+
+ assert(c);
+
+ /* Loads the certificates UAPI.16 manifests may be signed with, from the VOA hierarchy */
+
+ if (c->manifest_certificates_loaded)
+ return 0;
+
+ _cleanup_close_ int root_fd = -EBADF;
+ if (c->root) {
+ root_fd = xopenat(AT_FDCWD, c->root, O_DIRECTORY|O_PATH);
+ if (root_fd < 0)
+ return log_error_errno(root_fd, "Failed to open root directory '%s': %m", c->root);
+ }
+
+ r = uapi16_load_voa_certificates(
+ root_fd >= 0 ? root_fd : XAT_FDROOT,
+ SYSUPDATE_VOA_CONTEXT,
+ &c->manifest_certificates,
+ &c->n_manifest_certificates);
+ if (r < 0)
+ return r;
+
+ c->manifest_certificates_loaded = true;
+ return 0;
+}
+
static int context_load_available_instances(Context *c) {
int r;
@@ -600,11 +655,33 @@ static int context_load_available_instances(Context *c) {
FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
Transfer *t = *tr;
+ bool verify = c->verify >= 0 ? c->verify : t->verify != 0;
+
+ /* UAPI.16 manifests carry their signatures inline, which we verify ourselves, against the
+ * certificates from the VOA hierarchy */
+ if (verify && RESOURCE_IS_URL(t->source.type) && t->source.manifest_format == MANIFEST_FORMAT_UAPI16) {
+ r = context_load_manifest_certificates(c);
+ if (r < 0)
+ return r;
+
+ if (c->n_manifest_certificates == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOKEY),
+ "Transfer '%s' requires verification of the UAPI.16 manifest signature, but no certificates found in the VOA hierarchy (role '%s', context '%s'), refusing.",
+ t->id, UAPI16_VOA_ROLE, SYSUPDATE_VOA_CONTEXT);
+ }
r = resource_load_instances(
&t->source,
- c->verify >= 0 ? c->verify : t->verify != 0,
- &c->web_cache);
+ verify,
+ c->manifest_certificates,
+ c->n_manifest_certificates,
+ &c->web_cache,
+ &(const XAttrMatchFilter) {
+ .mask = resource_xattr_match_mask(&t->source),
+ .host = &c->host_properties,
+ .root = c->root,
+ },
+ t->target.partition_type_set ? &t->target.partition_type : NULL);
if (r < 0)
return r;
}
diff --git a/src/sysupdate/sysupdate.h b/src/sysupdate/sysupdate.h
index 8824c38947a..b1cee66533a 100644
--- a/src/sysupdate/sysupdate.h
+++ b/src/sysupdate/sysupdate.h
@@ -1,10 +1,14 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
+#include "host-properties.h"
#include "sysupdate-component.h"
#include "sysupdate-forward.h"
#include "sysupdate-target.h"
+/* The VOA context (see voa-util.h) UAPI.16 manifest certificates are looked up in */
+#define SYSUPDATE_VOA_CONTEXT "sysupdate"
+
typedef enum SelectMode {
SELECT_EXPLICIT, /* Only explicitly specified features/components */
SELECT_ALL, /* All available features/components */
@@ -55,6 +59,12 @@ typedef struct Context {
Hashmap *web_cache; /* Cache for downloaded resources, keyed by URL */
+ HostProperties host_properties; /* Loaded lazily, for MatchXAttr= */
+
+ X509 **manifest_certificates; /* Loaded lazily from the VOA hierarchy, for verifying UAPI.16 manifest signatures */
+ size_t n_manifest_certificates;
+ bool manifest_certificates_loaded;
+
int installdb_fd;
TargetIdentifier target_identifier;
diff --git a/src/test/test-data-fd-util.c b/src/test/test-data-fd-util.c
index 38ed7578949..bb920315695 100644
--- a/src/test/test-data-fd-util.c
+++ b/src/test/test-data-fd-util.c
@@ -11,6 +11,7 @@
#include "fileio.h"
#include "fs-util.h"
#include "io-util.h"
+#include "iovec-util.h"
#include "memfd-util.h"
#include "pidref.h"
#include "process-util.h"
@@ -59,6 +60,10 @@ TEST(copy_data_fd) {
fd2 = ASSERT_OK(copy_data_fd(fd1));
+ /* Small data ends up in a sealed memfd, which we return read-only, like the temporary files */
+ ASSERT_OK_POSITIVE(memfd_get_sealed(fd2));
+ ASSERT_EQ(ASSERT_OK_ERRNO(fcntl(fd2, F_GETFL)) & O_ACCMODE_STRICT, O_RDONLY);
+
safe_close(fd1);
fd1 = ASSERT_OK(memfd_new_and_seal_string("data", "hallo"));
@@ -216,4 +221,49 @@ TEST(open_seekable_data_fd) {
ASSERT_ERROR(open_seekable_data_fd(dir_fd, "missing", UINT64_MAX), ENOENT);
}
+TEST(data_fd_from_iovec) {
+ _cleanup_close_ int fd = -EBADF;
+ _cleanup_free_ uint8_t *large = NULL;
+ struct stat st;
+ char buf[16];
+
+ fd = ASSERT_OK(data_fd_from_iovec("data", &CONST_IOVEC_MAKE_STRING("hallo")));
+ assert_fd_contents(fd, "hallo");
+
+ /* The fd is sealed, read-only and positioned at the beginning, so that it can be passed as stdin */
+ ASSERT_OK_POSITIVE(memfd_get_sealed(fd));
+ ASSERT_EQ(ASSERT_OK_ERRNO(fcntl(fd, F_GETFL)) & O_ACCMODE_STRICT, O_RDONLY);
+ ASSERT_OK_ZERO_ERRNO(lseek(fd, 0, SEEK_CUR));
+ ASSERT_ERROR_ERRNO(write(fd, "x", 1), EBADF);
+ ASSERT_OK_EQ_ERRNO(read(fd, buf, sizeof(buf)), 5);
+ ASSERT_EQ(memcmp(buf, "hallo", 5), 0);
+ ASSERT_OK_ZERO_ERRNO(read(fd, buf, sizeof(buf)));
+ fd = safe_close(fd);
+
+ /* Empty data, with or without a buffer, and no name */
+ fd = ASSERT_OK(data_fd_from_iovec(/* name= */ NULL, &(const struct iovec) {}));
+ assert_fd_contents(fd, "");
+ ASSERT_OK_POSITIVE(memfd_get_sealed(fd));
+ fd = safe_close(fd);
+
+ fd = ASSERT_OK(data_fd_from_iovec("data", &IOVEC_MAKE(buf, 0)));
+ assert_fd_contents(fd, "");
+ fd = safe_close(fd);
+
+ /* Data larger than what copy_data_fd() places in a memfd still ends up in one */
+ size_t n = 1536 * 1024;
+ large = ASSERT_NOT_NULL(malloc(n));
+ for (size_t i = 0; i < n; i++)
+ large[i] = (uint8_t) (i * 7);
+
+ fd = ASSERT_OK(data_fd_from_iovec("large", &IOVEC_MAKE(large, n)));
+ ASSERT_OK_POSITIVE(memfd_get_sealed(fd));
+ ASSERT_OK_ERRNO(fstat(fd, &st));
+ ASSERT_EQ((uint64_t) st.st_size, (uint64_t) n);
+
+ _cleanup_free_ uint8_t *copy = ASSERT_NOT_NULL(malloc(n));
+ ASSERT_OK_EQ_ERRNO(pread(fd, copy, n, 0), (ssize_t) n);
+ ASSERT_EQ(memcmp(large, copy, n), 0);
+}
+
DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/test/units/TEST-72-SYSUPDATE.sh b/test/units/TEST-72-SYSUPDATE.sh
index 3bbab09e12e..2926d07ffbe 100755
--- a/test/units/TEST-72-SYSUPDATE.sh
+++ b/test/units/TEST-72-SYSUPDATE.sh
@@ -1022,9 +1022,9 @@ cmp "$MP/payload/mp-v1.bin" "$MP/target/mp-v1.bin"
# there is none
sed 's|^ManifestPath=.*|&\nManifestPath=|' "$MP/definitions/01-mp.transfer" >"$MP/bad-definitions/01-mp.transfer"
(! "$SYSUPDATE" --definitions="$MP/bad-definitions" --verify=no list)
-# Only SHA256SUMS manifests are supported for now
-sed 's|^ManifestFormat=.*|ManifestFormat=uapi16|' "$MP/definitions/01-mp.transfer" >"$MP/bad-definitions/01-mp.transfer"
-(! "$SYSUPDATE" --definitions="$MP/bad-definitions" --verify=no list) |& grep -F "Manifest format 'uapi16' not supported" >/dev/null
+# Only SHA256SUMS and UAPI.16 manifests are supported
+sed 's|^ManifestFormat=.*|ManifestFormat=mtree|' "$MP/definitions/01-mp.transfer" >"$MP/bad-definitions/01-mp.transfer"
+(! "$SYSUPDATE" --definitions="$MP/bad-definitions" --verify=no list) |& grep -F "Manifest format 'mtree' not supported" >/dev/null
# ManifestPath= must be a URL, and is only supported for URL sources
sed 's|^ManifestPath=.*|ManifestPath=/foo/SHA256SUMS|' "$MP/definitions/01-mp.transfer" >"$MP/bad-definitions/01-mp.transfer"
(! "$SYSUPDATE" --definitions="$MP/bad-definitions" --verify=no list) |& grep -F "Manifest path is not a valid" >/dev/null
@@ -1032,6 +1032,326 @@ sed -e 's|^Type=url-file|Type=regular-file|' -e "s|^Path=file://|Path=|" "$MP/de
(! "$SYSUPDATE" --definitions="$MP/bad-definitions" --verify=no list) |& grep -F "ManifestPath= is only supported for URL sources" >/dev/null
rm -rf "$MP"
+# Check that the sources listed in the "contents" arrays of a UAPI.16 manifest are honoured: "file"
+# entries are fetched relative to Path= (not relative to ManifestPath=), "url" entries from the specified
+# URL, "literal" and "url" entries may carry the file embedded in more data (at "offset"), and alternative
+# sources are tried in order. Entries without "file", "url" and "literal" refer to a file of the object's
+# name relative to Path=, and an empty "literal" makes an empty file. Files without "contents" are
+# acquired by name, as they are, i.e. without decompression.
+rm -rf "$WORKDIR/uapi16"
+mkdir -p "$WORKDIR/uapi16/payload/blobs" "$WORKDIR/uapi16/manifest/blobs" "$WORKDIR/uapi16/remote" "$WORKDIR/uapi16/target"
+head -c 3000 /dev/urandom >"$WORKDIR/uapi16/alt-v1.bin"
+cp "$WORKDIR/uapi16/alt-v1.bin" "$WORKDIR/uapi16/payload/blobs/alt-v1.bin"
+# A decoy next to the manifest, which must not be used, as "file" entries are not relative to ManifestPath=
+head -c 3000 /dev/urandom >"$WORKDIR/uapi16/manifest/blobs/alt-v1.bin"
+head -c 2000 /dev/urandom >"$WORKDIR/uapi16/implied-v1.bin"
+cp "$WORKDIR/uapi16/implied-v1.bin" "$WORKDIR/uapi16/payload/implied-v1.bin"
+: >"$WORKDIR/uapi16/empty-v1.bin"
+head -c 5000 /dev/urandom >"$WORKDIR/uapi16/remote-v1.bin"
+gzip -k -f "$WORKDIR/uapi16/remote-v1.bin"
+mv "$WORKDIR/uapi16/remote-v1.bin.gz" "$WORKDIR/uapi16/remote/"
+printf 'inline payload\n' >"$WORKDIR/uapi16/inline-v1.bin"
+{ printf 'garbage'; cat "$WORKDIR/uapi16/inline-v1.bin"; printf 'trailing garbage'; } >"$WORKDIR/uapi16/inline-v1.literal"
+head -c 4000 /dev/urandom | gzip >"$WORKDIR/uapi16/plain-v1.bin"
+cp "$WORKDIR/uapi16/plain-v1.bin" "$WORKDIR/uapi16/payload/plain-v1.bin"
+# A disk image like source, which carries the file at an offset, followed by more data
+head -c 6000 /dev/urandom >"$WORKDIR/uapi16/slice-v1.bin"
+cp "$WORKDIR/uapi16/slice-v1.bin" "$WORKDIR/uapi16/rawslice-v1.bin"
+{ head -c 1000 /dev/urandom; cat "$WORKDIR/uapi16/slice-v1.bin"; head -c 2000 /dev/urandom; } >"$WORKDIR/uapi16/remote/disk.raw"
+gzip -k -f "$WORKDIR/uapi16/remote/disk.raw"
+sha256_of() { sha256sum "$1" | cut -d' ' -f1; }
+{
+ printf '\036{"mediaType":"application/vnd.uapi.16.manifest"}\n'
+ # The first source does not exist, the second one is found relative to Path=
+ printf '\036{"name":"alt-v1.bin","size":%s,"sha256":"%s","contents":[{"file":"blobs/missing-v1.bin"},{"file":"blobs/alt-v1.bin"}]}\n' \
+ "$(stat -c %s "$WORKDIR/uapi16/alt-v1.bin")" "$(sha256_of "$WORKDIR/uapi16/alt-v1.bin")"
+ # The source refers to the file of the object's name relative to Path= implicitly
+ printf '\036{"name":"implied-v1.bin","size":%s,"sha256":"%s","contents":[{}]}\n' \
+ "$(stat -c %s "$WORKDIR/uapi16/implied-v1.bin")" "$(sha256_of "$WORKDIR/uapi16/implied-v1.bin")"
+ # An empty file carried inline
+ printf '\036{"name":"empty-v1.bin","size":0,"sha256":"%s","contents":[{"literal":""}]}\n' \
+ "$(sha256_of "$WORKDIR/uapi16/empty-v1.bin")"
+ printf '\036{"name":"remote-v1.bin","size":%s,"sha256":"%s","contents":[{"url":"file://%s/uapi16/remote/remote-v1.bin.gz","encoding":"gzip","encodedSize":%s,"encodedSha256":"%s","originalSize":%s,"originalSha256":"%s"}]}\n' \
+ "$(stat -c %s "$WORKDIR/uapi16/remote-v1.bin")" "$(sha256_of "$WORKDIR/uapi16/remote-v1.bin")" "$WORKDIR" \
+ "$(stat -c %s "$WORKDIR/uapi16/remote/remote-v1.bin.gz")" "$(sha256_of "$WORKDIR/uapi16/remote/remote-v1.bin.gz")" \
+ "$(stat -c %s "$WORKDIR/uapi16/remote-v1.bin")" "$(sha256_of "$WORKDIR/uapi16/remote-v1.bin")"
+ # The file sits at an offset inside the inline data
+ printf '\036{"name":"inline-v1.bin","size":%s,"sha256":"%s","contents":[{"literal":"%s","offset":7,"originalSize":%s,"originalSha256":"%s"}]}\n' \
+ "$(stat -c %s "$WORKDIR/uapi16/inline-v1.bin")" "$(sha256_of "$WORKDIR/uapi16/inline-v1.bin")" "$(base64 -w0 "$WORKDIR/uapi16/inline-v1.literal")" \
+ "$(stat -c %s "$WORKDIR/uapi16/inline-v1.literal")" "$(sha256_of "$WORKDIR/uapi16/inline-v1.literal")"
+ # No sources listed: acquired by name relative to Path=, and must not be decompressed even though it is
+ # gzip data, as the digest covers the file as it is
+ printf '\036{"name":"plain-v1.bin","size":%s,"sha256":"%s"}\n' \
+ "$(stat -c %s "$WORKDIR/uapi16/plain-v1.bin")" "$(sha256_of "$WORKDIR/uapi16/plain-v1.bin")"
+ # The file is a range of the decoded data of a source that is verified as a whole
+ printf '\036{"name":"slice-v1.bin","size":%s,"sha256":"%s","contents":[{"url":"file://%s/uapi16/remote/disk.raw.gz","encoding":"gzip","encodedSha256":"%s","originalSize":%s,"offset":1000}]}\n' \
+ "$(stat -c %s "$WORKDIR/uapi16/slice-v1.bin")" "$(sha256_of "$WORKDIR/uapi16/slice-v1.bin")" "$WORKDIR" \
+ "$(sha256_of "$WORKDIR/uapi16/remote/disk.raw.gz")" "$(stat -c %s "$WORKDIR/uapi16/remote/disk.raw")"
+ # The same, but without a digest of the source, hence only verified against the file's digest
+ printf '\036{"name":"rawslice-v1.bin","size":%s,"sha256":"%s","contents":[{"url":"file://%s/uapi16/remote/disk.raw","originalSize":%s,"offset":1000}]}\n' \
+ "$(stat -c %s "$WORKDIR/uapi16/slice-v1.bin")" "$(sha256_of "$WORKDIR/uapi16/slice-v1.bin")" "$WORKDIR" \
+ "$(stat -c %s "$WORKDIR/uapi16/remote/disk.raw")"
+ # The source matches its digest, but the range at the offset is not the file
+ printf '\036{"name":"badslice-v1.bin","size":%s,"sha256":"%s","contents":[{"url":"file://%s/uapi16/remote/disk.raw.gz","encoding":"gzip","encodedSha256":"%s","originalSize":%s,"offset":999}]}\n' \
+ "$(stat -c %s "$WORKDIR/uapi16/slice-v1.bin")" "$(sha256_of "$WORKDIR/uapi16/slice-v1.bin")" "$WORKDIR" \
+ "$(sha256_of "$WORKDIR/uapi16/remote/disk.raw.gz")" "$(stat -c %s "$WORKDIR/uapi16/remote/disk.raw")"
+ # The encoded data matches its digest, but does not decode to the file
+ printf '\036{"name":"bad-v1.bin","size":%s,"sha256":"%s","contents":[{"url":"file://%s/uapi16/remote/remote-v1.bin.gz","encoding":"gzip","encodedSha256":"%s"}]}\n' \
+ "$(stat -c %s "$WORKDIR/uapi16/remote-v1.bin")" "$(sha256_of "$WORKDIR/uapi16/alt-v1.bin")" "$WORKDIR" \
+ "$(sha256_of "$WORKDIR/uapi16/remote/remote-v1.bin.gz")"
+ printf '\036{"mediaType":"application/vnd.uapi.16.trailer"}\n'
+} >"$WORKDIR/uapi16/manifest/Uapi16Manifest"
+for f in alt implied empty remote inline plain slice rawslice; do
+ cat >"$CONFIGDIR/01-uapi16-$f.transfer" <"$WORKDIR/uapi16/bad-definitions/bad.transfer"
+(! "$SYSUPDATE" --definitions="$WORKDIR/uapi16/bad-definitions" --verify=no update)
+[[ ! -e "$WORKDIR/uapi16/target/bad-v1.bin" ]]
+sed -i 's/^MatchPattern=bad-/MatchPattern=badslice-/' "$WORKDIR/uapi16/bad-definitions/bad.transfer"
+(! "$SYSUPDATE" --definitions="$WORKDIR/uapi16/bad-definitions" --verify=no update)
+[[ ! -e "$WORKDIR/uapi16/target/badslice-v1.bin" ]]
+
+# systemd-pull cannot acquire a tarball that is embedded in a larger source, and we cannot cut into encoded
+# inline data, hence such sources are ignored, and the next source listed is used instead
+mkdir -p "$WORKDIR/uapi16/ranged/definitions" "$WORKDIR/uapi16/ranged/tree" "$WORKDIR/uapi16/ranged/dirs" "$WORKDIR/uapi16/ranged/target"
+printf 'tarball payload\n' >"$WORKDIR/uapi16/ranged/tree/file.txt"
+tar -C "$WORKDIR/uapi16/ranged/tree" -cf "$WORKDIR/uapi16/ranged/tree-v1.tar" .
+{ head -c 512 /dev/zero; cat "$WORKDIR/uapi16/ranged/tree-v1.tar"; } >"$WORKDIR/uapi16/ranged/embedded.raw"
+printf 'encoded inline payload\n' >"$WORKDIR/uapi16/ranged/enc-v1.bin"
+{ printf 'garbage'; cat "$WORKDIR/uapi16/ranged/enc-v1.bin"; } | gzip >"$WORKDIR/uapi16/ranged/enc-v1.literal.gz"
+{
+ printf '\036{"mediaType":"application/vnd.uapi.16.manifest"}\n'
+ printf '\036{"name":"tree-v1.tar","size":%s,"sha256":"%s","contents":[{"url":"file://%s/uapi16/ranged/embedded.raw","offset":512},{"url":"file://%s/uapi16/ranged/tree-v1.tar"}]}\n' \
+ "$(stat -c %s "$WORKDIR/uapi16/ranged/tree-v1.tar")" "$(sha256_of "$WORKDIR/uapi16/ranged/tree-v1.tar")" "$WORKDIR" "$WORKDIR"
+ printf '\036{"name":"enc-v1.bin","size":%s,"sha256":"%s","contents":[{"literal":"%s","encoding":"gzip","offset":7},{"literal":"%s"}]}\n' \
+ "$(stat -c %s "$WORKDIR/uapi16/ranged/enc-v1.bin")" "$(sha256_of "$WORKDIR/uapi16/ranged/enc-v1.bin")" \
+ "$(base64 -w0 "$WORKDIR/uapi16/ranged/enc-v1.literal.gz")" "$(base64 -w0 "$WORKDIR/uapi16/ranged/enc-v1.bin")"
+ printf '\036{"mediaType":"application/vnd.uapi.16.trailer"}\n'
+} >"$WORKDIR/uapi16/ranged/Uapi16Manifest"
+cat >"$WORKDIR/uapi16/ranged/definitions/01-tree.transfer" <"$WORKDIR/uapi16/ranged/definitions/02-enc.transfer" <"$WORKDIR/uapi16/ranged/update.log"
+grep -F "File 'tree-v1.tar' has a source that carries more than the file, which is not supported for tarballs, ignoring source." "$WORKDIR/uapi16/ranged/update.log" >/dev/null
+grep -F "File 'enc-v1.bin' has encoded inline data that carries more than the file, ignoring source." "$WORKDIR/uapi16/ranged/update.log" >/dev/null
+cmp "$WORKDIR/uapi16/ranged/tree/file.txt" "$WORKDIR/uapi16/ranged/dirs/tree-v1/file.txt"
+cmp "$WORKDIR/uapi16/ranged/enc-v1.bin" "$WORKDIR/uapi16/ranged/target/enc-v1.bin"
+
+# UAPI.16 manifests carry their signature inline, which is verified against the certificates in the VOA
+# hierarchy (role "manifest", context "sysupdate") if verification is enabled
+if command -v openssl >/dev/null; then
+ VOA_OS="$(. /etc/os-release; echo "${ID:-linux}")"
+ VOA_DIR="/run/voa/$VOA_OS/manifest/sysupdate/x509"
+ S="$WORKDIR/uapi16/signed"
+ mkdir -p "$S/payload" "$S/target" "$S/definitions"
+ head -c 2000 /dev/urandom >"$S/payload/signed-v1.bin"
+ openssl req -x509 -newkey rsa:2048 -nodes -keyout "$S/good.key" -out "$S/good.crt" -subj /CN=good -days 2 2>/dev/null
+ openssl req -x509 -newkey rsa:2048 -nodes -keyout "$S/other.key" -out "$S/other.crt" -subj /CN=other -days 2 2>/dev/null
+ systemd-manifest make --private-key="$S/good.key" --certificate="$S/good.crt" "$S/payload" >"$S/Uapi16Manifest"
+ systemd-manifest make "$S/payload" >"$S/Unsigned.Uapi16Manifest"
+ cat >"$S/definitions/signed.transfer" <"$S/unsigned.transfer"
+
+ # No certificates installed: refused
+ (! "$SYSUPDATE" --definitions="$S/definitions" update)
+ # Only a different certificate installed: refused
+ mkdir -p "$VOA_DIR"
+ cp "$S/other.crt" "$VOA_DIR/test-72-other-certificate.pem"
+ (! "$SYSUPDATE" --definitions="$S/definitions" update)
+ [[ ! -e "$S/target/signed-v1.bin" ]]
+ # The right certificate in a different VOA context: refused
+ mkdir -p "/run/voa/$VOA_OS/manifest/default/x509"
+ cp "$S/good.crt" "/run/voa/$VOA_OS/manifest/default/x509/test-72-good-certificate.pem"
+ (! "$SYSUPDATE" --definitions="$S/definitions" update)
+ [[ ! -e "$S/target/signed-v1.bin" ]]
+ rm -r "/run/voa/$VOA_OS/manifest/default"
+ # The right certificate installed too, but masked: refused
+ cp "$S/good.crt" "$VOA_DIR/test-72-good-certificate.pem"
+ mkdir -p "/etc/voa/$VOA_OS/manifest/sysupdate/x509"
+ ln -s /dev/null "/etc/voa/$VOA_OS/manifest/sysupdate/x509/test-72-good-certificate.pem"
+ (! "$SYSUPDATE" --definitions="$S/definitions" update)
+ [[ ! -e "$S/target/signed-v1.bin" ]]
+ rm -r "/etc/voa/$VOA_OS/manifest"
+ # Not masked anymore: works
+ "$SYSUPDATE" --definitions="$S/definitions" update
+ cmp "$S/payload/signed-v1.bin" "$S/target/signed-v1.bin"
+ # systemd-manifest can check against the same certificates
+ systemd-manifest verify --voa-context=sysupdate --field=size,hash "$S/payload" "$S/Uapi16Manifest"
+ (! systemd-manifest verify --field=size,hash "$S/payload" "$S/Uapi16Manifest")
+ # An unsigned manifest is refused, unless verification is turned off
+ rm "$S/target/signed-v1.bin"
+ mv "$S/unsigned.transfer" "$S/definitions/signed.transfer"
+ (! "$SYSUPDATE" --definitions="$S/definitions" update)
+ [[ ! -e "$S/target/signed-v1.bin" ]]
+ "$SYSUPDATE" --definitions="$S/definitions" --verify=no update
+ cmp "$S/payload/signed-v1.bin" "$S/target/signed-v1.bin"
+ rm -r "/run/voa/$VOA_OS/manifest"
+fi
+
+# The download tools decode strictly with the declared algorithm if one is passed to them
+PULL=/usr/lib/systemd/systemd-pull
+IMPORT=/usr/lib/systemd/systemd-import
+"$PULL" raw --direct --verify=no --compression=gzip "file://$WORKDIR/uapi16/remote/remote-v1.bin.gz" "$WORKDIR/uapi16/pull-gzip.bin"
+cmp "$WORKDIR/uapi16/remote-v1.bin" "$WORKDIR/uapi16/pull-gzip.bin"
+"$PULL" raw --direct --verify=no --compression=auto "file://$WORKDIR/uapi16/remote/remote-v1.bin.gz" "$WORKDIR/uapi16/pull-auto.bin"
+cmp "$WORKDIR/uapi16/remote-v1.bin" "$WORKDIR/uapi16/pull-auto.bin"
+"$PULL" raw --direct --verify=no --compression=none "file://$WORKDIR/uapi16/remote/remote-v1.bin.gz" "$WORKDIR/uapi16/pull-none.bin"
+cmp "$WORKDIR/uapi16/remote/remote-v1.bin.gz" "$WORKDIR/uapi16/pull-none.bin"
+(! "$PULL" raw --direct --verify=no --compression=xz "file://$WORKDIR/uapi16/remote/remote-v1.bin.gz" "$WORKDIR/uapi16/pull-xz.bin")
+(! "$PULL" raw --direct --verify=no --compression=foobar "file://$WORKDIR/uapi16/remote/remote-v1.bin.gz" "$WORKDIR/uapi16/pull-foobar.bin")
+"$IMPORT" raw --direct --compression=gzip - "$WORKDIR/uapi16/import-gzip.bin" <"$WORKDIR/uapi16/remote/remote-v1.bin.gz"
+cmp "$WORKDIR/uapi16/remote-v1.bin" "$WORKDIR/uapi16/import-gzip.bin"
+(! "$IMPORT" raw --direct --compression=xz - "$WORKDIR/uapi16/import-xz.bin" <"$WORKDIR/uapi16/remote/remote-v1.bin.gz")
+
+# … and optionally verify the decoded data
+"$PULL" raw --direct --verify=no --compression=gzip --verify-decoded="$(sha256_of "$WORKDIR/uapi16/remote-v1.bin")" \
+ "file://$WORKDIR/uapi16/remote/remote-v1.bin.gz" "$WORKDIR/uapi16/pull-decoded.bin"
+cmp "$WORKDIR/uapi16/remote-v1.bin" "$WORKDIR/uapi16/pull-decoded.bin"
+(! "$PULL" raw --direct --verify=no --compression=gzip --verify-decoded="$(sha256_of "$WORKDIR/uapi16/alt-v1.bin")" \
+ "file://$WORKDIR/uapi16/remote/remote-v1.bin.gz" "$WORKDIR/uapi16/pull-decoded-bad.bin")
+"$IMPORT" raw --direct --compression=gzip --verify-decoded="$(sha256_of "$WORKDIR/uapi16/remote-v1.bin")" \
+ - "$WORKDIR/uapi16/import-decoded.bin" <"$WORKDIR/uapi16/remote/remote-v1.bin.gz"
+cmp "$WORKDIR/uapi16/remote-v1.bin" "$WORKDIR/uapi16/import-decoded.bin"
+(! "$IMPORT" raw --direct --compression=gzip --verify-decoded="$(sha256_of "$WORKDIR/uapi16/alt-v1.bin")" \
+ - "$WORKDIR/uapi16/import-decoded-bad.bin" <"$WORKDIR/uapi16/remote/remote-v1.bin.gz")
+(! "$IMPORT" raw --direct --verify-decoded=foobar - "$WORKDIR/uapi16/import-decoded-invalid.bin" <"$WORKDIR/uapi16/remote-v1.bin")
+
+rm -f "$CONFIGDIR"/01-uapi16-*.transfer
+rm -rf "$WORKDIR/uapi16"
+
+# Check MatchXAttr=: instances restricted to other systems via "user.uapi.match_…" extended attributes are
+# ignored, both when listed in UAPI.16 manifests and when found on disk, and the extended attributes listed
+# in the manifest are applied to the installed instance.
+XM="$WORKDIR/xattr-match"
+rm -rf "$XM"
+mkdir -p "$XM/definitions" "$XM/definitions-all" "$XM/definitions-target" "$XM/payload/blobs" "$XM/manifest" "$XM/target"
+for v in 1 2 3; do
+ echo "xattr match payload v$v" >"$XM/payload/blobs/xm-v$v.bin"
+done
+XM_MID="$(cat /etc/machine-id)"
+XM_OSID="$(. /etc/os-release; echo "$ID")"
+XM_OTHER=0123456789abcdef0123456789abcdef
+xm_object() {
+ local v="${1:?}" xattrs=""
+
+ [[ -n "${2:-}" ]] && xattrs=",\"extendedAttributes\":{\"user\":{$2}}"
+ printf '\036{"name":"xm-v%s.bin","size":%s,"sha256":"%s","contents":[{"file":"blobs/xm-v%s.bin"}]%s}\n' \
+ "$v" "$(stat -c %s "$XM/payload/blobs/xm-v$v.bin")" "$(sha256_of "$XM/payload/blobs/xm-v$v.bin")" "$v" "$xattrs"
+}
+{
+ printf '\036{"mediaType":"application/vnd.uapi.16.manifest"}\n'
+ # Not restricted in any way
+ xm_object 1
+ # Restricted to two machines, one of which is us, and to our OS. Also carries an unrelated attribute.
+ xm_object 2 "$(printf '"uapi.match_machine_id":{"base64":"%s"},"uapi.match_os_id":{"text":"%s"},"foo":{"text":"bar"}' \
+ "$(printf '%s\0%s\0' "$XM_OTHER" "$XM_MID" | base64 -w0)" "$XM_OSID")"
+ # Restricted to some other machine
+ xm_object 3 "$(printf '"uapi.match_machine_id":{"text":"%s"}' "$XM_OTHER")"
+ printf '\036{"mediaType":"application/vnd.uapi.16.trailer"}\n'
+} >"$XM/manifest/Uapi16Manifest"
+for d in definitions definitions-all definitions-target; do
+ cat >"$XM/$d/xm.transfer" <>"$XM/definitions-all/xm.transfer" <>"$XM/definitions-target/xm.transfer" <"$XM/target/xm-v4.bin"
+setfattr -n user.uapi.match_machine_id -v "$XM_OTHER" "$XM/target/xm-v4.bin"
+[[ $("$SYSUPDATE" --definitions="$XM/definitions-all" --verify=no --offline --json=short list | jq -cr '.all') == '["v4","v2"]' ]]
+[[ $("$SYSUPDATE" --definitions="$XM/definitions" --verify=no --offline --json=short list | jq -cr '.all') == '["v4","v2"]' ]]
+[[ $("$SYSUPDATE" --definitions="$XM/definitions-target" --verify=no --offline --json=short list | jq -cr '.all') == '["v2"]' ]]
+"$SYSUPDATE" --definitions="$XM/definitions-target" --verify=no vacuum
+[[ -e "$XM/target/xm-v2.bin" ]]
+[[ -e "$XM/target/xm-v4.bin" ]]
+rm -rf "$XM"
+
# Check that malformed manifest hashes are rejected without aborting.
rm -rf "$WORKDIR/malformed-manifest"
mkdir -p "$WORKDIR/malformed-manifest/definitions" "$WORKDIR/malformed-manifest/source" "$WORKDIR/malformed-manifest/target"