diff --git a/man/systemd-dissect.xml b/man/systemd-dissect.xml
index 3aaa1744f3..2718feccb7 100644
--- a/man/systemd-dissect.xml
+++ b/man/systemd-dissect.xml
@@ -62,6 +62,9 @@
systemd-dissect OPTIONS --validate IMAGE
+
+ systemd-dissect OPTIONS --shift IMAGE UIDBASE
+
@@ -350,6 +353,27 @@
+
+
+
+ Recursively iterates through all inodes of the specified image and shifts the UIDs
+ and GIDs the inodes are owned by into the specified UID range. Takes an image path and a UID base as
+ parameter. The UID base can be specified numerically (in which case it must be a multiple of 65536,
+ and either 0 or within the container or foreign UID range, as per Users, Groups, UIDs and GIDs on systemd Systems), or as
+ the symbolic identifier foreign which is shorthand to the foreign UID base. This
+ command is useful for preparing directory container images for unprivileged use. Note that this
+ command is intended for images that use the 16bit UIDs/GIDs range only, and it always ignores the
+ upper 16bit of the current UID/GID ownership, combining the lower 16 bit with the target UID
+ base.
+
+ Use systemd-dissect --shift /some/container/tree foreign to shift a
+ container image into the foreign UID range, or systemd-dissect --shift /some/container/tree
+ 0 to shift it to host UID range.
+
+
+
+
diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c
index cb51d71384..3ca1e17be4 100644
--- a/src/dissect/dissect.c
+++ b/src/dissect/dissect.c
@@ -45,6 +45,7 @@
#include "process-util.h"
#include "recurse-dir.h"
#include "sha256.h"
+#include "shift-uid.h"
#include "stat-util.h"
#include "string-util.h"
#include "strv.h"
@@ -68,6 +69,7 @@ static enum {
ACTION_DISCOVER,
ACTION_VALIDATE,
ACTION_MAKE_ARCHIVE,
+ ACTION_SHIFT,
} arg_action = ACTION_DISSECT;
static char *arg_image = NULL;
static char *arg_root = NULL;
@@ -97,6 +99,7 @@ static bool arg_mtree_hash = true;
static bool arg_via_service = false;
static RuntimeScope arg_runtime_scope = _RUNTIME_SCOPE_INVALID;
static bool arg_all = false;
+static uid_t arg_uid_base = UID_INVALID;
STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
@@ -129,6 +132,7 @@ static int help(void) {
"%1$s [OPTIONS...] --make-archive IMAGE [TARGET]\n"
"%1$s [OPTIONS...] --discover\n"
"%1$s [OPTIONS...] --validate IMAGE\n"
+ "%1$s [OPTIONS...] --shift IMAGE UIDBASE\n"
"\n%5$sDissect a Discoverable Disk Image (DDI).%6$s\n\n"
"%3$sOptions:%4$s\n"
" --no-pager Do not pipe output into a pager\n"
@@ -174,6 +178,7 @@ static int help(void) {
" --make-archive Convert the DDI to an archive file\n"
" --discover Discover DDIs in well known directories\n"
" --validate Validate image and image policy\n"
+ " --shift Shift UID range to selected base\n"
"\nSee the %2$s for details.\n",
program_invocation_short_name,
link,
@@ -279,6 +284,7 @@ static int parse_argv(int argc, char *argv[]) {
ARG_VALIDATE,
ARG_MTREE_HASH,
ARG_MAKE_ARCHIVE,
+ ARG_SHIFT,
ARG_SYSTEM,
ARG_USER,
ARG_ALL,
@@ -315,6 +321,7 @@ static int parse_argv(int argc, char *argv[]) {
{ "validate", no_argument, NULL, ARG_VALIDATE },
{ "mtree-hash", required_argument, NULL, ARG_MTREE_HASH },
{ "make-archive", no_argument, NULL, ARG_MAKE_ARCHIVE },
+ { "shift", no_argument, NULL, ARG_SHIFT },
{ "system", no_argument, NULL, ARG_SYSTEM },
{ "user", no_argument, NULL, ARG_USER },
{ "all", no_argument, NULL, ARG_ALL },
@@ -550,6 +557,10 @@ static int parse_argv(int argc, char *argv[]) {
arg_action = ACTION_MAKE_ARCHIVE;
break;
+ case ARG_SHIFT:
+ arg_action = ACTION_SHIFT;
+ break;
+
case ARG_SYSTEM:
system_scope_requested = true;
break;
@@ -731,6 +742,33 @@ static int parse_argv(int argc, char *argv[]) {
arg_flags &= ~(DISSECT_IMAGE_PIN_PARTITION_DEVICES|DISSECT_IMAGE_ADD_PARTITION_DEVICES);
break;
+ case ACTION_SHIFT:
+ if (optind + 2 != argc)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Expected an image path and a UID base as only argument.");
+
+ r = parse_image_path_argument(argv[optind], &arg_root, &arg_image);
+ if (r < 0)
+ return r;
+
+ if (streq(argv[optind + 1], "foreign"))
+ arg_uid_base = FOREIGN_UID_BASE;
+ else {
+ r = parse_uid(argv[optind + 1], &arg_uid_base);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse UID base: %s", argv[optind + 1]);
+
+ if ((arg_uid_base & 0xFFFF) != 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected UID base not a multiple of 64K: " UID_FMT, arg_uid_base);
+ if (arg_uid_base != 0 &&
+ !uid_is_container(arg_uid_base) &&
+ !uid_is_foreign(arg_uid_base))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected UID range is not in the container range, nor the foreign one, refusing.");
+ }
+
+ arg_flags |= DISSECT_IMAGE_REQUIRE_ROOT;
+ break;
+
default:
assert_not_reached();
}
@@ -1444,7 +1482,7 @@ static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopD
const char *root;
int r;
- assert(IN_SET(arg_action, ACTION_LIST, ACTION_MTREE, ACTION_COPY_FROM, ACTION_COPY_TO, ACTION_MAKE_ARCHIVE));
+ assert(IN_SET(arg_action, ACTION_LIST, ACTION_MTREE, ACTION_COPY_FROM, ACTION_COPY_TO, ACTION_MAKE_ARCHIVE, ACTION_SHIFT));
if (arg_image) {
assert(m);
@@ -1699,6 +1737,13 @@ static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopD
#endif
}
+ case ACTION_SHIFT:
+ r = path_patch_uid(root, arg_uid_base, 0x10000);
+ if (r < 0)
+ return log_error_errno(r, "Failed to shift UID base: %m");
+
+ return 0;
+
default:
assert_not_reached();
}
@@ -2121,7 +2166,7 @@ static int run(int argc, char *argv[]) {
else
r = loop_device_make_by_path(arg_image, open_flags, /* sector_size= */ UINT32_MAX, loop_flags, LOCK_SH, &d);
if (r < 0) {
- if (!ERRNO_IS_PRIVILEGE(r) || !IN_SET(arg_action, ACTION_DISSECT, ACTION_LIST, ACTION_MTREE, ACTION_COPY_FROM, ACTION_COPY_TO))
+ if (!ERRNO_IS_PRIVILEGE(r) || !IN_SET(arg_action, ACTION_DISSECT, ACTION_LIST, ACTION_MTREE, ACTION_COPY_FROM, ACTION_COPY_TO, ACTION_SHIFT))
return log_error_errno(r, "Failed to set up loopback device for %s: %m", arg_image);
log_debug_errno(r, "Lacking permissions to set up loopback block device for %s, using service: %m", arg_image);
@@ -2206,6 +2251,7 @@ static int run(int argc, char *argv[]) {
case ACTION_COPY_FROM:
case ACTION_COPY_TO:
case ACTION_MAKE_ARCHIVE:
+ case ACTION_SHIFT:
return action_list_or_mtree_or_copy_or_make_archive(m, d, userns_fd);
case ACTION_WITH: