From 70733160eedc7b76477242a8cad079cfb6b0a913 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 25 Aug 2025 11:17:40 +0200 Subject: [PATCH 1/3] os-util: add fd_is_os_tree() which is like path_is_os_tree() but operates on an fd --- src/basic/os-util.c | 20 ++++++++++++++++++++ src/basic/os-util.h | 1 + 2 files changed, 21 insertions(+) diff --git a/src/basic/os-util.c b/src/basic/os-util.c index be845e2af7..c04f86893c 100644 --- a/src/basic/os-util.c +++ b/src/basic/os-util.c @@ -114,6 +114,26 @@ int path_is_extension_tree(ImageClass image_class, const char *path, const char return 1; } +int fd_is_os_tree(int fd) { + int r; + + assert(fd >= 0); + + r = open_extension_release_at( + fd, + IMAGE_MACHINE, + /* extension= */ NULL, + /* relax_extension_release_check= */ false, + /* ret_path= */ NULL, + /* ret_fd= */ NULL); + if (r == -ENOENT) + return false; + if (r < 0) + return r; + + return true; +} + static int extension_release_strict_xattr_value(int extension_release_fd, const char *extension_release_dir_path, const char *filename) { int r; diff --git a/src/basic/os-util.h b/src/basic/os-util.h index a3829b4f51..79cbe25a49 100644 --- a/src/basic/os-util.h +++ b/src/basic/os-util.h @@ -28,6 +28,7 @@ int path_is_extension_tree(ImageClass image_class, const char *path, const char static inline int path_is_os_tree(const char *path) { return path_is_extension_tree(_IMAGE_CLASS_INVALID, path, NULL, false); } +int fd_is_os_tree(int fd); int open_extension_release(const char *root, ImageClass image_class, const char *extension, bool relax_extension_release_check, char **ret_path, int *ret_fd); int open_extension_release_at(int rfd, ImageClass image_class, const char *extension, bool relax_extension_release_check, char **ret_path, int *ret_fd); From 7912b1ebe5c22b8b4585c1e40730c8b1aca46678 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 25 Aug 2025 11:19:02 +0200 Subject: [PATCH 2/3] import-common: rework import_mangle_os_tree() to operate based on fd to tree --- src/import/import-common.c | 60 +++++++++++++++++++++++--------------- src/import/import-common.h | 1 + src/import/import-tar.c | 2 +- src/import/pull-tar.c | 4 +-- 4 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/import/import-common.c b/src/import/import-common.c index eaa68fae5a..70b3c3be02 100644 --- a/src/import/import-common.c +++ b/src/import/import-common.c @@ -148,19 +148,24 @@ int import_fork_tar_c(int tree_fd, int userns_fd, PidRef *ret_pid) { return TAKE_FD(pipefd[0]); } -int import_mangle_os_tree(const char *path) { +int import_mangle_os_tree_fd(int tree_fd) { _cleanup_free_ char *child = NULL, *t = NULL, *joined = NULL; _cleanup_closedir_ DIR *d = NULL, *cd = NULL; struct dirent *dent; struct stat st; int r; - assert(path); + assert(tree_fd >= 0); /* Some tarballs contain a single top-level directory that contains the actual OS directory tree. Try to * recognize this, and move the tree one level up. */ - r = path_is_os_tree(path); + _cleanup_free_ char *path = NULL; + r = fd_get_path(tree_fd, &path); + if (r < 0) + return log_error_errno(r, "Failed to determine path of fd: %m"); + + r = fd_is_os_tree(tree_fd); if (r < 0) return log_error_errno(r, "Failed to determine whether '%s' is an OS tree: %m", path); if (r > 0) { @@ -170,9 +175,9 @@ int import_mangle_os_tree(const char *path) { log_debug("Directory tree '%s' is not recognizable as OS tree, checking whether to rearrange it.", path); - d = opendir(path); + d = xopendirat(tree_fd, /* path= */ NULL, /* flags= */ 0); if (!d) - return log_error_errno(r, "Failed to open directory '%s': %m", path); + return log_error_errno(errno, "Failed to open directory '%s': %m", path); errno = 0; dent = readdir_no_dot(d); @@ -191,29 +196,29 @@ int import_mangle_os_tree(const char *path) { errno = 0; dent = readdir_no_dot(d); if (dent) { - if (errno != 0) - return log_error_errno(errno, "Failed to iterate through directory '%s': %m", path); - log_debug("Directory '%s' does not look like an OS tree, and has multiple children, leaving as it is.", path); return 0; + } else if (errno != 0) + return log_error_errno(errno, "Failed to iterate through directory '%s': %m", path); + + _cleanup_close_ int child_fd = openat(dirfd(d), child, O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW|O_NONBLOCK); + if (child_fd < 0) { + if (IN_SET(errno, ENOTDIR, ELOOP)) { + log_debug_errno(errno, "Child '%s' of directory '%s' is not a directory, leaving things as they are.", child, path); + return 0; + } + + return log_debug_errno(errno, "Failed to open file '%s/%s': %m", path, child); } - if (fstatat(dirfd(d), child, &st, AT_SYMLINK_NOFOLLOW) < 0) + if (fstat(child_fd, &st) < 0) return log_debug_errno(errno, "Failed to stat file '%s/%s': %m", path, child); - r = stat_verify_directory(&st); - if (r < 0) { - log_debug_errno(r, "Child '%s' of directory '%s' is not a directory, leaving things as they are.", child, path); - return 0; - } joined = path_join(path, child); if (!joined) return log_oom(); - r = path_is_os_tree(joined); - if (r == -ENOTDIR) { - log_debug("Directory '%s' does not look like an OS tree, and contains a single regular file only, leaving as it is.", path); - return 0; - } + + r = fd_is_os_tree(child_fd); if (r < 0) return log_error_errno(r, "Failed to determine whether '%s' is an OS tree: %m", joined); if (r == 0) { @@ -230,7 +235,7 @@ int import_mangle_os_tree(const char *path) { * * Let's now rearrange things, moving everything in the inner directory one level up */ - cd = xopendirat(dirfd(d), child, O_NOFOLLOW); + cd = take_fdopendir(&child_fd); if (!cd) return log_error_errno(errno, "Can't open directory '%s': %m", joined); @@ -238,7 +243,7 @@ int import_mangle_os_tree(const char *path) { /* Let's rename the child to an unguessable name so that we can be sure all files contained in it can be * safely moved up and won't collide with the name. */ - r = tempfn_random(child, NULL, &t); + r = tempfn_random(child, /* extra= */ NULL, &t); if (r < 0) return log_oom(); r = rename_noreplace(dirfd(d), child, dirfd(d), t); @@ -259,17 +264,26 @@ int import_mangle_os_tree(const char *path) { r = futimens(dirfd(d), (struct timespec[2]) { st.st_atim, st.st_mtim }); if (r < 0) - log_debug_errno(r, "Failed to adjust top-level timestamps '%s', ignoring: %m", path); + log_debug_errno(errno, "Failed to adjust top-level timestamps '%s', ignoring: %m", path); r = fchmod_and_chown(dirfd(d), st.st_mode, st.st_uid, st.st_gid); if (r < 0) return log_error_errno(r, "Failed to adjust top-level directory mode/ownership '%s': %m", path); log_info("Successfully rearranged OS tree."); - return 0; } +int import_mangle_os_tree(const char *path) { + assert(path); + + _cleanup_close_ int fd = open(path, O_DIRECTORY|O_CLOEXEC|O_PATH); + if (fd < 0) + return log_error_errno(errno, "Failed to open '%s': %m", path); + + return import_mangle_os_tree_fd(fd); +} + bool import_validate_local(const char *name, ImportFlags flags) { /* By default we insist on a valid hostname for naming images. But optionally we relax that, in which diff --git a/src/import/import-common.h b/src/import/import-common.h index a922280d0a..289b83b4e6 100644 --- a/src/import/import-common.h +++ b/src/import/import-common.h @@ -37,6 +37,7 @@ typedef enum ImportFlags { int import_fork_tar_c(int tree_fd, int userns_fd, PidRef *ret_pid); int import_fork_tar_x(int tree_fd, int userns_fd, PidRef *ret_pid); +int import_mangle_os_tree_fd(int tree_fd); int import_mangle_os_tree(const char *path); bool import_validate_local(const char *name, ImportFlags flags); diff --git a/src/import/import-tar.c b/src/import/import-tar.c index 85fbf5abd1..e30be005fc 100644 --- a/src/import/import-tar.c +++ b/src/import/import-tar.c @@ -199,7 +199,7 @@ static int tar_import_finish(TarImport *i) { assert_se(d = i->temp_path ?: i->local); - r = import_mangle_os_tree(d); + r = import_mangle_os_tree_fd(i->tree_fd); if (r < 0) return r; diff --git a/src/import/pull-tar.c b/src/import/pull-tar.c index c9b9facaa5..65f88e7a88 100644 --- a/src/import/pull-tar.c +++ b/src/import/pull-tar.c @@ -497,7 +497,7 @@ static void tar_pull_job_on_finished(PullJob *j) { tar_pull_report_progress(i, TAR_FINALIZING); - r = import_mangle_os_tree(i->local); + r = import_mangle_os_tree_fd(i->tree_fd); if (r < 0) goto finish; @@ -523,7 +523,7 @@ static void tar_pull_job_on_finished(PullJob *j) { tar_pull_report_progress(i, TAR_FINALIZING); - r = import_mangle_os_tree(i->temp_path); + r = import_mangle_os_tree_fd(i->tree_fd); if (r < 0) goto finish; From 2348c56367826d3dd87dffb578148e7132484c99 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 25 Aug 2025 11:34:53 +0200 Subject: [PATCH 3/3] import: make sure image mangling works unpriv too --- src/import/import-common.c | 53 +++++++++++++++++++++++++++++++++++--- src/import/import-common.h | 5 ++-- src/import/import-fs.c | 2 +- src/import/import-tar.c | 2 +- src/import/pull-tar.c | 4 +-- 5 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/import/import-common.c b/src/import/import-common.c index 70b3c3be02..11a6c060bb 100644 --- a/src/import/import-common.c +++ b/src/import/import-common.c @@ -148,7 +148,7 @@ int import_fork_tar_c(int tree_fd, int userns_fd, PidRef *ret_pid) { return TAKE_FD(pipefd[0]); } -int import_mangle_os_tree_fd(int tree_fd) { +int import_mangle_os_tree_fd(int tree_fd, int userns_fd, ImportFlags flags) { _cleanup_free_ char *child = NULL, *t = NULL, *joined = NULL; _cleanup_closedir_ DIR *d = NULL, *cd = NULL; struct dirent *dent; @@ -157,6 +157,9 @@ int import_mangle_os_tree_fd(int tree_fd) { assert(tree_fd >= 0); + if (FLAGS_SET(flags, IMPORT_FOREIGN_UID) && userns_fd >= 0) + return import_mangle_os_tree_fd_foreign(tree_fd, userns_fd); + /* Some tarballs contain a single top-level directory that contains the actual OS directory tree. Try to * recognize this, and move the tree one level up. */ @@ -274,14 +277,58 @@ int import_mangle_os_tree_fd(int tree_fd) { return 0; } -int import_mangle_os_tree(const char *path) { +int import_mangle_os_tree(const char *path, int userns_fd, ImportFlags flags) { assert(path); _cleanup_close_ int fd = open(path, O_DIRECTORY|O_CLOEXEC|O_PATH); if (fd < 0) return log_error_errno(errno, "Failed to open '%s': %m", path); - return import_mangle_os_tree_fd(fd); + return import_mangle_os_tree_fd(fd, userns_fd, flags); +} + +int import_mangle_os_tree_fd_foreign( + int tree_fd, + int userns_fd) { + + int r; + + assert(tree_fd >= 0); + assert(userns_fd >= 0); + + r = safe_fork_full( + "mangle-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); + } + + r = import_mangle_os_tree_fd(tree_fd, /* userns_fd= */ -EBADF, /* flags= */ 0); + if (r < 0) { + log_error_errno(r, "Failed to mangle OS tree in foreign UID mode: %m"); + _exit(EXIT_FAILURE); + } + + _exit(EXIT_SUCCESS); + } + + return 0; + } bool import_validate_local(const char *name, ImportFlags flags) { diff --git a/src/import/import-common.h b/src/import/import-common.h index 289b83b4e6..8fab3f380b 100644 --- a/src/import/import-common.h +++ b/src/import/import-common.h @@ -37,8 +37,9 @@ typedef enum ImportFlags { int import_fork_tar_c(int tree_fd, int userns_fd, PidRef *ret_pid); int import_fork_tar_x(int tree_fd, int userns_fd, PidRef *ret_pid); -int import_mangle_os_tree_fd(int tree_fd); -int import_mangle_os_tree(const char *path); +int import_mangle_os_tree_fd(int tree_fd, int userns_fd, ImportFlags flags); +int import_mangle_os_tree(const char *path, int userns_fd, ImportFlags flags); +int import_mangle_os_tree_fd_foreign(int tree_fd, int userns_fd); bool import_validate_local(const char *name, ImportFlags flags); diff --git a/src/import/import-fs.c b/src/import/import-fs.c index 537fe46853..947bc7c9b5 100644 --- a/src/import/import-fs.c +++ b/src/import/import-fs.c @@ -238,7 +238,7 @@ static int import_fs(int argc, char *argv[], void *userdata) { return log_error_errno(r, "Failed to copy directory: %m"); } - r = import_mangle_os_tree(dest); + r = import_mangle_os_tree(dest, /* userns_fd= */ -EBADF, /* flags= */ 0); if (r < 0) return r; diff --git a/src/import/import-tar.c b/src/import/import-tar.c index e30be005fc..5aa3a98640 100644 --- a/src/import/import-tar.c +++ b/src/import/import-tar.c @@ -199,7 +199,7 @@ static int tar_import_finish(TarImport *i) { assert_se(d = i->temp_path ?: i->local); - r = import_mangle_os_tree_fd(i->tree_fd); + r = import_mangle_os_tree_fd(i->tree_fd, i->userns_fd, i->flags); if (r < 0) return r; diff --git a/src/import/pull-tar.c b/src/import/pull-tar.c index 65f88e7a88..23bdfb186a 100644 --- a/src/import/pull-tar.c +++ b/src/import/pull-tar.c @@ -497,7 +497,7 @@ static void tar_pull_job_on_finished(PullJob *j) { tar_pull_report_progress(i, TAR_FINALIZING); - r = import_mangle_os_tree_fd(i->tree_fd); + r = import_mangle_os_tree_fd(i->tree_fd, i->userns_fd, i->flags); if (r < 0) goto finish; @@ -523,7 +523,7 @@ static void tar_pull_job_on_finished(PullJob *j) { tar_pull_report_progress(i, TAR_FINALIZING); - r = import_mangle_os_tree_fd(i->tree_fd); + r = import_mangle_os_tree_fd(i->tree_fd, i->userns_fd, i->flags); if (r < 0) goto finish;