From 8a9b726de0bd2c99ddc39dbe6389259867018876 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 28 Oct 2026 11:59:44 +0100 Subject: [PATCH] =?UTF-8?q?vpick:=20honour=20the=20"user.uapi.match=5F?= =?UTF-8?q?=E2=80=A6"=20extended=20attributes=20of=20entries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit systemd-sysupdate copies the "user.uapi.match_…" extended attributes of files in UAPI.16 manifests onto the files it installs, which restrict the files to specific systems (hostname, machine ID, machine tags, os-release image/OS ID and version). Let's honour them when picking entries from .v/ directories too: entries whose extended attributes don't match the local system are skipped, as if they didn't exist. The system properties are only loaded once an entry actually carries one of the attributes. Entries referenced literally, i.e. without making use of .v/ logic, are not filtered. This may be turned off with the new PICK_IGNORE_XATTR_MATCH flag, which systemd-vpick exposes as --match-xattr=no. --- man/systemd-vpick.xml | 12 +++ man/systemd.v.xml | 20 +++++ shell-completion/bash/systemd-vpick | 10 ++- shell-completion/zsh/_systemd-vpick | 22 +++++ shell-completion/zsh/meson.build | 1 + src/bootctl/bootctl-link.c | 2 + src/core/exec-invoke.c | 3 + src/core/namespace.c | 2 + src/portable/portable.c | 3 + src/shared/discover-image.c | 2 + src/shared/mstack.c | 2 +- src/shared/vpick.c | 64 +++++++++++++- src/shared/vpick.h | 8 +- src/test/test-vpick.c | 118 ++++++++++++++++++++------ src/vpick/vpick-tool.c | 10 +++ test/units/TEST-74-AUX-UTILS.vpick.sh | 14 +++ 16 files changed, 261 insertions(+), 32 deletions(-) create mode 100644 shell-completion/zsh/_systemd-vpick diff --git a/man/systemd-vpick.xml b/man/systemd-vpick.xml index 010701637e6..a0a6df04b65 100644 --- a/man/systemd-vpick.xml +++ b/man/systemd-vpick.xml @@ -147,6 +147,18 @@ + + + + Takes a boolean argument. If true (the default), entries of the + .v/ directory whose user.uapi.match_… extended attributes do + not match the local system are skipped, see + systemd.v7 for + details. If false, these extended attributes are ignored. + + + + diff --git a/man/systemd.v.xml b/man/systemd.v.xml index 9bdf775378a..bb345fd697f 100644 --- a/man/systemd.v.xml +++ b/man/systemd.v.xml @@ -106,6 +106,26 @@ version comparison. The rest of the variable part is the version string. + + Entries may be restricted to specific systems via the extended attributes + user.uapi.match_hostname, user.uapi.match_machine_id, + user.uapi.match_machine_tags, user.uapi.match_image_id, + user.uapi.match_image_version, user.uapi.match_os_id and + user.uapi.match_os_version, which are matched against the hostname, the machine + ID, the machine tags (see TAGS= in + machine-info5), + and the IMAGE_ID=, IMAGE_VERSION=, ID= and + VERSION_ID= fields of + os-release5 of + the local system. Entries carrying any of these extended attributes that do not match the local + system (or that cannot be parsed) are filtered and not considered for the version comparison. The + syntax of these extended attributes is described in the documentation of the + MatchXAttr= setting in + sysupdate.d5: + systemd-sysupdate sets them on the files it installs, if they are defined for + them in the UAPI.16 manifest the files are downloaded from. Note that entries that are explicitly + selected by their full path (i.e. without making use of the .v/ logic) are not + filtered this way. Or in other words, the files in the .v/ directories should follow one of these diff --git a/shell-completion/bash/systemd-vpick b/shell-completion/bash/systemd-vpick index 7c67c0f09e8..bc5087beeab 100644 --- a/shell-completion/bash/systemd-vpick +++ b/shell-completion/bash/systemd-vpick @@ -30,17 +30,23 @@ _systemd-vpick() { local -A OPTS=( [STANDALONE]='--help -h --version' - [ARG]='-B --baseline -V -A -S --suffix --type -p --print --resolve' + [ARG]='-B --basename -V -A -S --suffix -t --type -p --print --resolve --match-xattr' ) _init_completion || return if __contains_word "$prev" ${OPTS[ARG]}; then case $prev in + -A) + comps="native secondary uname auto $(systemd-analyze --no-legend --no-pager architectures 2>/dev/null | { while read -r a _; do echo " $a"; done; })" + ;; + -t|--type) + comps='reg dir lnk chr blk fifo sock' + ;; -p|--print) comps='filename version type arch tries all' ;; - --resolve) + --resolve|--match-xattr) comps='yes no' ;; esac diff --git a/shell-completion/zsh/_systemd-vpick b/shell-completion/zsh/_systemd-vpick new file mode 100644 index 00000000000..ec105385a0e --- /dev/null +++ b/shell-completion/zsh/_systemd-vpick @@ -0,0 +1,22 @@ +#compdef systemd-vpick +# SPDX-License-Identifier: LGPL-2.1-or-later + +(( $+functions[_systemd-vpick_architectures] )) || +_systemd-vpick_architectures() { + local -a _architectures + _architectures=( native secondary uname auto $(systemd-analyze --quiet --no-pager --no-legend architectures 2>/dev/null | { while read -r a _; do echo "$a"; done; }) ) + _describe -t architectures 'architecture' _architectures +} + +_arguments \ + '(- *)'{-h,--help}'[Show help text]' \ + '(- *)--version[Show package version]' \ + '(-B --basename)'{-B+,--basename=}'[Look for specified basename]:basename' \ + '-V+[Look for specified version]:version' \ + '-A+[Look for specified architecture]:architecture:_systemd-vpick_architectures' \ + '(-S --suffix)'{-S+,--suffix=}'[Look for specified suffix]:suffix' \ + '*'{-t+,--type=}'[Look for specified inode type]:type:(reg dir lnk chr blk fifo sock)' \ + "--match-xattr=[Skip entries whose user.uapi.match_* xattrs don't match the system]:boolean:(yes no)" \ + '(-p --print)'{-p+,--print=}'[Print selected WHAT rather than path]:what:(filename version type arch tries all)' \ + '--resolve=[Canonicalize the result path]:boolean:(yes no)' \ + '*:path:_files' diff --git a/shell-completion/zsh/meson.build b/shell-completion/zsh/meson.build index cd1e1a7a482..8a02fd46461 100644 --- a/shell-completion/zsh/meson.build +++ b/shell-completion/zsh/meson.build @@ -64,6 +64,7 @@ foreach item : [ ['_systemd-sysupdate', 'ENABLE_SYSUPDATE'], ['_systemd-tmpfiles', 'ENABLE_TMPFILES'], ['_systemd-vmspawn', 'ENABLE_VMSPAWN'], + ['_systemd-vpick', ''], ['_timedatectl', 'ENABLE_TIMEDATED'], ['_udevadm', ''], ['_updatectl', 'ENABLE_SYSUPDATED'], diff --git a/src/bootctl/bootctl-link.c b/src/bootctl/bootctl-link.c index d5dec50e546..dbb4cfe0e23 100644 --- a/src/bootctl/bootctl-link.c +++ b/src/bootctl/bootctl-link.c @@ -1194,6 +1194,7 @@ static int link_context_add_extras(LinkContext *c, int uki_dir_fd, Set **seen) { de->d_name, &filter, /* n_filters= */ 1, + /* match_root= */ c->root, /* flags= */ 0, &pick); if (r == -ENOENT || r == 0) { @@ -1249,6 +1250,7 @@ static int link_context_find_kernel(LinkContext *c, int uki_dir_fd) { "kernel.efi.v", &pick_filter_uki, /* n_filters= */ 1, + /* match_root= */ c->root, /* flags= */ 0, &pick); if (r == -ENOENT || r == 0) diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index 2e3d106ff43..58ef65cbdee 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -3983,6 +3983,7 @@ static int pin_rootfs( context->root_image, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), + /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE, &result); if (r < 0) { @@ -4025,6 +4026,7 @@ static int pin_rootfs( context->root_directory, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), + /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE, &result); if (r < 0) { @@ -4055,6 +4057,7 @@ static int pin_rootfs( context->root_mstack, pick_filter_image_mstack, /* n_filters= */ 1, + /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE, &result); if (r < 0) { diff --git a/src/core/namespace.c b/src/core/namespace.c index 628e85594db..704b6d0a21a 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -570,6 +570,7 @@ static int append_extensions( m->source, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), + /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result); if (r == -ENOENT && m->ignore_enoent) @@ -643,6 +644,7 @@ static int append_extensions( e, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), + /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result); if (r == -ENOENT && ignore_enoent) diff --git a/src/portable/portable.c b/src/portable/portable.c index 878cf219068..27f0566c179 100644 --- a/src/portable/portable.c +++ b/src/portable/portable.c @@ -836,6 +836,7 @@ static int extract_image_and_extensions( name_or_path, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), + /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE, &result); if (r < 0) @@ -876,6 +877,7 @@ static int extract_image_and_extensions( *p, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), + /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE, &ext_result); if (r < 0) @@ -2224,6 +2226,7 @@ static int marker_matches_images(const char *marker, const char *name_or_path, c *image_name_or_path, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), + /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE, &result); if (r < 0 && r != -ENOENT) diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index 08a4dce4873..d9585af2a04 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -962,6 +962,7 @@ int image_find(RuntimeScope scope, fname_path, /* This has to be the unresolved entry with the .v suffix */ &filter, /* n_filters= */ 1, + /* match_root= */ root, PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE, &result); if (r < 0) { @@ -1184,6 +1185,7 @@ int image_discover( fname_path, /* This has to be the unresolved entry with the .v suffix */ &filter, /* n_filters= */ 1, + /* match_root= */ root, PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE, &result); if (r < 0) { diff --git a/src/shared/mstack.c b/src/shared/mstack.c index c2c1d393526..179c158870a 100644 --- a/src/shared/mstack.c +++ b/src/shared/mstack.c @@ -139,7 +139,7 @@ static int mstack_load_one(MStack *mstack, const char *dir, int dir_fd, const ch }; _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; - r = path_pick(dir, dir_fd, dir_fd, fname, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE, &result); + r = path_pick(dir, dir_fd, dir_fd, fname, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE, &result); if (r < 0) return log_debug_errno(r, "Failed to resolve '%s' directory: %m", fname); if (r == 0) diff --git a/src/shared/vpick.c b/src/shared/vpick.c index d842c166e8c..8855d807b3a 100644 --- a/src/shared/vpick.c +++ b/src/shared/vpick.c @@ -6,7 +6,9 @@ #include "architecture.h" #include "bitfield.h" #include "chase.h" +#include "errno-util.h" #include "fd-util.h" +#include "host-properties.h" #include "log.h" #include "parse-util.h" #include "path-util.h" @@ -14,6 +16,7 @@ #include "stat-util.h" #include "string-util.h" #include "vpick.h" +#include "xattr-match.h" void pick_result_done(PickResult *p) { assert(p); @@ -184,6 +187,7 @@ static int pin_choice( unsigned tries_left, unsigned tries_done, const PickFilter *filter, + const XAttrMatchFilter *xattr_filter, /* NULL to ignore "user.uapi.match_…" xattrs */ PickFlags flags, PickResult *ret) { @@ -224,6 +228,32 @@ static int pin_choice( return 0; } + if (xattr_filter) { + _cleanup_(xattr_match_lists_done) XAttrMatchLists lists = {}; + r = xattr_match_lists_read(inode_fd, "", xattr_filter->mask, &lists); + if (ERRNO_IS_NEG_PRIVILEGE(r)) { + /* Unlike fstat() on the O_PATH fd, reading user xattrs requires read access to the + * inode. Don't let a single inaccessible entry fail the whole operation. */ + log_debug_errno(r, "Not allowed to read 'user.uapi.match_*' extended attributes of '%s/%s', ignoring entry: %m", + empty_to_root(root_path), skip_leading_slash(inode_path)); + *ret = PICK_RESULT_NULL; + return 0; + } + if (r < 0) + return log_debug_errno(r, "Failed to read 'user.uapi.match_*' extended attributes of '%s/%s': %m", + empty_to_root(root_path), skip_leading_slash(inode_path)); + + r = xattr_match_filter_test_lists(xattr_filter, lists, inode_path); + if (r < 0) + return r; + if (r == 0) { + log_debug("Inode '%s/%s' is not suitable for this system, ignoring.", + empty_to_root(root_path), skip_leading_slash(inode_path)); + *ret = PICK_RESULT_NULL; + return 0; + } + } + _cleanup_(pick_result_done) PickResult result = { .fd = TAKE_FD(inode_fd), .st = st, @@ -319,6 +349,7 @@ static int make_choice( const char *inode_path, int _inode_fd, /* we always take ownership of the fd, even on failure */ const PickFilter *filter, + const XAttrMatchFilter *xattr_filter, PickFlags flags, PickResult *ret) { @@ -367,6 +398,7 @@ static int make_choice( /* tries_left= */ UINT_MAX, /* tries_done= */ UINT_MAX, filter, + xattr_filter, flags & ~PICK_RESOLVE, ret); @@ -476,6 +508,7 @@ static int make_choice( .architecture = a, .suffix = filter->suffix, }, + xattr_filter, flags, &found); if (r == 0) @@ -512,6 +545,7 @@ static int path_pick_one( int dir_fd, const char *path, const PickFilter *filter, + const XAttrMatchFilter *xattr_filter, PickFlags flags, PickResult *ret) { @@ -551,6 +585,7 @@ static int path_pick_one( path, /* inode_fd= */ -EBADF, filter, + xattr_filter, flags, ret); @@ -644,11 +679,13 @@ static int path_pick_one( .architecture = filter->architecture, .suffix = filter_suffix, }, + xattr_filter, flags, ret); bypass: - /* Don't make any choice, but just use the passed path literally */ + /* Don't make any choice, but just use the passed path literally. Since the path was explicitly + * specified we also don't check the "user.uapi.match_…" xattrs here. */ return pin_choice( root_path, root_fd, @@ -658,6 +695,7 @@ bypass: /* tries_left= */ UINT_MAX, /* tries_done= */ UINT_MAX, filter, + /* xattr_filter= */ NULL, flags, ret); } @@ -668,6 +706,7 @@ int path_pick(const char *root_path, const char *path, const PickFilter filters[], size_t n_filters, + const char *match_root, PickFlags flags, PickResult *ret) { @@ -680,11 +719,31 @@ int path_pick(const char *root_path, assert(filters || n_filters == 0); assert(ret); + /* Entries in .v/ directories may be restricted to specific systems via "user.uapi.match_…" xattrs + * (as set by systemd-sysupdate, see MatchXAttr= in sysupdate.d(5)). The properties of the system + * are only loaded once an entry actually carries one of them, from match_root (or the host if NULL). + * Note that this is deliberately separate from root_path: the directory we resolve paths in is not + * necessarily the root of the system the entries are going to be used on. */ + _cleanup_(host_properties_done) HostProperties host = {}; + const XAttrMatchFilter xattr_filter = { + .mask = XATTR_MATCH_MASK_ALL, + .host = &host, + .root = empty_to_null(match_root), + }; + /* Iterate through all filters and pick the best result */ for (size_t i = 0; i < n_filters; i++) { _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; - r = path_pick_one(root_path, root_fd, dir_fd, path, &filters[i], flags, &result); + r = path_pick_one( + root_path, + root_fd, + dir_fd, + path, + &filters[i], + FLAGS_SET(flags, PICK_IGNORE_XATTR_MATCH) ? NULL : &xattr_filter, + flags, + &result); if (r < 0) return r; if (r == 0) @@ -736,6 +795,7 @@ int path_pick_update_warn( *path, filters, n_filters, + /* match_root= */ NULL, flags, &result); if (r == -ENOENT) { diff --git a/src/shared/vpick.h b/src/shared/vpick.h index 3d9647fc32e..cc015a827aa 100644 --- a/src/shared/vpick.h +++ b/src/shared/vpick.h @@ -7,9 +7,10 @@ #include "forward.h" typedef enum PickFlags { - PICK_ARCHITECTURE = 1 << 0, /* Look for an architecture suffix */ - PICK_TRIES = 1 << 1, /* Look for tries left/tries done counters */ - PICK_RESOLVE = 1 << 2, /* Return the fully resolved (chased) path, rather than the path to the entry */ + PICK_ARCHITECTURE = 1 << 0, /* Look for an architecture suffix */ + PICK_TRIES = 1 << 1, /* Look for tries left/tries done counters */ + PICK_RESOLVE = 1 << 2, /* Return the fully resolved (chased) path, rather than the path to the entry */ + PICK_IGNORE_XATTR_MATCH = 1 << 3, /* Don't skip entries whose "user.uapi.match_…" xattrs don't match the system */ } PickFlags; typedef struct PickFilter { @@ -51,6 +52,7 @@ int path_pick(const char *root_path, const char *path, const PickFilter filters[], size_t n_filters, + const char *match_root, PickFlags flags, PickResult *ret); diff --git a/src/test/test-vpick.c b/src/test/test-vpick.c index 8e9fce19ba2..fb118a6a775 100644 --- a/src/test/test-vpick.c +++ b/src/test/test-vpick.c @@ -2,6 +2,7 @@ #include +#include "errno-util.h" #include "fd-util.h" #include "fileio.h" #include "fs-util.h" @@ -10,6 +11,7 @@ #include "tests.h" #include "tmpfile-util.h" #include "vpick.h" +#include "xattr-util.h" TEST(path_pick) { _cleanup_(rm_rf_physical_and_freep) char *p = NULL; @@ -48,7 +50,7 @@ TEST(path_pick) { }; if (IN_SET(native_architecture(), ARCHITECTURE_X86, ARCHITECTURE_X86_64)) { - ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result)); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result)); ASSERT_TRUE(S_ISREG(result.st.st_mode)); ASSERT_STREQ(result.version, "99"); ASSERT_EQ(result.architecture, ARCHITECTURE_X86); @@ -58,7 +60,7 @@ TEST(path_pick) { } filter.architecture = ARCHITECTURE_X86_64; - ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result)); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result)); ASSERT_TRUE(S_ISREG(result.st.st_mode)); ASSERT_STREQ(result.version, "55"); ASSERT_EQ(result.architecture, ARCHITECTURE_X86_64); @@ -66,7 +68,7 @@ TEST(path_pick) { pick_result_done(&result); filter.architecture = ARCHITECTURE_IA64; - ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result)); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result)); ASSERT_TRUE(S_ISREG(result.st.st_mode)); ASSERT_STREQ(result.version, "5"); ASSERT_EQ(result.architecture, ARCHITECTURE_IA64); @@ -75,7 +77,7 @@ TEST(path_pick) { filter.architecture = _ARCHITECTURE_INVALID; filter.version = "5"; - ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result)); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result)); ASSERT_TRUE(S_ISREG(result.st.st_mode)); ASSERT_STREQ(result.version, "5"); if (native_architecture() != ARCHITECTURE_IA64) { @@ -85,7 +87,7 @@ TEST(path_pick) { pick_result_done(&result); filter.architecture = ARCHITECTURE_IA64; - ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result)); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result)); ASSERT_TRUE(S_ISREG(result.st.st_mode)); ASSERT_STREQ(result.version, "5"); ASSERT_EQ(result.architecture, ARCHITECTURE_IA64); @@ -93,7 +95,7 @@ TEST(path_pick) { pick_result_done(&result); filter.architecture = ARCHITECTURE_CRIS; - ASSERT_OK_ZERO(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result)); + ASSERT_OK_ZERO(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result)); ASSERT_EQ(result.st.st_mode, MODE_INVALID); ASSERT_NULL(result.version); ASSERT_LT(result.architecture, 0); @@ -104,7 +106,7 @@ TEST(path_pick) { filter.architecture = _ARCHITECTURE_INVALID; filter.version = NULL; if (IN_SET(native_architecture(), ARCHITECTURE_X86_64, ARCHITECTURE_X86)) { - ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result)); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result)); ASSERT_TRUE(S_ISREG(result.st.st_mode)); ASSERT_STREQ(result.version, "55"); @@ -123,7 +125,7 @@ TEST(path_pick) { pp = ASSERT_NOT_NULL(path_join(p, "foo.v/foo___.raw")); if (IN_SET(native_architecture(), ARCHITECTURE_X86, ARCHITECTURE_X86_64)) { - ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result)); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result)); ASSERT_TRUE(S_ISREG(result.st.st_mode)); ASSERT_STREQ(result.version, "55"); ASSERT_EQ(result.architecture, native_architecture()); @@ -137,10 +139,10 @@ TEST(path_pick) { pp = ASSERT_NOT_NULL(path_join(p, "foo.v/foo_5.raw")); filter.type_mask = UINT32_C(1) << DT_DIR; - ASSERT_OK_ZERO(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result)); + ASSERT_OK_ZERO(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result)); filter.type_mask = UINT32_C(1) << DT_REG; - ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result)); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result)); ASSERT_TRUE(S_ISREG(result.st.st_mode)); ASSERT_NULL(result.version); ASSERT_EQ(result.architecture, _ARCHITECTURE_INVALID); @@ -153,7 +155,7 @@ TEST(path_pick) { filter.architecture = ARCHITECTURE_S390; filter.basename = "quux"; - ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result)); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result)); ASSERT_TRUE(S_ISREG(result.st.st_mode)); ASSERT_STREQ(result.version, "2"); ASSERT_EQ(result.tries_left, 4U); @@ -209,7 +211,7 @@ TEST(pick_filter_image_any) { _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; /* Test pick_filter_image_any: should pick the highest version, which is the directory test_5 */ - ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result)); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), /* match_root= */ NULL, PICK_ARCHITECTURE, &result)); ASSERT_TRUE(S_ISDIR(result.st.st_mode)); ASSERT_STREQ(result.version, "5"); ASSERT_TRUE(endswith(result.path, "/test_5")); @@ -219,14 +221,14 @@ TEST(pick_filter_image_any) { ASSERT_OK(unlinkat(sub_dfd, "test_4", AT_REMOVEDIR)); ASSERT_OK(unlinkat(sub_dfd, "test_5", AT_REMOVEDIR)); - ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result)); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), /* match_root= */ NULL, PICK_ARCHITECTURE, &result)); ASSERT_TRUE(S_ISREG(result.st.st_mode)); ASSERT_STREQ(result.version, "3"); ASSERT_TRUE(endswith(result.path, "/test_3.raw")); pick_result_done(&result); /* Verify that pick_filter_image_raw only matches .raw files */ - ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), PICK_ARCHITECTURE, &result)); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), /* match_root= */ NULL, PICK_ARCHITECTURE, &result)); ASSERT_TRUE(S_ISREG(result.st.st_mode)); ASSERT_STREQ(result.version, "3"); ASSERT_TRUE(endswith(result.path, "/test_3.raw")); @@ -239,12 +241,12 @@ TEST(pick_filter_image_any) { ASSERT_OK(unlinkat(sub_dfd, "test_3.raw", 0)); /* Now only test_10.txt, test_11.img, and test_12 remain - none should match */ - ASSERT_OK_ZERO(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result)); + ASSERT_OK_ZERO(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), /* match_root= */ NULL, PICK_ARCHITECTURE, &result)); /* But if we add a directory, it should be picked */ ASSERT_OK(mkdirat(sub_dfd, "test_6", 0755)); - ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result)); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), /* match_root= */ NULL, PICK_ARCHITECTURE, &result)); ASSERT_TRUE(S_ISDIR(result.st.st_mode)); ASSERT_STREQ(result.version, "6"); ASSERT_TRUE(endswith(result.path, "/test_6")); @@ -263,7 +265,7 @@ TEST(pick_filter_image_any) { pick_result_done(&result); - ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE, &result)); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), /* match_root= */ NULL, PICK_ARCHITECTURE, &result)); ASSERT_TRUE(S_ISDIR(result.st.st_mode)); ASSERT_STREQ(result.version, "2"); ASSERT_TRUE(endswith(result.path, "/myimage_2")); @@ -273,7 +275,7 @@ TEST(pick_filter_image_any) { ASSERT_OK(unlinkat(sub_dfd, "myimage_1", AT_REMOVEDIR)); ASSERT_OK(unlinkat(sub_dfd, "myimage_2", AT_REMOVEDIR)); - ASSERT_OK_ZERO(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE, &result)); + ASSERT_OK_ZERO(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), /* match_root= */ NULL, PICK_ARCHITECTURE, &result)); } TEST(path_pick_resolve) { @@ -294,26 +296,26 @@ TEST(path_pick_resolve) { _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; /* Test without PICK_RESOLVE - should return the symlink path */ - ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result)); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), /* match_root= */ NULL, PICK_ARCHITECTURE, &result)); ASSERT_STREQ(result.version, "2"); ASSERT_TRUE(endswith(result.path, "/resolve_2.raw")); pick_result_done(&result); /* Test with PICK_RESOLVE - should return the resolved (target) path */ - ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE|PICK_RESOLVE, &result)); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_RESOLVE, &result)); ASSERT_STREQ(result.version, "2"); ASSERT_TRUE(endswith(result.path, "/target_file.raw")); pick_result_done(&result); /* Test pick_filter_image_dir without PICK_RESOLVE */ - ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE, &result)); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), /* match_root= */ NULL, PICK_ARCHITECTURE, &result)); ASSERT_TRUE(S_ISDIR(result.st.st_mode)); ASSERT_STREQ(result.version, "1"); ASSERT_TRUE(endswith(result.path, "/resolve_1")); pick_result_done(&result); /* Test pick_filter_image_dir with PICK_RESOLVE */ - ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE|PICK_RESOLVE, &result)); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_RESOLVE, &result)); ASSERT_TRUE(S_ISDIR(result.st.st_mode)); ASSERT_STREQ(result.version, "1"); ASSERT_TRUE(endswith(result.path, "/target_dir")); @@ -323,12 +325,12 @@ TEST(path_pick_resolve) { ASSERT_OK(symlinkat("target_file.raw", dfd, "intermediate_link.raw")); ASSERT_OK(symlinkat("../intermediate_link.raw", sub_dfd, "resolve_3.raw")); - ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), PICK_ARCHITECTURE, &result)); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), /* match_root= */ NULL, PICK_ARCHITECTURE, &result)); ASSERT_STREQ(result.version, "3"); ASSERT_TRUE(endswith(result.path, "/resolve_3.raw")); pick_result_done(&result); - ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), PICK_ARCHITECTURE|PICK_RESOLVE, &result)); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_RESOLVE, &result)); ASSERT_STREQ(result.version, "3"); /* The chain should be fully resolved to target_file.raw */ ASSERT_TRUE(endswith(result.path, "/target_file.raw")); @@ -404,4 +406,72 @@ TEST(pick_result_compare) { a.version = b.version = NULL; } +TEST(path_pick_xattr_match) { + _cleanup_(rm_rf_physical_and_freep) char *p = NULL; + _cleanup_close_ int dfd = -EBADF, sub_dfd = -EBADF; + int r; + + dfd = ASSERT_OK(mkdtemp_open(/* template= */ NULL, O_DIRECTORY, &p)); + sub_dfd = ASSERT_OK(open_mkdir_at(dfd, "foo.v", /* flags= */ 0, 0777)); + + ASSERT_OK(write_string_file_at(sub_dfd, "foo_1.raw", "1", WRITE_STRING_FILE_CREATE)); + ASSERT_OK(write_string_file_at(sub_dfd, "foo_2.raw", "2", WRITE_STRING_FILE_CREATE)); + ASSERT_OK(write_string_file_at(sub_dfd, "foo_3.raw", "3", WRITE_STRING_FILE_CREATE)); + + /* The newest one is restricted to a machine ID no system has, the second newest to any hostname */ + r = xsetxattr_strv(sub_dfd, "foo_3.raw", /* at_flags= */ 0, "user.uapi.match_machine_id", STRV_MAKE("00000000000000000000000000000000")); + if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) + return (void) log_tests_skipped_errno(r, "User extended attributes not supported"); + ASSERT_OK(r); + ASSERT_OK(xsetxattr_strv(sub_dfd, "foo_2.raw", /* at_flags= */ 0, "user.uapi.match_hostname", STRV_MAKE("*"))); + + _cleanup_free_ char *pp = ASSERT_NOT_NULL(path_join(p, "foo.v")); + _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; + PickFilter filter = { + .architecture = _ARCHITECTURE_INVALID, + .suffix = ".raw", + }; + + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result)); + ASSERT_STREQ(result.version, "2"); + pick_result_done(&result); + + /* Also when the version is fully specified */ + filter.version = "3"; + ASSERT_OK_ZERO(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, /* flags= */ 0, &result)); + filter.version = NULL; + + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES|PICK_IGNORE_XATTR_MATCH, &result)); + ASSERT_STREQ(result.version, "3"); + pick_result_done(&result); + + /* An invalid extended attribute disqualifies an entry too */ + ASSERT_OK(xsetxattr_strv(sub_dfd, "foo_2.raw", /* at_flags= */ 0, "user.uapi.match_hostname", STRV_MAKE(""))); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result)); + ASSERT_STREQ(result.version, "1"); + pick_result_done(&result); + + /* The host properties are loaded from match_root if specified, not from the host */ + _cleanup_(rm_rf_physical_and_freep) char *mr = NULL; + _cleanup_close_ int mr_fd = ASSERT_OK(mkdtemp_open(/* template= */ NULL, O_DIRECTORY, &mr)); + ASSERT_OK(write_string_file_at(mr_fd, "etc/hostname", "vpick-test-host", WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755)); + ASSERT_OK(write_string_file_at(sub_dfd, "foo_4.raw", "4", WRITE_STRING_FILE_CREATE)); + ASSERT_OK(xsetxattr_strv(sub_dfd, "foo_4.raw", /* at_flags= */ 0, "user.uapi.match_hostname", STRV_MAKE("vpick-test-host"))); + + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, mr, PICK_ARCHITECTURE|PICK_TRIES, &result)); + ASSERT_STREQ(result.version, "4"); + pick_result_done(&result); + + /* An empty /etc/hostname in match_root means no hostname, which doesn't match, but isn't an error */ + ASSERT_OK(write_string_file_at(mr_fd, "etc/hostname", "# nothing", WRITE_STRING_FILE_TRUNCATE)); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, mr, PICK_ARCHITECTURE|PICK_TRIES, &result)); + ASSERT_STREQ(result.version, "1"); + pick_result_done(&result); + + /* A path that doesn't make use of .v/ is taken literally */ + _cleanup_free_ char *literal = ASSERT_NOT_NULL(path_join(p, "foo.v/foo_3.raw")); + ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, literal, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result)); + ASSERT_TRUE(endswith(result.path, "/foo_3.raw")); +} + DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/vpick/vpick-tool.c b/src/vpick/vpick-tool.c index 5e6e8687960..46f3d90a19f 100644 --- a/src/vpick/vpick-tool.c +++ b/src/vpick/vpick-tool.c @@ -10,6 +10,7 @@ #include "format-table.h" #include "log.h" #include "main-func.h" +#include "parse-argument.h" #include "parse-util.h" #include "path-util.h" #include "stat-util.h" @@ -140,6 +141,14 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { break; + OPTION_LONG("match-xattr", "BOOL", "Skip entries whose user.uapi.match_* xattrs don't match the system (default: yes)"): + r = parse_boolean_argument("--match-xattr=", opts.arg, /* ret= */ NULL); + if (r < 0) + return r; + + SET_FLAG(arg_flags, PICK_IGNORE_XATTR_MATCH, !r); + break; + OPTION_GROUP("Output"): {} OPTION_COMMON_HELP: @@ -223,6 +232,7 @@ static int run(int argc, char *argv[]) { .type_mask = arg_filter_type_mask, }, /* n_filters= */ 1, + /* match_root= */ NULL, arg_flags, &result); if (r < 0) diff --git a/test/units/TEST-74-AUX-UTILS.vpick.sh b/test/units/TEST-74-AUX-UTILS.vpick.sh index 4c78b91002c..065f4df67a6 100755 --- a/test/units/TEST-74-AUX-UTILS.vpick.sh +++ b/test/units/TEST-74-AUX-UTILS.vpick.sh @@ -8,6 +8,7 @@ at_exit() { rm -rf /var/lib/machines/mymachine.raw.v rm -rf /var/lib/machines/mytree.v rm -rf /var/lib/machines/testroot.v + rm -rf /var/lib/machines/xattr.raw.v umount -l /tmp/dotvroot rmdir /tmp/dotvroot } @@ -114,3 +115,16 @@ test "$(systemd-vpick --resolve=yes /var/lib/machines/testroot.v)" = /var/lib/ma rm -rf /var/lib/machines/testroot.v/testroot_32 (! systemd-vpick /var/lib/machines/testroot.v) (! systemd-run --wait -p RootDirectory=/var/lib/machines/testroot.v true) + +# Entries may be restricted to specific systems via "user.uapi.match_…" extended attributes +mkdir /var/lib/machines/xattr.raw.v +touch /var/lib/machines/xattr.raw.v/xattr_1.raw /var/lib/machines/xattr.raw.v/xattr_2.raw +setfattr -n user.uapi.match_machine_id -v 00000000000000000000000000000000 /var/lib/machines/xattr.raw.v/xattr_2.raw +test "$(systemd-vpick --suffix=.raw /var/lib/machines/xattr.raw.v)" = /var/lib/machines/xattr.raw.v/xattr_1.raw +test "$(systemd-vpick --suffix=.raw --match-xattr=no /var/lib/machines/xattr.raw.v)" = /var/lib/machines/xattr.raw.v/xattr_2.raw +setfattr -n user.uapi.match_machine_id -v "$(