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 "$(