From bcafa70974df1ea1d5a81fa3498512af1394840b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 21 Oct 2025 23:00:46 +0200 Subject: [PATCH 01/10] nsresource: add constants for the only supported UID range size allocations As requested: https://github.com/systemd/systemd/pull/39322#discussion_r2447366728 --- src/dissect/dissect.c | 2 +- src/nspawn/nspawn.c | 4 ++-- src/shared/nsresource.h | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index 332471a409..ca69b36c22 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -2302,7 +2302,7 @@ static int run(int argc, char *argv[]) { /* Don't run things in private userns, if the mount shall be attached to the host */ if (!IN_SET(arg_action, ACTION_MOUNT, ACTION_WITH)) { - userns_fd = nsresource_allocate_userns(/* name= */ NULL, UINT64_C(0x10000)); /* allocate 64K users by default */ + userns_fd = nsresource_allocate_userns(/* name= */ NULL, NSRESOURCE_UIDS_64K); /* allocate 64K users by default */ if (userns_fd < 0) return log_error_errno(userns_fd, "Failed to allocate user namespace with 64K users: %m"); } diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 3e4565e4a2..5de826c44a 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -6007,7 +6007,7 @@ static int run(int argc, char *argv[]) { goto finish; } - userns_fd = nsresource_allocate_userns(userns_name, UINT64_C(0x10000)); + userns_fd = nsresource_allocate_userns(userns_name, NSRESOURCE_UIDS_64K); /* allocate 64K UIDs */ if (userns_fd < 0) { r = log_error_errno(userns_fd, "Failed to allocate user namespace with 64K users: %m"); goto finish; @@ -6019,7 +6019,7 @@ static int run(int argc, char *argv[]) { goto finish; } - arg_uid_range = UINT32_C(0x10000); + arg_uid_range = NSRESOURCE_UIDS_64K; } if (arg_directory) { diff --git a/src/shared/nsresource.h b/src/shared/nsresource.h index c06025761a..136b1f85ac 100644 --- a/src/shared/nsresource.h +++ b/src/shared/nsresource.h @@ -3,6 +3,10 @@ #include "shared-forward.h" +/* Helpful constants for the only numbers of UIDs that can currently be allocated */ +#define NSRESOURCE_UIDS_64K 0x10000U +#define NSRESOURCE_UIDS_1 1U + int nsresource_allocate_userns(const char *name, uint64_t size); int nsresource_register_userns(const char *name, int userns_fd); int nsresource_add_mount(int userns_fd, int mount_fd); From 0a9e2e45f03b605b59b8b20c0968524ff4ff8714 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 19 Aug 2025 21:17:32 +0200 Subject: [PATCH 02/10] copy: optionally apply source inode's stat() when doing a merge copy --- src/shared/copy.c | 2 +- src/shared/copy.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/shared/copy.c b/src/shared/copy.c index 981427214b..baed8b0696 100644 --- a/src/shared/copy.c +++ b/src/shared/copy.c @@ -1257,7 +1257,7 @@ static int fd_copy_directory( } finish: - if (!exists) { + if (FLAGS_SET(copy_flags, COPY_MERGE_APPLY_STAT) || !exists) { if (fchown(fdt, uid_is_valid(override_uid) ? override_uid : st->st_uid, gid_is_valid(override_gid) ? override_gid : st->st_gid) < 0) diff --git a/src/shared/copy.h b/src/shared/copy.h index d4b131d102..8dd3176e8e 100644 --- a/src/shared/copy.h +++ b/src/shared/copy.h @@ -33,6 +33,7 @@ typedef enum CopyFlags { */ COPY_NOCOW_AFTER = 1 << 20, COPY_PRESERVE_FS_VERITY = 1 << 21, /* Preserve fs-verity when copying. */ + COPY_MERGE_APPLY_STAT = 1 << 22, /* When we reuse an existing directory inode, apply source ownership/mode/xattrs/timestamps */ } CopyFlags; typedef enum DenyType { From 69332065e204ad8516d19b8197e78c715873d54d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 19 Aug 2025 21:14:19 +0200 Subject: [PATCH 03/10] copy: make copy_tree_at_full()'s 'to' parameter optional Sometimes it's quite useful to pin a source dir via an fd, as well as a target dir the same way, and then ask copy_tree_at_full() to copy the contents from one to the other. Make this possible, by allowing 'to' be NULL. (Previously, it had to be non-NULL, i.e. the function would always create a new dir, no matter what.) Note that we only support that for dir inodes. --- src/shared/copy.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/shared/copy.c b/src/shared/copy.c index baed8b0696..054a01e86b 100644 --- a/src/shared/copy.c +++ b/src/shared/copy.c @@ -657,7 +657,10 @@ static int hardlink_context_setup( return -errno; } - r = tempfn_random_child(to, "hardlink", &c->subdir); + if (to) + r = tempfn_random_child(to, "hardlink", &c->subdir); + else + r = tempfn_random("hardlink", /* extra= */ NULL, &c->subdir); if (r < 0) return r; @@ -1127,7 +1130,6 @@ static int fd_copy_directory( int r; assert(st); - assert(to); if (depth_left == 0) return -ENAMETOOLONG; @@ -1345,6 +1347,10 @@ static int fd_copy_tree_generic( override_gid, copy_flags, denylist, subvolumes, hardlink_context, display_path, progress_path, progress_bytes, userdata); + /* Only if we are copying a directory we are fine if the target dir is referenced by fd only */ + if (!to) + return -ENOTDIR; + DenyType t = PTR_TO_INT(hashmap_get(denylist, st)); if (t == DENY_INODE) { log_debug("%s is in the denylist, ignoring", from ?: "file to copy"); @@ -1382,7 +1388,6 @@ int copy_tree_at_full( struct stat st; int r; - assert(to); assert(!FLAGS_SET(copy_flags, COPY_LOCK_BSD)); if (fstatat(fdf, strempty(from), &st, AT_SYMLINK_NOFOLLOW | (isempty(from) ? AT_EMPTY_PATH : 0)) < 0) From 570366b6e17f674c6621b06384563cf6f71d0383 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 19 Aug 2025 21:07:08 +0200 Subject: [PATCH 04/10] install-file: add flags to handle RO and syncing failures graceful When operating in unprivileged mode we might not be able to execute the necessary operations to make a disk image read-only (because FS_IMMUTABEL_FL needs privs for example), and syncing (because for that we might need to open the root inode, but that might not be possible from the outside). Let's deal with that by making these operation optional: if they work great, if not they don't. --- src/shared/install-file.c | 106 +++++++++++++++++++++++++------------- src/shared/install-file.h | 1 + 2 files changed, 70 insertions(+), 37 deletions(-) diff --git a/src/shared/install-file.c b/src/shared/install-file.c index c3de84f579..b84e0a2986 100644 --- a/src/shared/install-file.c +++ b/src/shared/install-file.c @@ -95,6 +95,11 @@ static int unlinkat_maybe_dir(int dirfd, const char *pathname) { return 0; } +static bool need_opath(InstallFileFlags flags) { + /* Returns true when we need to pin the source file via an O_PATH fd */ + return (flags & (INSTALL_FSYNC|INSTALL_FSYNC_FULL|INSTALL_SYNCFS|INSTALL_READ_ONLY)) != 0; +} + int install_file(int source_atfd, const char *source_name, int target_atfd, const char *target_name, InstallFileFlags flags) { @@ -118,7 +123,7 @@ int install_file(int source_atfd, const char *source_name, * already in place, and only the syncing/read-only marking shall be applied. Note that with * target_name=NULL and flags=0 this call is a NOP */ - if ((flags & (INSTALL_FSYNC|INSTALL_FSYNC_FULL|INSTALL_SYNCFS|INSTALL_READ_ONLY)) != 0) { + if (need_opath(flags)) { _cleanup_close_ int pfd = -EBADF; struct stat st; @@ -137,24 +142,32 @@ int install_file(int source_atfd, const char *source_name, _cleanup_close_ int regfd = -EBADF; regfd = fd_reopen(pfd, O_RDONLY|O_CLOEXEC); - if (regfd < 0) - return regfd; + if (regfd < 0) { + if (!FLAGS_SET(flags, INSTALL_GRACEFUL)) + return log_debug_errno(regfd, "Failed to open referenced inode: %m"); - if ((flags & (INSTALL_FSYNC_FULL|INSTALL_SYNCFS)) != 0) { - /* If this is just a regular file (as oppose to a fully populated directory) - * let's downgrade INSTALL_SYNCFS to INSTALL_FSYNC_FULL, after all this is - * going to be a single inode we install */ - r = fsync_full(regfd); - if (r < 0) - return r; - } else if (flags & INSTALL_FSYNC) { - if (fsync(regfd) < 0) - return -errno; + log_debug_errno(regfd, "Failed to open referenced inode, ignoring: %m"); + } else { + if ((flags & (INSTALL_FSYNC_FULL|INSTALL_SYNCFS)) != 0) + /* If this is just a regular file (as opposed to a fully populated + * directory) let's downgrade INSTALL_SYNCFS to INSTALL_FSYNC_FULL, + * after all this is going to be a single inode we install */ + r = fsync_full(regfd); + else if (flags & INSTALL_FSYNC) + r = RET_NERRNO(fsync(regfd)); + else + r = 0; + if (r < 0) { + if (!FLAGS_SET(flags, INSTALL_GRACEFUL)) + return log_debug_errno(r, "Failed to sync source inode: %m"); + + log_debug_errno(r, "Failed to sync source inode, ignoring: %m"); + } + + if (flags & INSTALL_READ_ONLY) + rofd = TAKE_FD(regfd); } - if (flags & INSTALL_READ_ONLY) - rofd = TAKE_FD(regfd); - break; } @@ -162,24 +175,31 @@ int install_file(int source_atfd, const char *source_name, _cleanup_close_ int dfd = -EBADF; dfd = fd_reopen(pfd, O_RDONLY|O_DIRECTORY|O_CLOEXEC); - if (dfd < 0) - return dfd; + if (dfd < 0) { + if (!FLAGS_SET(flags, INSTALL_GRACEFUL)) + return log_debug_errno(dfd, "Failed to open referenced inode: %m"); - if (flags & INSTALL_SYNCFS) { - if (syncfs(dfd) < 0) - return -errno; - } else if (flags & INSTALL_FSYNC_FULL) { - r = fsync_full(dfd); - if (r < 0) - return r; - } else if (flags & INSTALL_FSYNC) { - if (fsync(dfd) < 0) - return -errno; + log_debug_errno(dfd, "Failed to open referenced inode, ignoring: %m"); + } else { + if (flags & INSTALL_SYNCFS) + r = RET_NERRNO(syncfs(dfd)); + else if (flags & INSTALL_FSYNC_FULL) + r = fsync_full(dfd); + else if (flags & INSTALL_FSYNC) + r = RET_NERRNO(fsync(dfd)); + else + r = 0; + if (r < 0) { + if (!FLAGS_SET(flags, INSTALL_GRACEFUL)) + return log_debug_errno(r, "Failed to sync source inode: %m"); + + log_debug_errno(r, "Failed to sync source inode, ignoring: %m"); + } + + if (flags & INSTALL_READ_ONLY) + rofd = TAKE_FD(dfd); } - if (flags & INSTALL_READ_ONLY) - rofd = TAKE_FD(dfd); - break; } @@ -190,8 +210,12 @@ int install_file(int source_atfd, const char *source_name, if (target_name && (flags & (INSTALL_FSYNC_FULL|INSTALL_SYNCFS)) != 0) { r = fsync_directory_of_file(pfd); - if (r < 0) - return r; + if (r < 0) { + if (!FLAGS_SET(flags, INSTALL_GRACEFUL)) + return log_debug_errno(r, "Failed to sync source inode: %m"); + + log_debug_errno(r, "Failed to sync source inode, ignoring: %m"); + } } break; @@ -254,8 +278,12 @@ int install_file(int source_atfd, const char *source_name, if (rofd >= 0) { r = fs_make_very_read_only(rofd); - if (r < 0) - return r; + if (r < 0) { + if (!FLAGS_SET(flags, INSTALL_GRACEFUL)) + return log_debug_errno(r, "Failed to make inode read-only: %m"); + + log_debug_errno(r, "Failed to make inode read-only, ignoring: %m"); + } } if ((flags & (INSTALL_FSYNC_FULL|INSTALL_SYNCFS)) != 0) { @@ -263,8 +291,12 @@ int install_file(int source_atfd, const char *source_name, r = fsync_parent_at(target_atfd, target_name); else r = fsync_parent_at(source_atfd, source_name); - if (r < 0) - return r; + if (r < 0) { + if (!FLAGS_SET(flags, INSTALL_GRACEFUL)) + return log_debug_errno(r, "Failed to sync inode: %m"); + + log_debug_errno(r, "Failed to sync inode, ignoring: %m"); + } } return 0; diff --git a/src/shared/install-file.h b/src/shared/install-file.h index cb94e32cae..f5e2fa33ca 100644 --- a/src/shared/install-file.h +++ b/src/shared/install-file.h @@ -9,6 +9,7 @@ typedef enum InstallFileFlags { INSTALL_FSYNC = 1 << 2, /* fsync() file contents before moving file in */ INSTALL_FSYNC_FULL = 1 << 3, /* like INSTALL_FSYNC, but also fsync() parent dir before+after moving file in */ INSTALL_SYNCFS = 1 << 4, /* syncfs() before moving file in, fsync() parent dir after moving file in */ + INSTALL_GRACEFUL = 1 << 5, /* don't fail if we cannot sync or mark read-only */ } InstallFileFlags; int install_file(int source_atfd, const char *source_name, int target_atfd, const char *target_name, InstallFileFlags flags); From b76a76ac15fa3a609982c298e58ee756a6b21f46 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 18 Aug 2025 09:12:27 +0200 Subject: [PATCH 05/10] importd: teach all backends --system/--user mode The runtime scope logic is internally already in place, let's expose this via getopt() command line too. This way importd later can propagate the invocation scope down to the backends. --- src/import/export.c | 16 +++++++++++++++- src/import/import-fs.c | 16 +++++++++++++++- src/import/import.c | 16 +++++++++++++++- src/import/pull.c | 16 +++++++++++++++- 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/import/export.c b/src/import/export.c index d891ca1816..bbea5b0343 100644 --- a/src/import/export.c +++ b/src/import/export.c @@ -207,7 +207,9 @@ static int help(int argc, char *argv[], void *userdata) { " --version Show package version\n" " --format=FORMAT Select format\n" " --class=CLASS Select image class (machine, sysext, confext,\n" - " portable)\n", + " portable)\n" + " --system Operate in per-system mode\n" + " --user Operate in per-user mode\n", program_invocation_short_name, ansi_underline(), ansi_normal(), @@ -223,6 +225,8 @@ static int parse_argv(int argc, char *argv[]) { ARG_VERSION = 0x100, ARG_FORMAT, ARG_CLASS, + ARG_SYSTEM, + ARG_USER, }; static const struct option options[] = { @@ -230,6 +234,8 @@ static int parse_argv(int argc, char *argv[]) { { "version", no_argument, NULL, ARG_VERSION }, { "format", required_argument, NULL, ARG_FORMAT }, { "class", required_argument, NULL, ARG_CLASS }, + { "system", no_argument, NULL, ARG_SYSTEM }, + { "user", no_argument, NULL, ARG_USER }, {} }; @@ -262,6 +268,14 @@ static int parse_argv(int argc, char *argv[]) { break; + case ARG_SYSTEM: + arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; + break; + + case ARG_USER: + arg_runtime_scope = RUNTIME_SCOPE_USER; + break; + case '?': return -EINVAL; diff --git a/src/import/import-fs.c b/src/import/import-fs.c index 783b0461ef..342c024d56 100644 --- a/src/import/import-fs.c +++ b/src/import/import-fs.c @@ -280,7 +280,9 @@ static int help(int argc, char *argv[], void *userdata) { " subvolume\n" " --sync=BOOL Controls whether to sync() before completing\n" " --class=CLASS Select image class (machine, sysext, confext,\n" - " portable)\n", + " portable)\n" + " --system Operate in per-system mode\n" + " --user Operate in per-user mode\n", program_invocation_short_name, ansi_underline(), ansi_normal(), @@ -302,6 +304,8 @@ static int parse_argv(int argc, char *argv[]) { ARG_BTRFS_QUOTA, ARG_SYNC, ARG_CLASS, + ARG_SYSTEM, + ARG_USER, }; static const struct option options[] = { @@ -315,6 +319,8 @@ static int parse_argv(int argc, char *argv[]) { { "btrfs-quota", required_argument, NULL, ARG_BTRFS_QUOTA }, { "sync", required_argument, NULL, ARG_SYNC }, { "class", required_argument, NULL, ARG_CLASS }, + { "system", no_argument, NULL, ARG_SYSTEM }, + { "user", no_argument, NULL, ARG_USER }, {} }; @@ -377,6 +383,14 @@ static int parse_argv(int argc, char *argv[]) { break; + case ARG_SYSTEM: + arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; + break; + + case ARG_USER: + arg_runtime_scope = RUNTIME_SCOPE_USER; + break; + case '?': return -EINVAL; diff --git a/src/import/import.c b/src/import/import.c index 9b458dff9f..f41a42f7e6 100644 --- a/src/import/import.c +++ b/src/import/import.c @@ -290,7 +290,9 @@ static int help(int argc, char *argv[], void *userdata) { " --offset=BYTES Offset to seek to in destination\n" " --size-max=BYTES Maximum number of bytes to write to destination\n" " --class=CLASS Select image class (machine, sysext, confext,\n" - " portable)\n", + " portable)\n" + " --system Operate in per-system mode\n" + " --user Operate in per-user mode\n", program_invocation_short_name, ansi_underline(), ansi_normal(), @@ -315,6 +317,8 @@ static int parse_argv(int argc, char *argv[]) { ARG_OFFSET, ARG_SIZE_MAX, ARG_CLASS, + ARG_SYSTEM, + ARG_USER, }; static const struct option options[] = { @@ -331,6 +335,8 @@ static int parse_argv(int argc, char *argv[]) { { "offset", required_argument, NULL, ARG_OFFSET }, { "size-max", required_argument, NULL, ARG_SIZE_MAX }, { "class", required_argument, NULL, ARG_CLASS }, + { "system", no_argument, NULL, ARG_SYSTEM }, + { "user", no_argument, NULL, ARG_USER }, {} }; @@ -430,6 +436,14 @@ static int parse_argv(int argc, char *argv[]) { break; + case ARG_SYSTEM: + arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; + break; + + case ARG_USER: + arg_runtime_scope = RUNTIME_SCOPE_USER; + break; + case '?': return -EINVAL; diff --git a/src/import/pull.c b/src/import/pull.c index 41193e4397..9c915536d8 100644 --- a/src/import/pull.c +++ b/src/import/pull.c @@ -275,7 +275,9 @@ static int help(int argc, char *argv[], void *userdata) { " --class=CLASS Select image class (machine, sysext, confext,\n" " portable)\n" " --keep-download=BOOL Keep a copy pristine copy of the downloaded file\n" - " around\n", + " around\n" + " --system Operate in per-system mode\n" + " --user Operate in per-user mode\n", program_invocation_short_name, ansi_underline(), ansi_normal(), @@ -306,6 +308,8 @@ static int parse_argv(int argc, char *argv[]) { ARG_SIZE_MAX, ARG_CLASS, ARG_KEEP_DOWNLOAD, + ARG_SYSTEM, + ARG_USER, }; static const struct option options[] = { @@ -328,6 +332,8 @@ static int parse_argv(int argc, char *argv[]) { { "size-max", required_argument, NULL, ARG_SIZE_MAX }, { "class", required_argument, NULL, ARG_CLASS }, { "keep-download", required_argument, NULL, ARG_KEEP_DOWNLOAD }, + { "system", no_argument, NULL, ARG_SYSTEM }, + { "user", no_argument, NULL, ARG_USER }, {} }; @@ -508,6 +514,14 @@ static int parse_argv(int argc, char *argv[]) { auto_keep_download = false; break; + case ARG_SYSTEM: + arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; + break; + + case ARG_USER: + arg_runtime_scope = RUNTIME_SCOPE_USER; + break; + case '?': return -EINVAL; From f7c72e87258e285fa006125f751daebeb6089c2c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 25 Jul 2025 08:25:29 +0200 Subject: [PATCH 06/10] importd: properly support operation in per-user mode --- src/import/importd.c | 195 +++++++++++------- src/import/meson.build | 3 + ...rg.freedesktop.import1.service-for-session | 13 ++ units/user/meson.build | 10 + units/user/systemd-importd.service.in | 28 +++ units/user/systemd-importd.socket | 18 ++ 6 files changed, 189 insertions(+), 78 deletions(-) create mode 100644 src/import/org.freedesktop.import1.service-for-session create mode 100644 units/user/systemd-importd.service.in create mode 100644 units/user/systemd-importd.socket diff --git a/src/import/importd.c b/src/import/importd.c index e9923d93ad..99b8af580d 100644 --- a/src/import/importd.c +++ b/src/import/importd.c @@ -32,6 +32,7 @@ #include "notify-recv.h" #include "os-util.h" #include "parse-util.h" +#include "path-lookup.h" #include "percent-util.h" #include "pidref.h" #include "process-util.h" @@ -100,7 +101,8 @@ typedef struct Transfer { typedef struct Manager { sd_event *event; - sd_bus *bus; + sd_bus *api_bus; + sd_bus *system_bus; sd_varlink_server *varlink_server; uint32_t current_transfer_id; @@ -113,7 +115,7 @@ typedef struct Manager { bool use_btrfs_subvol; bool use_btrfs_quota; - RuntimeScope runtime_scope; /* for now: always RUNTIME_SCOPE_SYSTEM */ + RuntimeScope runtime_scope; } Manager; #define TRANSFERS_MAX 64 @@ -223,7 +225,7 @@ static void transfer_send_log_line(Transfer *t, const char *line) { log_full(priority, "(transfer%" PRIu32 ") %s", t->id, line); r = sd_bus_emit_signal( - t->manager->bus, + t->manager->api_bus, t->object_path, "org.freedesktop.import1.Transfer", "LogMessage", @@ -254,7 +256,7 @@ static void transfer_send_progress_update(Transfer *t) { double progress = transfer_percent_as_double(t); r = sd_bus_emit_signal( - t->manager->bus, + t->manager->api_bus, t->object_path, "org.freedesktop.import1.Transfer", "ProgressUpdate", @@ -335,7 +337,7 @@ static int transfer_finalize(Transfer *t, bool success) { transfer_send_logs(t, true); r = sd_bus_emit_signal( - t->manager->bus, + t->manager->api_bus, "/org/freedesktop/import1", "org.freedesktop.import1.Manager", "TransferRemoved", @@ -443,6 +445,7 @@ static int transfer_start(Transfer *t) { const char *cmd[] = { NULL, /* systemd-import, systemd-import-fs, systemd-export or systemd-pull */ NULL, /* tar, raw */ + NULL, /* --system or --user */ NULL, /* --verify= */ NULL, /* verify argument */ NULL, /* --class= */ @@ -524,6 +527,8 @@ static int transfer_start(Transfer *t) { ; } + cmd[k++] = runtime_scope_cmdline_option_to_string(t->manager->runtime_scope); + if (t->verify != _IMPORT_VERIFY_INVALID) { cmd[k++] = "--verify"; cmd[k++] = import_verify_to_string(t->verify); @@ -602,7 +607,7 @@ static int transfer_start(Transfer *t) { return r; r = sd_bus_emit_signal( - t->manager->bus, + t->manager->api_bus, "/org/freedesktop/import1", "org.freedesktop.import1.Manager", "TransferNew", @@ -630,7 +635,8 @@ static Manager *manager_unref(Manager *m) { hashmap_free(m->polkit_registry); - m->bus = sd_bus_flush_close_unref(m->bus); + m->api_bus = sd_bus_flush_close_unref(m->api_bus); + m->system_bus = sd_bus_flush_close_unref(m->system_bus); m->varlink_server = sd_varlink_server_unref(m->varlink_server); sd_event_unref(m->event); @@ -681,7 +687,7 @@ static int manager_on_notify(sd_event_source *s, int fd, uint32_t revents, void return 0; } -static int manager_new(Manager **ret) { +static int manager_new(RuntimeScope scope, Manager **ret) { _cleanup_(manager_unrefp) Manager *m = NULL; int r; @@ -694,7 +700,7 @@ static int manager_new(Manager **ret) { *m = (Manager) { .use_btrfs_subvol = true, .use_btrfs_quota = true, - .runtime_scope = RUNTIME_SCOPE_SYSTEM, + .runtime_scope = scope, }; r = sd_event_default(&m->event); @@ -757,16 +763,18 @@ static int method_import_tar_or_raw(sd_bus_message *msg, void *userdata, sd_bus_ assert(msg); - r = bus_verify_polkit_async( - msg, - "org.freedesktop.import1.import", - /* details= */ NULL, - &m->polkit_registry, - error); - if (r < 0) - return r; - if (r == 0) - return 1; /* Will call us back */ + if (m->runtime_scope != RUNTIME_SCOPE_USER) { + r = bus_verify_polkit_async( + msg, + "org.freedesktop.import1.import", + /* details= */ NULL, + &m->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + } if (endswith(sd_bus_message_get_member(msg), "Ex")) { const char *sclass; @@ -858,16 +866,18 @@ static int method_import_fs(sd_bus_message *msg, void *userdata, sd_bus_error *e assert(msg); - r = bus_verify_polkit_async( - msg, - "org.freedesktop.import1.import", - /* details= */ NULL, - &m->polkit_registry, - error); - if (r < 0) - return r; - if (r == 0) - return 1; /* Will call us back */ + if (m->runtime_scope != RUNTIME_SCOPE_USER) { + r = bus_verify_polkit_async( + msg, + "org.freedesktop.import1.import", + /* details= */ NULL, + &m->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + } if (endswith(sd_bus_message_get_member(msg), "Ex")) { const char *sclass; @@ -956,16 +966,18 @@ static int method_export_tar_or_raw(sd_bus_message *msg, void *userdata, sd_bus_ assert(msg); - r = bus_verify_polkit_async( - msg, - "org.freedesktop.import1.export", - /* details= */ NULL, - &m->polkit_registry, - error); - if (r < 0) - return r; - if (r == 0) - return 1; /* Will call us back */ + if (m->runtime_scope != RUNTIME_SCOPE_USER) { + r = bus_verify_polkit_async( + msg, + "org.freedesktop.import1.export", + /* details= */ NULL, + &m->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + } if (endswith(sd_bus_message_get_member(msg), "Ex")) { const char *sclass; @@ -1054,16 +1066,18 @@ static int method_pull_tar_or_raw(sd_bus_message *msg, void *userdata, sd_bus_er assert(msg); - r = bus_verify_polkit_async( - msg, - "org.freedesktop.import1.pull", - /* details= */ NULL, - &m->polkit_registry, - error); - if (r < 0) - return r; - if (r == 0) - return 1; /* Will call us back */ + if (m->runtime_scope != RUNTIME_SCOPE_USER) { + r = bus_verify_polkit_async( + msg, + "org.freedesktop.import1.pull", + /* details= */ NULL, + &m->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + } if (endswith(sd_bus_message_get_member(msg), "Ex")) { const char *sclass; @@ -1702,28 +1716,43 @@ static int manager_connect_bus(Manager *m) { assert(m); assert(m->event); - assert(!m->bus); + assert(!m->system_bus); + assert(!m->api_bus); - r = bus_open_system_watch_bind(&m->bus); + r = bus_open_system_watch_bind(&m->system_bus); if (r < 0) return log_error_errno(r, "Failed to get system bus connection: %m"); - r = bus_add_implementation(m->bus, &manager_object, m); + r = sd_bus_attach_event(m->system_bus, m->event, 0); + if (r < 0) + return log_error_errno(r, "Failed to attach system bus to event loop: %m"); + + if (m->runtime_scope == RUNTIME_SCOPE_SYSTEM) + m->api_bus = sd_bus_ref(m->system_bus); + else { + assert(m->runtime_scope == RUNTIME_SCOPE_USER); + + r = sd_bus_default_user(&m->api_bus); + if (r < 0) + return log_error_errno(r, "Failed to get user bus connection: %m"); + + r = sd_bus_attach_event(m->api_bus, m->event, 0); + if (r < 0) + return log_error_errno(r, "Failed to attach user bus to event loop: %m"); + } + + r = bus_add_implementation(m->api_bus, &manager_object, m); if (r < 0) return r; - r = bus_log_control_api_register(m->bus); + r = bus_log_control_api_register(m->api_bus); if (r < 0) return r; - r = sd_bus_request_name_async(m->bus, NULL, "org.freedesktop.import1", 0, NULL, NULL); + r = sd_bus_request_name_async(m->api_bus, NULL, "org.freedesktop.import1", 0, NULL, NULL); if (r < 0) return log_error_errno(r, "Failed to request name: %m"); - r = sd_bus_attach_event(m->bus, m->event, 0); - if (r < 0) - return log_error_errno(r, "Failed to attach bus to event loop: %m"); - return 0; } @@ -1867,19 +1896,21 @@ static int vl_method_pull(sd_varlink *link, sd_json_variant *parameters, sd_varl if (manager_find(m, tt, p.remote)) return sd_varlink_errorbo(link, "io.systemd.Import.AlreadyInProgress", SD_JSON_BUILD_PAIR_STRING("remote", p.remote)); - r = varlink_verify_polkit_async( - link, - m->bus, - "org.freedesktop.import1.pull", - (const char**) STRV_MAKE( - "remote", p.remote, - "local", p.local, - "class", image_class_to_string(p.class), - "type", import_type_to_string(p.type), - "verify", import_verify_to_string(p.verify)), - &m->polkit_registry); - if (r <= 0) - return r; + if (m->runtime_scope != RUNTIME_SCOPE_USER) { + r = varlink_verify_polkit_async( + link, + m->system_bus, + "org.freedesktop.import1.pull", + (const char**) STRV_MAKE( + "remote", p.remote, + "local", p.local, + "class", image_class_to_string(p.class), + "type", import_type_to_string(p.type), + "verify", import_verify_to_string(p.verify)), + &m->polkit_registry); + if (r <= 0) + return r; + } _cleanup_(transfer_unrefp) Transfer *t = NULL; @@ -1938,9 +1969,11 @@ static int manager_connect_varlink(Manager *m) { assert(m->event); assert(!m->varlink_server); - r = varlink_server_new(&m->varlink_server, - SD_VARLINK_SERVER_ACCOUNT_UID|SD_VARLINK_SERVER_INHERIT_USERDATA, - m); + r = varlink_server_new( + &m->varlink_server, + (m->runtime_scope != RUNTIME_SCOPE_USER ? SD_VARLINK_SERVER_ACCOUNT_UID : 0)| + SD_VARLINK_SERVER_INHERIT_USERDATA, + m); if (r < 0) return log_error_errno(r, "Failed to allocate varlink server object: %m"); @@ -1969,7 +2002,12 @@ static int manager_connect_varlink(Manager *m) { if (r < 0) return log_error_errno(r, "Failed to bind to passed Varlink sockets: %m"); if (r == 0) { - r = sd_varlink_server_listen_address(m->varlink_server, "/run/systemd/io.systemd.Import", 0666); + _cleanup_free_ char *socket_path = NULL; + r = runtime_directory_generic(m->runtime_scope, "systemd/io.systemd.Import", &socket_path); + if (r < 0) + return log_error_errno(r, "Failed to determine socket path: %m"); + + r = sd_varlink_server_listen_address(m->varlink_server, socket_path, runtime_scope_to_socket_mode(m->runtime_scope) | SD_VARLINK_SERVER_MODE_MKDIR_0755); if (r < 0) return log_error_errno(r, "Failed to bind to Varlink socket: %m"); } @@ -2009,6 +2047,7 @@ static void manager_parse_env(Manager *m) { static int run(int argc, char *argv[]) { _cleanup_(manager_unrefp) Manager *m = NULL; + RuntimeScope scope = RUNTIME_SCOPE_SYSTEM; int r; log_setup(); @@ -2017,7 +2056,7 @@ static int run(int argc, char *argv[]) { "VM and container image import and export service.", BUS_IMPLEMENTATIONS(&manager_object, &log_control_object), - /* runtime_scope= */ NULL, + &scope, argc, argv); if (r <= 0) return r; @@ -2026,7 +2065,7 @@ static int run(int argc, char *argv[]) { assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD) >= 0); - r = manager_new(&m); + r = manager_new(scope, &m); if (r < 0) return log_error_errno(r, "Failed to allocate manager object: %m"); @@ -2046,7 +2085,7 @@ static int run(int argc, char *argv[]) { r = bus_event_loop_with_idle( m->event, - m->bus, + m->api_bus, "org.freedesktop.import1", DEFAULT_EXIT_USEC, manager_check_idle, diff --git a/src/import/meson.build b/src/import/meson.build index 4aec57373a..3a50d13b0c 100644 --- a/src/import/meson.build +++ b/src/import/meson.build @@ -111,6 +111,9 @@ install_data('org.freedesktop.import1.conf', install_dir : dbuspolicydir) install_data('org.freedesktop.import1.service', install_dir : dbussystemservicedir) +install_data('org.freedesktop.import1.service-for-session', + install_dir : dbussessionservicedir, + rename : 'org.freedesktop.import1.service') install_data('org.freedesktop.import1.policy', install_dir : polkitpolicydir) diff --git a/src/import/org.freedesktop.import1.service-for-session b/src/import/org.freedesktop.import1.service-for-session new file mode 100644 index 0000000000..6e6c4a4c94 --- /dev/null +++ b/src/import/org.freedesktop.import1.service-for-session @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[D-BUS Service] +Name=org.freedesktop.import1 +Exec=/bin/false +SystemdService=dbus-org.freedesktop.import1.service diff --git a/units/user/meson.build b/units/user/meson.build index 8ad3f0eb76..71d2425038 100644 --- a/units/user/meson.build +++ b/units/user/meson.build @@ -28,6 +28,16 @@ units = [ 'conditions' : ['ENABLE_MACHINED'], 'symlinks' : ['sockets.target.wants/'], }, + { + 'file' : 'systemd-importd.service.in', + 'conditions' : ['ENABLE_IMPORTD'], + 'symlinks' : ['dbus-org.freedesktop.import1.service'], + }, + { + 'file' : 'systemd-importd.socket', + 'conditions' : ['ENABLE_IMPORTD'], + 'symlinks' : ['sockets.target.wants/'], + }, { 'file' : 'paths.target' }, { 'file' : 'printer.target' }, { 'file' : 'session.slice' }, diff --git a/units/user/systemd-importd.service.in b/units/user/systemd-importd.service.in new file mode 100644 index 0000000000..420fb06105 --- /dev/null +++ b/units/user/systemd-importd.service.in @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Disk Image Download Service +Documentation=man:systemd-importd.service(8) +Documentation=man:org.freedesktop.import1(5) + +[Service] +Type=notify +ExecStart={{LIBEXECDIR}}/systemd-importd --user +BusName=org.freedesktop.import1 +KillMode=mixed +NoNewPrivileges=yes +MemoryDenyWriteExecute=yes +RestrictRealtime=yes +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 +SystemCallFilter=@system-service @mount +SystemCallErrorNumber=EPERM +SystemCallArchitectures=native +LockPersonality=yes +{{SERVICE_WATCHDOG}} diff --git a/units/user/systemd-importd.socket b/units/user/systemd-importd.socket new file mode 100644 index 0000000000..d266d927f8 --- /dev/null +++ b/units/user/systemd-importd.socket @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Disk Image Download Service Socket +Documentation=man:systemd-importd.service(8) +Documentation=man:org.freedesktop.import1(5) + +[Socket] +ListenStream=%t/systemd/io.systemd.Import +FileDescriptorName=varlink +SocketMode=0600 From 0960b905bc849ff52cae26acc9cadbd33a0c35ec Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 25 Jul 2025 08:29:32 +0200 Subject: [PATCH 07/10] importctl: add --user/--system switches --- src/import/importctl.c | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/import/importctl.c b/src/import/importctl.c index 94140469c4..731c311ffe 100644 --- a/src/import/importctl.c +++ b/src/import/importctl.c @@ -45,6 +45,7 @@ static ImportVerify arg_verify = IMPORT_VERIFY_SIGNATURE; static const char* arg_format = NULL; static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF; static ImageClass arg_image_class = _IMAGE_CLASS_INVALID; +static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; #define PROGRESS_PREFIX "Total:" @@ -1014,6 +1015,8 @@ static int help(int argc, char *argv[], void *userdata) { " --no-ask-password Do not ask for system passwords\n" " -H --host=[USER@]HOST Operate on remote host\n" " -M --machine=CONTAINER Operate on local container\n" + " --system Connect to system machine manager\n" + " --user Connect to user machine manager\n" " --read-only Create read-only image\n" " -q --quiet Suppress output\n" " --json=pretty|short|off Generate JSON output\n" @@ -1055,6 +1058,8 @@ static int parse_argv(int argc, char *argv[]) { ARG_FORMAT, ARG_CLASS, ARG_KEEP_DOWNLOAD, + ARG_SYSTEM, + ARG_USER, }; static const struct option options[] = { @@ -1073,6 +1078,8 @@ static int parse_argv(int argc, char *argv[]) { { "format", required_argument, NULL, ARG_FORMAT }, { "class", required_argument, NULL, ARG_CLASS }, { "keep-download", required_argument, NULL, ARG_KEEP_DOWNLOAD }, + { "system", no_argument, NULL, ARG_SYSTEM }, + { "user", no_argument, NULL, ARG_USER }, {} }; @@ -1198,6 +1205,14 @@ static int parse_argv(int argc, char *argv[]) { arg_import_flags_mask |= IMPORT_PULL_KEEP_DOWNLOAD; break; + case ARG_USER: + arg_runtime_scope = RUNTIME_SCOPE_USER; + break; + + case ARG_SYSTEM: + arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; + break; + case '?': return -EINVAL; @@ -1240,9 +1255,9 @@ static int run(int argc, char *argv[]) { if (r <= 0) return r; - r = bus_connect_transport(arg_transport, arg_host, RUNTIME_SCOPE_SYSTEM, &bus); + r = bus_connect_transport(arg_transport, arg_host, arg_runtime_scope, &bus); if (r < 0) - return bus_log_connect_error(r, arg_transport, RUNTIME_SCOPE_SYSTEM); + return bus_log_connect_error(r, arg_transport, arg_runtime_scope); (void) sd_bus_set_allow_interactive_authorization(bus, arg_ask_password); From d9c10bf1d2a46d4d4bafb3eef25048d414573824 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 18 Aug 2025 16:45:16 +0200 Subject: [PATCH 08/10] importd: clean up how we determine image root in importd backends Let's introduce a single helper that determines where to download images to, taking all three primary parameters into account: the image class, the runtime scope and whether to do runtime or persistency. Then port everything over to this. This not only cleans things up, but makes sure the importd backends actually properly can deal with per-user downloads, as before we never took the runtime scope into account for determining download location. --- src/import/import-fs.c | 16 +++++++++--- src/import/import-generator.c | 8 ++++-- src/import/import.c | 16 +++++++++--- src/import/importctl.c | 12 +++++++-- src/import/pull.c | 15 +++++++++--- src/shared/discover-image.c | 46 +++++++++++++++++++++++++++++++++-- src/shared/discover-image.h | 3 +-- 7 files changed, 96 insertions(+), 20 deletions(-) diff --git a/src/import/import-fs.c b/src/import/import-fs.c index 342c024d56..537fe46853 100644 --- a/src/import/import-fs.c +++ b/src/import/import-fs.c @@ -35,10 +35,12 @@ static bool arg_btrfs_subvol = true; static bool arg_btrfs_quota = true; static bool arg_sync = true; static bool arg_direct = false; -static const char *arg_image_root = NULL; +static char *arg_image_root = NULL; static ImageClass arg_class = IMAGE_MACHINE; static RuntimeScope arg_runtime_scope = _RUNTIME_SCOPE_INVALID; +STATIC_DESTRUCTOR_REGISTER(arg_image_root, freep); + typedef struct ProgressInfo { RateLimit limit; char *path; @@ -344,7 +346,10 @@ static int parse_argv(int argc, char *argv[]) { break; case ARG_IMAGE_ROOT: - arg_image_root = optarg; + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image_root); + if (r < 0) + return r; + break; case ARG_READ_ONLY: @@ -398,8 +403,11 @@ static int parse_argv(int argc, char *argv[]) { assert_not_reached(); } - if (!arg_image_root) - arg_image_root = image_root_to_string(arg_class); + if (!arg_image_root) { + r = image_root_pick(arg_runtime_scope < 0 ? RUNTIME_SCOPE_SYSTEM : arg_runtime_scope, arg_class, /* runtime= */ false, &arg_image_root); + if (r < 0) + return log_error_errno(r, "Failed to pick image root: %m"); + } return 1; } diff --git a/src/import/import-generator.c b/src/import/import-generator.c index bf5f41a747..4aab1618d3 100644 --- a/src/import/import-generator.c +++ b/src/import/import-generator.c @@ -17,6 +17,7 @@ #include "parse-util.h" #include "path-util.h" #include "proc-cmdline.h" +#include "runtime-scope.h" #include "specifier.h" #include "string-util.h" #include "unit-name.h" @@ -200,7 +201,10 @@ static int parse_pull_expression(const char *v) { if (!GREEDY_REALLOC(arg_transfers, arg_n_transfers + 1)) return log_oom(); - const char *image_root = runtime ? image_root_runtime_to_string(class) : image_root_to_string(class); + _cleanup_free_ char *image_root = NULL; + r = image_root_pick(RUNTIME_SCOPE_SYSTEM, class, runtime, &image_root); + if (r < 0) + return log_error_errno(r, "Failed to pick image root: %m"); _cleanup_(sd_json_variant_unrefp) sd_json_variant *j = NULL; r = sd_json_buildo( @@ -220,7 +224,7 @@ static int parse_pull_expression(const char *v) { .type = type, .local = TAKE_PTR(local), .remote = TAKE_PTR(remote), - .image_root = image_root, + .image_root = TAKE_PTR(image_root), .json = TAKE_PTR(j), .blockdev = blockdev, }; diff --git a/src/import/import.c b/src/import/import.c index f41a42f7e6..531c082077 100644 --- a/src/import/import.c +++ b/src/import/import.c @@ -26,12 +26,14 @@ #include "string-util.h" #include "verbs.h" -static const char *arg_image_root = NULL; +static char *arg_image_root = NULL; static ImportFlags arg_import_flags = IMPORT_BTRFS_SUBVOL | IMPORT_BTRFS_QUOTA | IMPORT_CONVERT_QCOW2 | IMPORT_SYNC; static uint64_t arg_offset = UINT64_MAX, arg_size_max = UINT64_MAX; static ImageClass arg_class = IMAGE_MACHINE; static RuntimeScope arg_runtime_scope = _RUNTIME_SCOPE_INVALID; +STATIC_DESTRUCTOR_REGISTER(arg_image_root, freep); + static int normalize_local(const char *local, char **ret) { _cleanup_free_ char *ll = NULL; int r; @@ -360,7 +362,10 @@ static int parse_argv(int argc, char *argv[]) { break; case ARG_IMAGE_ROOT: - arg_image_root = optarg; + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image_root); + if (r < 0) + return r; + break; case ARG_READ_ONLY: @@ -460,8 +465,11 @@ static int parse_argv(int argc, char *argv[]) { if (arg_offset != UINT64_MAX && !FLAGS_SET(arg_import_flags, IMPORT_DIRECT)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "File offset only supported in --direct mode."); - if (!arg_image_root) - arg_image_root = image_root_to_string(arg_class); + if (!arg_image_root) { + r = image_root_pick(arg_runtime_scope < 0 ? RUNTIME_SCOPE_SYSTEM : arg_runtime_scope, arg_class, /* runtime= */ false, &arg_image_root); + if (r < 0) + return log_error_errno(r, "Failed to pick image root: %m"); + } return 1; } diff --git a/src/import/importctl.c b/src/import/importctl.c index 731c311ffe..d131943413 100644 --- a/src/import/importctl.c +++ b/src/import/importctl.c @@ -50,15 +50,23 @@ static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; #define PROGRESS_PREFIX "Total:" static int settle_image_class(void) { + int r; if (arg_image_class < 0) { _cleanup_free_ char *j = NULL; - for (ImageClass class = 0; class < _IMAGE_CLASS_MAX; class++) + for (ImageClass class = 0; class < _IMAGE_CLASS_MAX; class++) { + _cleanup_free_ char *root = NULL; + + r = image_root_pick(arg_runtime_scope, class, /* runtime= */ false, &root); + if (r < 0) + return log_error_errno(r, "Failed to pick image root: %m"); + if (strextendf_with_separator(&j, ", ", "%s (downloads to %s/)", image_class_to_string(class), - image_root_to_string(class)) < 0) + root) < 0) return log_oom(); + } return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No image class specified, retry with --class= set to one of: %s.", j); diff --git a/src/import/pull.c b/src/import/pull.c index 9c915536d8..a41b4b0155 100644 --- a/src/import/pull.c +++ b/src/import/pull.c @@ -28,7 +28,7 @@ #include "verbs.h" #include "web-util.h" -static const char *arg_image_root = NULL; +static char *arg_image_root = NULL; static ImportVerify arg_verify = IMPORT_VERIFY_SIGNATURE; static ImportFlags arg_import_flags = IMPORT_PULL_SETTINGS | IMPORT_PULL_ROOTHASH | IMPORT_PULL_ROOTHASH_SIGNATURE | IMPORT_PULL_VERITY | IMPORT_BTRFS_SUBVOL | IMPORT_BTRFS_QUOTA | IMPORT_CONVERT_QCOW2 | IMPORT_SYNC; static uint64_t arg_offset = UINT64_MAX, arg_size_max = UINT64_MAX; @@ -37,6 +37,7 @@ static ImageClass arg_class = IMAGE_MACHINE; static RuntimeScope arg_runtime_scope = _RUNTIME_SCOPE_INVALID; STATIC_DESTRUCTOR_REGISTER(arg_checksum, freep); +STATIC_DESTRUCTOR_REGISTER(arg_image_root, freep); static int normalize_local(const char *local, const char *url, char **ret) { _cleanup_free_ char *ll = NULL; @@ -358,7 +359,10 @@ static int parse_argv(int argc, char *argv[]) { break; case ARG_IMAGE_ROOT: - arg_image_root = optarg; + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image_root); + if (r < 0) + return r; + break; case ARG_VERIFY: { @@ -541,8 +545,11 @@ static int parse_argv(int argc, char *argv[]) { if (arg_checksum && (arg_import_flags & (IMPORT_PULL_SETTINGS|IMPORT_PULL_ROOTHASH|IMPORT_PULL_ROOTHASH_SIGNATURE|IMPORT_PULL_VERITY)) != 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Literal checksum verification only supported if no associated files are downloaded."); - if (!arg_image_root) - arg_image_root = image_root_to_string(arg_class); + if (!arg_image_root) { + r = image_root_pick(arg_runtime_scope < 0 ? RUNTIME_SCOPE_SYSTEM : arg_runtime_scope, arg_class, /* runtime= */ false, &arg_image_root); + if (r < 0) + return log_error_errno(r, "Failed to pick image root: %m"); + } /* .nspawn settings files only really make sense for machine images, not for sysext/confext/portable */ if (auto_settings && arg_class != IMAGE_MACHINE) diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index 15875968b7..53938a1256 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -110,7 +110,7 @@ static const char *const image_root_table[_IMAGE_CLASS_MAX] = { [IMAGE_CONFEXT] = "/var/lib/confexts", }; -DEFINE_STRING_TABLE_LOOKUP_TO_STRING(image_root, ImageClass); +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(image_root, ImageClass); static const char *const image_root_runtime_table[_IMAGE_CLASS_MAX] = { [IMAGE_MACHINE] = "/run/machines", @@ -119,7 +119,7 @@ static const char *const image_root_runtime_table[_IMAGE_CLASS_MAX] = { [IMAGE_CONFEXT] = "/run/confexts", }; -DEFINE_STRING_TABLE_LOOKUP_TO_STRING(image_root_runtime, ImageClass); +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(image_root_runtime, ImageClass); static const char *const image_dirname_table[_IMAGE_CLASS_MAX] = { [IMAGE_MACHINE] = "machines", @@ -2022,3 +2022,45 @@ static const char* const image_type_table[_IMAGE_TYPE_MAX] = { }; DEFINE_STRING_TABLE_LOOKUP(image_type, ImageType); + +int image_root_pick( + RuntimeScope scope, + ImageClass c, + bool runtime, + char **ret) { + + int r; + + assert(scope >= 0); + assert(scope < _RUNTIME_SCOPE_MAX); + assert(c >= 0); + assert(c < _IMAGE_CLASS_MAX); + assert(ret); + + /* Picks the primary target directory for downloads, depending on invocation contexts */ + + _cleanup_free_ char *s = NULL; + switch (scope) { + + case RUNTIME_SCOPE_SYSTEM: { + s = strdup(runtime ? image_root_runtime_to_string(c) : image_root_to_string(c)); + if (!s) + return -ENOMEM; + + break; + } + + case RUNTIME_SCOPE_USER: + r = sd_path_lookup(runtime ? SD_PATH_USER_RUNTIME : SD_PATH_USER_STATE_PRIVATE, "machines", &s); + if (r < 0) + return r; + + break; + + default: + return -EOPNOTSUPP; + } + + *ret = TAKE_PTR(s); + return 0; +} diff --git a/src/shared/discover-image.h b/src/shared/discover-image.h index 7ba44bfb79..a7370450a8 100644 --- a/src/shared/discover-image.h +++ b/src/shared/discover-image.h @@ -95,8 +95,7 @@ bool image_is_host(const struct Image *i); int image_to_json(const struct Image *i, sd_json_variant **ret); -const char* image_root_to_string(ImageClass c) _const_; -const char* image_root_runtime_to_string(ImageClass c) _const_; +int image_root_pick(RuntimeScope scope, ImageClass c, bool runtime, char **ret); extern const struct hash_ops image_hash_ops; From 1be8caa6be6f5a10a7dea5ac562a0df5c5fac2e9 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 18 Aug 2025 23:18:18 +0200 Subject: [PATCH 09/10] importd: support unpacking tarballs to foreign UID range When invoked unprivileged, let's use a transiently allocated userns, so that we can properly untar UIDs/GIDs so that the trees appear owned by the foreign UID/GID range. --- src/import/import-common.c | 161 ++++++++++++++++++++++++++++++++++++- src/import/import-common.h | 20 +++-- src/import/import-tar.c | 64 ++++++++++----- src/import/import.c | 3 + src/import/pull-tar.c | 148 +++++++++++++++++++++++++--------- src/import/pull.c | 3 + src/shared/dissect-image.c | 111 +++++++++++++++++++++++-- src/shared/dissect-image.h | 4 + 8 files changed, 439 insertions(+), 75 deletions(-) diff --git a/src/import/import-common.c b/src/import/import-common.c index 31bc7c95df..d8fd6e3982 100644 --- a/src/import/import-common.c +++ b/src/import/import-common.c @@ -6,21 +6,26 @@ #include "sd-event.h" #include "capability-util.h" +#include "copy.h" #include "dirent-util.h" +#include "dissect-image.h" #include "fd-util.h" #include "fs-util.h" #include "import-common.h" #include "libarchive-util.h" #include "log.h" +#include "namespace-util.h" +#include "nsresource.h" #include "os-util.h" #include "pidref.h" #include "process-util.h" +#include "rm-rf.h" #include "selinux-util.h" #include "stat-util.h" #include "tar-util.h" #include "tmpfile-util.h" -int import_fork_tar_x(int tree_fd, PidRef *ret_pid) { +int import_fork_tar_x(int tree_fd, int userns_fd, PidRef *ret_pid) { int r; assert(tree_fd >= 0); @@ -41,7 +46,7 @@ int import_fork_tar_x(int tree_fd, PidRef *ret_pid) { r = pidref_safe_fork_full( "tar-x", /* stdio_fds= */ NULL, - (int[]) { tree_fd, pipefd[0] }, 2, + (int[]) { tree_fd, pipefd[0], userns_fd }, userns_fd >= 0 ? 3 : 2, FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_REOPEN_LOG, ret_pid); if (r < 0) @@ -58,6 +63,14 @@ int import_fork_tar_x(int tree_fd, PidRef *ret_pid) { /* Child */ + if (userns_fd >= 0) { + r = detach_mount_namespace_userns(userns_fd); + if (r < 0) { + log_error_errno(r, "Failed to join user namespace: %m"); + _exit(EXIT_FAILURE); + } + } + if (unshare(CLONE_NEWNET) < 0) log_warning_errno(errno, "Failed to lock tar into network namespace, ignoring: %m"); @@ -291,3 +304,147 @@ int import_allocate_event_with_signals(sd_event **ret) { *ret = TAKE_PTR(event); return 0; } + +int import_make_foreign_userns(int *userns_fd) { + assert(userns_fd); + + if (*userns_fd >= 0) + return 0; + + *userns_fd = nsresource_allocate_userns(/* name= */ NULL, NSRESOURCE_UIDS_64K); /* allocate 64K users */ + if (*userns_fd < 0) + return log_error_errno(*userns_fd, "Failed to allocate transient user namespace: %m"); + + return 1; +} + +int import_copy_foreign( + int source_fd, + int target_fd, + int *userns_fd) { + + int r; + + assert(source_fd >= 0); + assert(target_fd >= 0); + assert(userns_fd); + + /* Copies dir referenced by source_fd into dir referenced by source_fd, moves to the specified userns + * for that (allocated if needed), which should be foreign UID range */ + + r = import_make_foreign_userns(userns_fd); + if (r < 0) + return r; + + r = safe_fork_full( + "copy-tree", + /* stdio_fds= */ NULL, + (int[]) { *userns_fd, source_fd, target_fd }, 3, + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_REOPEN_LOG|FORK_WAIT, + /* ret_pid= */ NULL); + if (r < 0) + return r; + if (r == 0) { + r = namespace_enter( + /* pidns_fd= */ -EBADF, + /* mntns_fd= */ -EBADF, + /* netns_fd= */ -EBADF, + *userns_fd, + /* root_fd= */ -EBADF); + if (r < 0) { + log_error_errno(r, "Failed to join user namespace: %m"); + _exit(EXIT_FAILURE); + } + + r = copy_tree_at( + source_fd, /* from= */ NULL, + target_fd, /* to = */ NULL, + /* override_uid= */ UID_INVALID, + /* override_gid= */ GID_INVALID, + COPY_REFLINK|COPY_HARDLINKS|COPY_MERGE_EMPTY|COPY_MERGE_APPLY_STAT|COPY_SAME_MOUNT|COPY_ALL_XATTRS, + /* denylist= */ NULL, + /* subvolumes= */ NULL); + if (r < 0) { + log_error_errno(r, "Failed to copy tree: %m"); + _exit(EXIT_FAILURE); + } + + _exit(EXIT_SUCCESS); + } + + return 0; +} + +int import_remove_tree_foreign(const char *path, int *userns_fd) { + int r; + + assert(path); + assert(userns_fd); + + r = import_make_foreign_userns(userns_fd); + if (r < 0) + return r; + + _cleanup_close_ int tree_fd = -EBADF; + r = mountfsd_mount_directory( + path, + *userns_fd, + DISSECT_IMAGE_FOREIGN_UID, + &tree_fd); + if (r < 0) + return r; + + r = safe_fork_full( + "rm-tree", + /* stdio_fds= */ NULL, + (int[]) { *userns_fd, tree_fd }, 2, + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_REOPEN_LOG|FORK_WAIT, + /* ret_pid= */ NULL); + if (r < 0) + return r; + if (r == 0) { + /* child */ + + r = namespace_enter( + /* pidns_fd= */ -EBADF, + /* mntns_fd= */ -EBADF, + /* netns_fd= */ -EBADF, + *userns_fd, + /* root_fd= */ -EBADF); + if (r < 0) { + log_error_errno(r, "Failed to join user namespace: %m"); + _exit(EXIT_FAILURE); + } + + _cleanup_close_ int dfd = fd_reopen(tree_fd, O_DIRECTORY|O_CLOEXEC); + if (dfd < 0) { + log_error_errno(r, "Failed to reopen tree fd: %m"); + _exit(EXIT_FAILURE); + } + + r = rm_rf_children(dfd, REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_CHMOD, /* root_dev= */ NULL); + if (r < 0) + log_warning_errno(r, "Failed to empty '%s' directory in foreign UID mode, ignoring: %m", path); + + _exit(EXIT_SUCCESS); + } + + return 0; +} + +int import_remove_tree(const char *path, int *userns_fd, ImportFlags flags) { + int r; + + assert(path); + assert(userns_fd); + + /* Try the userns dance first, to remove foreign UID range owned trees */ + if (FLAGS_SET(flags, IMPORT_FOREIGN_UID)) + (void) import_remove_tree_foreign(path, userns_fd); + + r = rm_rf(path, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_MISSING_OK|REMOVE_CHMOD); + if (r < 0) + return log_error_errno(r, "Failed to remove '%s': %m", path); + + return 0; +} diff --git a/src/import/import-common.h b/src/import/import-common.h index 55d00ea466..6c6ac655b0 100644 --- a/src/import/import-common.h +++ b/src/import/import-common.h @@ -15,15 +15,16 @@ typedef enum ImportFlags { IMPORT_CONVERT_QCOW2 = 1 << 5, /* raw: if we detect a qcow2 image, unpack it */ IMPORT_DIRECT = 1 << 6, /* import without rename games */ IMPORT_SYNC = 1 << 7, /* fsync() right before we are done */ + IMPORT_FOREIGN_UID = 1 << 8, /* tar: go via nsresourced/mountfsd and make owned by foreign UID */ /* When pulling these flags are defined too */ - IMPORT_PULL_SETTINGS = 1 << 8, /* download .nspawn settings file */ - IMPORT_PULL_ROOTHASH = 1 << 9, /* only for raw: download .roothash file for verity */ - IMPORT_PULL_ROOTHASH_SIGNATURE = 1 << 10, /* only for raw: download .roothash.p7s file for verity */ - IMPORT_PULL_VERITY = 1 << 11, /* only for raw: download .verity file for verity */ + IMPORT_PULL_SETTINGS = 1 << 9, /* download .nspawn settings file */ + IMPORT_PULL_ROOTHASH = 1 << 10, /* only for raw: download .roothash file for verity */ + IMPORT_PULL_ROOTHASH_SIGNATURE = 1 << 11, /* only for raw: download .roothash.p7s file for verity */ + IMPORT_PULL_VERITY = 1 << 12, /* only for raw: download .verity file for verity */ /* The supported flags for the tar and the raw importing */ - IMPORT_FLAGS_MASK_TAR = IMPORT_FORCE|IMPORT_READ_ONLY|IMPORT_BTRFS_SUBVOL|IMPORT_BTRFS_QUOTA|IMPORT_DIRECT|IMPORT_SYNC, + IMPORT_FLAGS_MASK_TAR = IMPORT_FORCE|IMPORT_READ_ONLY|IMPORT_BTRFS_SUBVOL|IMPORT_BTRFS_QUOTA|IMPORT_DIRECT|IMPORT_SYNC|IMPORT_FOREIGN_UID, IMPORT_FLAGS_MASK_RAW = IMPORT_FORCE|IMPORT_READ_ONLY|IMPORT_CONVERT_QCOW2|IMPORT_DIRECT|IMPORT_SYNC, /* The supported flags for the tar and the raw pulling */ @@ -34,7 +35,7 @@ typedef enum ImportFlags { } ImportFlags; int import_fork_tar_c(const char *path, PidRef *ret); -int import_fork_tar_x(int tree_fd, PidRef *ret_pid); +int import_fork_tar_x(int tree_fd, int userns_fd, PidRef *ret_pid); int import_mangle_os_tree(const char *path); @@ -42,4 +43,11 @@ bool import_validate_local(const char *name, ImportFlags flags); int import_allocate_event_with_signals(sd_event **ret); +int import_make_foreign_userns(int *userns_fd); + +int import_copy_foreign(int source_fd, int target_fd, int *userns_fd); + +int import_remove_tree_foreign(const char *path, int *userns_fd); +int import_remove_tree(const char *path, int *userns_fd, ImportFlags flags); + #define IMPORT_BUFFER_SIZE (128U*1024U) diff --git a/src/import/import-tar.c b/src/import/import-tar.c index 7e5499b2b9..85fbf5abd1 100644 --- a/src/import/import-tar.c +++ b/src/import/import-tar.c @@ -7,6 +7,7 @@ #include "alloc-util.h" #include "btrfs-util.h" +#include "dissect-image.h" #include "errno-util.h" #include "fd-util.h" #include "format-util.h" @@ -46,6 +47,7 @@ typedef struct TarImport { int input_fd; int tar_fd; int tree_fd; + int userns_fd; ImportCompress compress; @@ -73,7 +75,10 @@ TarImport* tar_import_unref(TarImport *i) { pidref_done_sigkill_wait(&i->tar_pid); - rm_rf_subvolume_and_free(i->temp_path); + if (i->temp_path) { + import_remove_tree(i->temp_path, &i->userns_fd, i->flags); + free(i->temp_path); + } import_compress_free(&i->compress); @@ -81,6 +86,7 @@ TarImport* tar_import_unref(TarImport *i) { safe_close(i->tar_fd); safe_close(i->tree_fd); + safe_close(i->userns_fd); free(i->final_path); free(i->image_root); @@ -114,6 +120,7 @@ int tar_import_new( .input_fd = -EBADF, .tar_fd = -EBADF, .tree_fd = -EBADF, + .userns_fd = -EBADF, .on_finished = on_finished, .userdata = userdata, .last_percent = UINT_MAX, @@ -200,8 +207,8 @@ static int tar_import_finish(TarImport *i) { AT_FDCWD, d, AT_FDCWD, i->final_path, (i->flags & IMPORT_FORCE ? INSTALL_REPLACE : 0) | - (i->flags & IMPORT_READ_ONLY ? INSTALL_READ_ONLY : 0) | - (i->flags & IMPORT_SYNC ? INSTALL_SYNCFS : 0)); + (i->flags & IMPORT_READ_ONLY ? INSTALL_READ_ONLY|INSTALL_GRACEFUL : 0) | + (i->flags & IMPORT_SYNC ? INSTALL_SYNCFS|INSTALL_GRACEFUL : 0)); if (r < 0) return log_error_errno(r, "Failed to move '%s' into place: %m", i->final_path ?: i->local); @@ -244,26 +251,41 @@ static int tar_import_fork_tar(TarImport *i) { if (FLAGS_SET(i->flags, IMPORT_DIRECT|IMPORT_FORCE)) (void) rm_rf(d, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME); - if (i->flags & IMPORT_BTRFS_SUBVOL) - r = btrfs_subvol_make_fallback(AT_FDCWD, d, 0755); - else - r = RET_NERRNO(mkdir(d, 0755)); - if (r == -EEXIST && (i->flags & IMPORT_DIRECT)) /* EEXIST is OK if in direct mode, but not otherwise, - * because in that case our temporary path collided */ - r = 0; - if (r < 0) - return log_error_errno(r, "Failed to create directory/subvolume %s: %m", d); - if (r > 0 && (i->flags & IMPORT_BTRFS_QUOTA)) { /* actually btrfs subvol */ - if (!(i->flags & IMPORT_DIRECT)) - (void) import_assign_pool_quota_and_warn(root); - (void) import_assign_pool_quota_and_warn(d); + if (FLAGS_SET(i->flags, IMPORT_FOREIGN_UID)) { + r = import_make_foreign_userns(&i->userns_fd); + if (r < 0) + return r; + + _cleanup_close_ int directory_fd = -EBADF; + r = mountfsd_make_directory(d, /* flags= */ 0, &directory_fd); + if (r < 0) + return r; + + r = mountfsd_mount_directory_fd(directory_fd, i->userns_fd, DISSECT_IMAGE_FOREIGN_UID, &i->tree_fd); + if (r < 0) + return r; + } else { + if (i->flags & IMPORT_BTRFS_SUBVOL) + r = btrfs_subvol_make_fallback(AT_FDCWD, d, 0755); + else + r = RET_NERRNO(mkdir(d, 0755)); + if (r == -EEXIST && (i->flags & IMPORT_DIRECT)) /* EEXIST is OK if in direct mode, but not otherwise, + * because in that case our temporary path collided */ + r = 0; + if (r < 0) + return log_error_errno(r, "Failed to create directory/subvolume %s: %m", d); + if (r > 0 && (i->flags & IMPORT_BTRFS_QUOTA)) { /* actually btrfs subvol */ + if (!(i->flags & IMPORT_DIRECT)) + (void) import_assign_pool_quota_and_warn(root); + (void) import_assign_pool_quota_and_warn(d); + } + + i->tree_fd = open(d, O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW); + if (i->tree_fd < 0) + return log_error_errno(errno, "Failed to open '%s': %m", d); } - i->tree_fd = open(d, O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW); - if (i->tree_fd < 0) - return log_error_errno(errno, "Failed to open '%s': %m", d); - - i->tar_fd = import_fork_tar_x(i->tree_fd, &i->tar_pid); + i->tar_fd = import_fork_tar_x(i->tree_fd, i->userns_fd, &i->tar_pid); if (i->tar_fd < 0) return i->tar_fd; diff --git a/src/import/import.c b/src/import/import.c index 531c082077..5276f26977 100644 --- a/src/import/import.c +++ b/src/import/import.c @@ -471,6 +471,9 @@ static int parse_argv(int argc, char *argv[]) { return log_error_errno(r, "Failed to pick image root: %m"); } + if (arg_runtime_scope == RUNTIME_SCOPE_USER) + arg_import_flags |= IMPORT_FOREIGN_UID; + return 1; } diff --git a/src/import/pull-tar.c b/src/import/pull-tar.c index fa44216f8c..cf9e2060da 100644 --- a/src/import/pull-tar.c +++ b/src/import/pull-tar.c @@ -9,11 +9,10 @@ #include "btrfs-util.h" #include "copy.h" #include "curl-util.h" +#include "dissect-image.h" #include "errno-util.h" #include "fd-util.h" #include "fs-util.h" -#include "import-common.h" -#include "import-util.h" #include "install-file.h" #include "log.h" #include "mkdir-label.h" @@ -26,6 +25,7 @@ #include "rm-rf.h" #include "string-util.h" #include "tmpfile-util.h" +#include "uid-classification.h" #include "web-util.h" typedef enum TarProgress { @@ -64,6 +64,7 @@ typedef struct TarPull { char *checksum; int tree_fd; + int userns_fd; } TarPull; TarPull* tar_pull_unref(TarPull *i) { @@ -80,7 +81,10 @@ TarPull* tar_pull_unref(TarPull *i) { curl_glue_unref(i->glue); sd_event_unref(i->event); - rm_rf_subvolume_and_free(i->temp_path); + if (i->temp_path) { + import_remove_tree(i->temp_path, &i->userns_fd, i->flags); + free(i->temp_path); + } unlink_and_free(i->settings_temp_path); free(i->final_path); @@ -90,6 +94,7 @@ TarPull* tar_pull_unref(TarPull *i) { free(i->checksum); safe_close(i->tree_fd); + safe_close(i->userns_fd); return mfree(i); } @@ -138,6 +143,7 @@ int tar_pull_new( .glue = TAKE_PTR(g), .tar_pid = PIDREF_NULL, .tree_fd = -EBADF, + .userns_fd = -EBADF, }; i->glue->on_finished = pull_job_curl_on_finished; @@ -233,6 +239,9 @@ static int tar_pull_make_local_copy(TarPull *i) { if (!i->local) return 0; + /* Creates a copy/clone of the original downloaded version (which is supposed to remain untouched) + * under a local image name (which may then be modified) */ + assert(i->final_path); p = path_join(i->image_root, i->local); @@ -244,18 +253,61 @@ static int tar_pull_make_local_copy(TarPull *i) { if (r < 0) return log_error_errno(r, "Failed to generate temporary filename for %s: %m", p); - if (i->flags & IMPORT_BTRFS_SUBVOL) - r = btrfs_subvol_snapshot_at( - AT_FDCWD, i->final_path, - AT_FDCWD, t, - (i->flags & IMPORT_BTRFS_QUOTA ? BTRFS_SNAPSHOT_QUOTA : 0)| - BTRFS_SNAPSHOT_FALLBACK_COPY| - BTRFS_SNAPSHOT_FALLBACK_DIRECTORY| - BTRFS_SNAPSHOT_RECURSIVE); - else - r = copy_tree(i->final_path, t, UID_INVALID, GID_INVALID, COPY_REFLINK|COPY_HARDLINKS, NULL, NULL); - if (r < 0) - return log_error_errno(r, "Failed to create local image: %m"); + if (FLAGS_SET(i->flags, IMPORT_FOREIGN_UID)) { + /* Copy in userns */ + + r = import_make_foreign_userns(&i->userns_fd); + if (r < 0) + return r; + + /* Usually, tar_pull_job_on_open_disk_tar() would allocate ->tree_fd for us, but if + * already downloaded the image before, and are just making a copy of the original + * download, we need to open ->tree_fd now */ + if (i->tree_fd < 0) { + _cleanup_close_ int directory_fd = open(i->final_path, O_DIRECTORY|O_CLOEXEC); + if (directory_fd < 0) + return log_error_errno(errno, "Failed to open '%s': %m", i->final_path); + + struct stat st; + if (fstat(directory_fd, &st) < 0) + return log_error_errno(errno, "Failed to stat '%s': %m", i->final_path); + + if (uid_is_foreign(st.st_uid)) { + r = mountfsd_mount_directory_fd(directory_fd, i->userns_fd, DISSECT_IMAGE_FOREIGN_UID, &i->tree_fd); + if (r < 0) + return r; + } else + i->tree_fd = TAKE_FD(directory_fd); + } + + _cleanup_close_ int directory_fd = -EBADF; + r = mountfsd_make_directory(t, /* flags= */ 0, &directory_fd); + if (r < 0) + return r; + + _cleanup_close_ int copy_fd = -EBADF; + r = mountfsd_mount_directory_fd(directory_fd, i->userns_fd, DISSECT_IMAGE_FOREIGN_UID, ©_fd); + if (r < 0) + return r; + + r = import_copy_foreign(i->tree_fd, copy_fd, &i->userns_fd); + if (r < 0) + return r; + } else { + /* Copy locally */ + if (i->flags & IMPORT_BTRFS_SUBVOL) + r = btrfs_subvol_snapshot_at( + AT_FDCWD, i->final_path, + AT_FDCWD, t, + (i->flags & IMPORT_BTRFS_QUOTA ? BTRFS_SNAPSHOT_QUOTA : 0)| + BTRFS_SNAPSHOT_FALLBACK_COPY| + BTRFS_SNAPSHOT_FALLBACK_DIRECTORY| + BTRFS_SNAPSHOT_RECURSIVE); + else + r = copy_tree(i->final_path, t, UID_INVALID, GID_INVALID, COPY_REFLINK|COPY_HARDLINKS, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to create original download image: %m"); + } source = t; } else @@ -264,8 +316,8 @@ static int tar_pull_make_local_copy(TarPull *i) { r = install_file(AT_FDCWD, source, AT_FDCWD, p, (i->flags & IMPORT_FORCE ? INSTALL_REPLACE : 0) | - (i->flags & IMPORT_READ_ONLY ? INSTALL_READ_ONLY : 0) | - (i->flags & IMPORT_SYNC ? INSTALL_SYNCFS : 0)); + (i->flags & IMPORT_READ_ONLY ? INSTALL_READ_ONLY|INSTALL_GRACEFUL : 0) | + (i->flags & IMPORT_SYNC ? INSTALL_SYNCFS|INSTALL_GRACEFUL : 0)); if (r < 0) return log_error_errno(r, "Failed to install local image '%s': %m", p); @@ -427,8 +479,8 @@ static void tar_pull_job_on_finished(PullJob *j) { r = install_file( AT_FDCWD, i->local, AT_FDCWD, NULL, - (i->flags & IMPORT_READ_ONLY ? INSTALL_READ_ONLY : 0) | - (i->flags & IMPORT_SYNC ? INSTALL_SYNCFS : 0)); + (i->flags & IMPORT_READ_ONLY ? INSTALL_READ_ONLY|INSTALL_GRACEFUL : 0) | + (i->flags & IMPORT_SYNC ? INSTALL_SYNCFS|INSTALL_GRACEFUL : 0)); if (r < 0) { log_error_errno(r, "Failed to finalize '%s': %m", i->local); goto finish; @@ -453,8 +505,8 @@ static void tar_pull_job_on_finished(PullJob *j) { r = install_file( AT_FDCWD, i->temp_path, AT_FDCWD, i->final_path, - (i->flags & IMPORT_PULL_KEEP_DOWNLOAD ? INSTALL_READ_ONLY : 0) | - (i->flags & IMPORT_SYNC ? INSTALL_SYNCFS : 0)); + (i->flags & IMPORT_PULL_KEEP_DOWNLOAD ? INSTALL_READ_ONLY|INSTALL_GRACEFUL : 0) | + (i->flags & IMPORT_SYNC ? INSTALL_SYNCFS|INSTALL_GRACEFUL : 0)); if (r < 0) { log_error_errno(r, "Failed to rename to final image name to %s: %m", i->final_path); goto finish; @@ -480,7 +532,7 @@ static void tar_pull_job_on_finished(PullJob *j) { r = install_file( AT_FDCWD, i->settings_temp_path, AT_FDCWD, i->settings_path, - INSTALL_READ_ONLY| + INSTALL_READ_ONLY|INSTALL_GRACEFUL| (i->flags & IMPORT_SYNC ? INSTALL_FSYNC_FULL : 0)); if (r < 0) { log_error_errno(r, "Failed to rename settings file to %s: %m", i->settings_path); @@ -537,26 +589,42 @@ static int tar_pull_job_on_open_disk_tar(PullJob *j) { if (FLAGS_SET(i->flags, IMPORT_DIRECT|IMPORT_FORCE)) (void) rm_rf(where, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME); - if (i->flags & IMPORT_BTRFS_SUBVOL) - r = btrfs_subvol_make_fallback(AT_FDCWD, where, 0755); - else - r = RET_NERRNO(mkdir(where, 0755)); - if (r == -EEXIST && (i->flags & IMPORT_DIRECT)) /* EEXIST is OK if in direct mode, but not otherwise, - * because in that case our temporary path collided */ - r = 0; - if (r < 0) - return log_error_errno(r, "Failed to create directory/subvolume %s: %m", where); - if (r > 0 && (i->flags & IMPORT_BTRFS_QUOTA)) { /* actually btrfs subvol */ - if (!(i->flags & IMPORT_DIRECT)) - (void) import_assign_pool_quota_and_warn(i->image_root); - (void) import_assign_pool_quota_and_warn(where); + if (FLAGS_SET(i->flags, IMPORT_FOREIGN_UID)) { + r = import_make_foreign_userns(&i->userns_fd); + if (r < 0) + return r; + + _cleanup_close_ int directory_fd = -EBADF; + r = mountfsd_make_directory(where, /* flags= */ 0, &directory_fd); + if (r < 0) + return r; + + r = mountfsd_mount_directory_fd(directory_fd, i->userns_fd, DISSECT_IMAGE_FOREIGN_UID, &i->tree_fd); + if (r < 0) + return r; + } else { + if (i->flags & IMPORT_BTRFS_SUBVOL) + r = btrfs_subvol_make_fallback(AT_FDCWD, where, 0755); + else + r = RET_NERRNO(mkdir(where, 0755)); + if (r == -EEXIST && (i->flags & IMPORT_DIRECT)) /* EEXIST is OK if in direct mode, but not otherwise, + * because in that case our temporary path collided */ + r = 0; + if (r < 0) + return log_error_errno(r, "Failed to create directory/subvolume %s: %m", where); + + if (r > 0 && (i->flags & IMPORT_BTRFS_QUOTA)) { /* actually btrfs subvol */ + if (!(i->flags & IMPORT_DIRECT)) + (void) import_assign_pool_quota_and_warn(i->image_root); + (void) import_assign_pool_quota_and_warn(where); + } + + i->tree_fd = open(where, O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW); + if (i->tree_fd < 0) + return log_error_errno(errno, "Failed to open '%s': %m", where); } - i->tree_fd = open(where, O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW); - if (i->tree_fd < 0) - return log_error_errno(errno, "Failed to open '%s': %m", where); - - j->disk_fd = import_fork_tar_x(i->tree_fd, &i->tar_pid); + j->disk_fd = import_fork_tar_x(i->tree_fd, i->userns_fd, &i->tar_pid); if (j->disk_fd < 0) return j->disk_fd; diff --git a/src/import/pull.c b/src/import/pull.c index a41b4b0155..a1b9b940fd 100644 --- a/src/import/pull.c +++ b/src/import/pull.c @@ -560,6 +560,9 @@ static int parse_argv(int argc, char *argv[]) { if (auto_keep_download) SET_FLAG(arg_import_flags, IMPORT_PULL_KEEP_DOWNLOAD, arg_class == IMAGE_MACHINE); + if (arg_runtime_scope == RUNTIME_SCOPE_USER) + arg_import_flags |= IMPORT_FOREIGN_UID; + return 1; } diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 2166af2e7e..0cbb7d02cf 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -4848,14 +4848,17 @@ int mountfsd_mount_image( #endif } -int mountfsd_mount_directory( - const char *path, +int mountfsd_mount_directory_fd( + int directory_fd, int userns_fd, DissectImageFlags flags, int *ret_mount_fd) { int r; + assert(directory_fd >= 0); + assert(ret_mount_fd); + /* Pick one identity, not both, that makes no sense. */ assert(!FLAGS_SET(flags, DISSECT_IMAGE_FOREIGN_UID|DISSECT_IMAGE_IDENTITY_UID)); @@ -4872,10 +4875,6 @@ int mountfsd_mount_directory( if (r < 0) return log_error_errno(r, "Failed to enable varlink fd passing for write: %m"); - _cleanup_close_ int directory_fd = open(path, O_DIRECTORY|O_RDONLY|O_CLOEXEC|O_PATH); - if (directory_fd < 0) - return log_error_errno(errno, "Failed to open '%s': %m", path); - r = sd_varlink_push_dup_fd(vl, directory_fd); if (r < 0) return log_error_errno(r, "Failed to push directory fd into varlink connection: %m"); @@ -4919,3 +4918,103 @@ int mountfsd_mount_directory( *ret_mount_fd = TAKE_FD(fsmount_fd); return 0; } + +int mountfsd_mount_directory( + const char *path, + int userns_fd, + DissectImageFlags flags, + int *ret_mount_fd) { + + assert(path); + assert(ret_mount_fd); + + _cleanup_close_ int directory_fd = open(path, O_DIRECTORY|O_RDONLY|O_CLOEXEC|O_PATH); + if (directory_fd < 0) + return log_error_errno(errno, "Failed to open '%s': %m", path); + + return mountfsd_mount_directory_fd(directory_fd, userns_fd, flags, ret_mount_fd); +} + +int mountfsd_make_directory_fd( + int parent_fd, + const char *name, + DissectImageFlags flags, + int *ret_directory_fd) { + + int r; + + assert(parent_fd >= 0); + assert(name); + assert(ret_directory_fd); + + _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; + r = sd_varlink_connect_address(&vl, "/run/systemd/io.systemd.MountFileSystem"); + if (r < 0) + return log_error_errno(r, "Failed to connect to mountfsd: %m"); + + r = sd_varlink_set_allow_fd_passing_input(vl, true); + if (r < 0) + return log_error_errno(r, "Failed to enable varlink fd passing for read: %m"); + + r = sd_varlink_set_allow_fd_passing_output(vl, true); + if (r < 0) + return log_error_errno(r, "Failed to enable varlink fd passing for write: %m"); + + r = sd_varlink_push_dup_fd(vl, parent_fd); + if (r < 0) + return log_error_errno(r, "Failed to push parent fd into varlink connection: %m"); + + sd_json_variant *reply = NULL; + const char *error_id = NULL; + r = varlink_callbo_and_log( + vl, + "io.systemd.MountFileSystem.MakeDirectory", + &reply, + &error_id, + SD_JSON_BUILD_PAIR_UNSIGNED("parentFileDescriptor", 0), + SD_JSON_BUILD_PAIR_STRING("name", name), + SD_JSON_BUILD_PAIR_BOOLEAN("allowInteractiveAuthentication", FLAGS_SET(flags, DISSECT_IMAGE_ALLOW_INTERACTIVE_AUTH))); + if (r < 0) + return r; + + static const sd_json_dispatch_field dispatch_table[] = { + { "directoryFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, 0, SD_JSON_MANDATORY }, + {} + }; + + unsigned directory_fd_idx = UINT_MAX; + r = sd_json_dispatch(reply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &directory_fd_idx); + if (r < 0) + return log_error_errno(r, "Failed to parse MountImage() reply: %m"); + + _cleanup_close_ int directory_fd = sd_varlink_take_fd(vl, directory_fd_idx); + if (directory_fd < 0) + return log_error_errno(directory_fd, "Failed to take directory fd from Varlink connection: %m"); + + *ret_directory_fd = TAKE_FD(directory_fd); + return 0; +} + +int mountfsd_make_directory( + const char *path, + DissectImageFlags flags, + int *ret_directory_fd) { + + int r; + + _cleanup_free_ char *parent = NULL; + r = path_extract_directory(path, &parent); + if (r < 0) + return log_error_errno(r, "Failed to extract parent directory from '%s': %m", path); + + _cleanup_free_ char *dirname = NULL; + r = path_extract_filename(path, &dirname); + if (r < 0) + return log_error_errno(r, "Failed to extract directory name from '%s': %m", path); + + _cleanup_close_ int fd = open(parent, O_DIRECTORY|O_CLOEXEC); + if (fd < 0) + return log_error_errno(r, "Failed to open '%s': %m", parent); + + return mountfsd_make_directory_fd(fd, dirname, flags, ret_directory_fd); +} diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index b4353f9c9e..48d2359960 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -258,4 +258,8 @@ static inline const char* dissected_partition_fstype(const DissectedPartition *m int get_common_dissect_directory(char **ret); int mountfsd_mount_image(const char *path, int userns_fd, const ImagePolicy *image_policy, const VeritySettings *verity, DissectImageFlags flags, DissectedImage **ret); +int mountfsd_mount_directory_fd(int directory_fd, int userns_fd, DissectImageFlags flags, int *ret_mount_fd); int mountfsd_mount_directory(const char *path, int userns_fd, DissectImageFlags flags, int *ret_mount_fd); + +int mountfsd_make_directory_fd(int parent_fd, const char *name, DissectImageFlags flags, int *ret_directory_fd); +int mountfsd_make_directory(const char *path, DissectImageFlags flags, int *ret_directory_fd); From 25dbf586b20e7418ed06857a3cad8dac630a6123 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 18 Aug 2025 23:18:34 +0200 Subject: [PATCH 10/10] importd: downgrade log message about bound capability set dropping + netns An unprivileged process cannot reduce its own capability bounding set, hence, while it is nice to reduce the set, let's not log about it loudly, in case we are invoked unpriv (which we explicity support these days after all). An unpriv process also cannot detach from its netns, hence also downgrade the warning to a debug message. --- src/import/import-common.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/import/import-common.c b/src/import/import-common.c index d8fd6e3982..f3d70c6f0f 100644 --- a/src/import/import-common.c +++ b/src/import/import-common.c @@ -72,11 +72,11 @@ int import_fork_tar_x(int tree_fd, int userns_fd, PidRef *ret_pid) { } if (unshare(CLONE_NEWNET) < 0) - log_warning_errno(errno, "Failed to lock tar into network namespace, ignoring: %m"); + log_debug_errno(errno, "Failed to lock tar into network namespace, ignoring: %m"); r = capability_bounding_set_drop(retain, true); if (r < 0) - log_warning_errno(r, "Failed to drop capabilities, ignoring: %m"); + log_debug_errno(r, "Failed to drop capabilities, ignoring: %m"); if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) log_warning_errno(errno, "Failed to enable PR_SET_NO_NEW_PRIVS, ignoring: %m");