Compare commits

...

2 Commits

Author SHA1 Message Date
Andres Beltran 03e55e7570
Merge 185b7307d9 into 7a7f306b6c 2024-09-17 22:50:39 +02:00
Andres Beltran 185b7307d9 Add id-mapped mount support for StateDirectory= 2024-09-10 18:03:59 +00:00
7 changed files with 203 additions and 7 deletions

View File

@ -1476,6 +1476,12 @@ CapabilityBoundingSet=~CAP_B CAP_C</programlisting>
below the locations defined in the following table. Also, the corresponding environment variable will
be defined with the full paths of the directories. If multiple directories are set, then in the
environment variable the paths are concatenated with colon (<literal>:</literal>).</para>
<para>If the kernel version suppors <ulink url="https://lwn.net/Articles/896255/">id-mapped mounts</ulink>,
the specified directories will be owned by "nobody" in the host namespace and will be mapped to (and will be
owned by) the service's UID/GUID in its own namespace. For backward compatibility, existing directories
created without id-mapped mounts will be kept untouched.</para>
<table>
<title>Automatic directory creation and environment variables</title>
<tgroup cols='4'>

View File

@ -2280,6 +2280,42 @@ static int create_many_symlinks(const char *root, const char *source, char **sym
return 0;
}
static bool is_idmapping_supported(const char *path) {
_cleanup_close_ int mount_fd = -EBADF, userns_fd = -EBADF, dir_fd = -EBADF;
_cleanup_free_ char *uid_map = NULL, *gid_map = NULL;
int r;
dir_fd = open(path, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
if (dir_fd < 0)
return false;
mount_fd = open_tree(dir_fd, "", AT_EMPTY_PATH | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
if (mount_fd < 0)
return false;
r = strextendf(&uid_map, UID_FMT " " UID_FMT " " UID_FMT "\n", UID_NOBODY, UID_NOBODY, 1u);
if (r < 0)
return false;
r = strextendf(&gid_map, GID_FMT " " GID_FMT " " GID_FMT "\n", GID_NOBODY, GID_NOBODY, 1u);
if (r < 0)
return false;
userns_fd = userns_acquire(uid_map, gid_map);
if (userns_fd < 0)
return false;
r = mount_setattr(mount_fd, "", AT_EMPTY_PATH,
&(struct mount_attr) {
.attr_set = MOUNT_ATTR_IDMAP,
.userns_fd = userns_fd,
}, sizeof(struct mount_attr));
if (r < 0)
return false;
return true;
}
static int setup_exec_directory(
const ExecContext *context,
const ExecParameters *params,
@ -2563,12 +2599,37 @@ static int setup_exec_directory(
if (params->runtime_scope != RUNTIME_SCOPE_SYSTEM)
continue;
/* Then, change the ownership of the whole tree, if necessary. When dynamic users are used we
* drop the suid/sgid bits, since we really don't want SUID/SGID files for dynamic UID/GID
* assignments to exist. */
r = path_chown_recursive(pp ?: p, uid, gid, context->dynamic_user ? 01777 : 07777, AT_SYMLINK_FOLLOW);
/* Use 'nobody' uid/gid for exec directories if ID-mapping is supported. For backward compatibility,
* continue doing chmod/chown if the directory was chmod/chowned before (if uid/gid is not 'nobody') */
bool idmapping_supported = is_idmapping_supported(pp ?: p);
log_debug("ID-mapping is%s supported for exec directory %s", idmapping_supported ? "" : "not", pp ?: p);
struct stat st;
r = RET_NERRNO(stat(pp ?: p, &st));
if (r < 0)
goto fail;
/* Change the ownership of the whole tree, if necessary. When dynamic users are used we
* drop the suid/sgid bits, since we really don't want SUID/SGID files for dynamic UID/GID
* assignments to exist. */
if (uid == 0 || gid == 0) {
i->idmapped = false;
r = path_chown_recursive(pp ?: p, uid, gid, context->dynamic_user ? 01777 : 07777, AT_SYMLINK_FOLLOW);
if (r < 0)
goto fail;
}else if (idmapping_supported && st.st_uid == UID_NOBODY && st.st_gid == GID_NOBODY) {
i->idmapped = true;
}else if (idmapping_supported && st.st_uid == (uid_t)0 && st.st_gid == (gid_t)0) {
r = path_chown_recursive(pp ?: p, UID_NOBODY, GID_NOBODY, context->dynamic_user ? 01777 : 07777, AT_SYMLINK_FOLLOW);
if (r < 0)
goto fail;
i->idmapped = true;
}else {
i->idmapped = false;
r = path_chown_recursive(pp ?: p, uid, gid, context->dynamic_user ? 01777 : 07777, AT_SYMLINK_FOLLOW);
if (r < 0)
goto fail;
}
}
/* If we are not going to run in a namespace, set up the symlinks - otherwise
@ -2620,6 +2681,8 @@ static int setup_smack(
static int compile_bind_mounts(
const ExecContext *context,
const ExecParameters *params,
uid_t uid,
gid_t gid,
BindMount **ret_bind_mounts,
size_t *ret_n_bind_mounts,
char ***ret_empty_directories) {
@ -2718,6 +2781,9 @@ static int compile_bind_mounts(
.destination = TAKE_PTR(d),
.nosuid = context->dynamic_user, /* don't allow suid/sgid when DynamicUser= is on */
.recursive = true,
.idmapped = i->idmapped,
.uid = uid,
.gid = gid,
};
}
}
@ -3040,7 +3106,9 @@ static int apply_mount_namespace(
ExecRuntime *runtime,
const char *memory_pressure_path,
bool needs_sandboxing,
char **error_path) {
char **error_path,
uid_t uid,
gid_t gid) {
_cleanup_(verity_settings_done) VeritySettings verity = VERITY_SETTINGS_DEFAULT;
_cleanup_strv_free_ char **empty_directories = NULL, **symlinks = NULL,
@ -3076,7 +3144,7 @@ static int apply_mount_namespace(
return r;
}
r = compile_bind_mounts(context, params, &bind_mounts, &n_bind_mounts, &empty_directories);
r = compile_bind_mounts(context, params, uid, gid, &bind_mounts, &n_bind_mounts, &empty_directories);
if (r < 0)
return r;
@ -4830,7 +4898,9 @@ int exec_invoke(
runtime,
memory_pressure_path,
needs_sandboxing,
&error_path);
&error_path,
uid,
gid);
if (r < 0) {
*exit_status = EXIT_NAMESPACE;
return log_exec_error_errno(context, params, r, "Failed to set up mount namespacing%s%s: %m",

View File

@ -2721,6 +2721,7 @@ int exec_directory_add(ExecDirectory *d, const char *path, const char *symlink)
d->items[d->n_items++] = (ExecDirectoryItem) {
.path = TAKE_PTR(p),
.symlinks = TAKE_PTR(s),
.idmapped = false,
};
return 1; /* new item is added */

View File

@ -156,6 +156,7 @@ typedef struct ExecDirectoryItem {
char *path;
char **symlinks;
bool only_create;
bool idmapped;
} ExecDirectoryItem;
typedef struct ExecDirectory {

View File

@ -113,6 +113,9 @@ typedef struct MountEntry {
LIST_HEAD(MountOptions, image_options_const);
char **overlay_layers;
VeritySettings verity;
bool idmapped;
uid_t uid;
gid_t gid;
} MountEntry;
typedef struct MountList {
@ -451,6 +454,9 @@ static int append_bind_mounts(MountList *ml, const BindMount *binds, size_t n) {
.flags = b->nodev ? MS_NODEV : 0,
.source_const = b->source,
.ignore = b->ignore_enoent,
.idmapped = b->idmapped,
.uid = b->uid,
.gid = b->gid,
};
}
@ -1806,6 +1812,48 @@ static int apply_one_mount(
}
log_debug("Successfully mounted %s to %s", what, mount_entry_path(m));
/* Take care of id-mapped mounts */
if (m->idmapped && uid_is_valid(m->uid) && gid_is_valid(m->gid)) {
_cleanup_close_ int userns_fd = -EBADF;
_cleanup_free_ char *uid_map = NULL, *gid_map = NULL;
log_debug("Setting an id-mapped mount on %s", mount_entry_path(m));
// Do mapping from nobody (in setup_exec_directory()) -> this uid
r = strextendf(&uid_map, UID_FMT " " UID_FMT " " UID_FMT "\n", UID_NOBODY, m->uid, 1u);
if (r < 0)
return log_oom();
// Consider StateDirectory=xxx aaa xxx:aaa/222
// To allow for later symlink creation (by root) in create_symlinks_from_tuples(), map root as well
if (m->uid != (uid_t)0) {
r = strextendf(&uid_map, UID_FMT " " UID_FMT " " UID_FMT "\n", (uid_t)0, (uid_t)0, 1u);
if (r < 0)
return log_oom();
}
r = strextendf(&gid_map, GID_FMT " " GID_FMT " " GID_FMT "\n", GID_NOBODY, m->gid, 1u);
if (r < 0)
return log_oom();
if (m->gid != (gid_t)0) {
r = strextendf(&gid_map, GID_FMT " " GID_FMT " " GID_FMT "\n", (gid_t)0, (gid_t)0, 1u);
if (r < 0)
return log_oom();
}
userns_fd = userns_acquire(uid_map, gid_map);
if (userns_fd < 0)
return log_error_errno(userns_fd, "Failed to allocate user namespace: %m");
r = remount_idmap_fd(STRV_MAKE(mount_entry_path(m)), userns_fd);
if (r < 0)
return log_error_errno(r, "Failed to create an id-mapped mount: %m");
log_debug("ID-mapped mount created successfully for %s from %u to %u", mount_entry_path(m), UID_NOBODY, m->uid);
}
return 1;
}

View File

@ -78,6 +78,9 @@ struct BindMount {
bool noexec;
bool recursive;
bool ignore_enoent;
bool idmapped;
uid_t uid;
gid_t gid;
};
struct TemporaryFileSystem {

View File

@ -148,12 +148,79 @@ EOF
systemctl start testservice-34-check-writable.service
}
test_check_idmapped_mounts() {
rm -rf /var/lib/testidmapped /var/lib/private/testidmapped
cat >/run/systemd/system/testservice-34-check-idmapped.service <<\EOF
[Unit]
Description=Check id-mapped directories when DynamicUser=yes with StateDirectory
[Service]
# Relevant only for sanitizer runs
EnvironmentFile=-/usr/lib/systemd/systemd-asan-env
Type=oneshot
MountAPIVFS=yes
DynamicUser=yes
PrivateUsers=yes
TemporaryFileSystem=/run /var/opt /var/lib /vol
UMask=0000
StateDirectory=testidmapped:sampleservice
ExecStart=/bin/bash -c ' \
set -eux; \
set -o pipefail; \
touch /var/lib/sampleservice/testfile; \
[[ $(awk "NR==2 {print \$1}" /proc/self/uid_map) == $(stat -c "%%u" /var/lib/private/testidmapped/testfile) ]]; \
'
EOF
systemctl daemon-reload
systemctl start testservice-34-check-idmapped.service
[[ $(stat -c "%u" /var/lib/private/testidmapped/testfile) == 65534 ]]
}
test_check_idmapped_mounts_root() {
rm -rf /var/lib/testidmapped /var/lib/private/testidmapped
cat >/run/systemd/system/testservice-34-check-idmapped.service <<\EOF
[Unit]
Description=Check id-mapped directories when DynamicUser=no with StateDirectory
[Service]
# Relevant only for sanitizer runs
EnvironmentFile=-/usr/lib/systemd/systemd-asan-env
Type=oneshot
MountAPIVFS=yes
User=root
DynamicUser=no
PrivateUsers=no
TemporaryFileSystem=/run /var/opt /var/lib /vol
UMask=0000
StateDirectory=testidmapped:sampleservice
ExecStart=/bin/bash -c ' \
set -eux; \
set -o pipefail; \
touch /var/lib/sampleservice/testfile; \
[[ 0 == $(stat -c "%%u" /var/lib/testidmapped/testfile) ]]; \
'
EOF
systemctl daemon-reload
systemctl start testservice-34-check-idmapped.service
[[ $(stat -c "%u" /var/lib/testidmapped/testfile) == 0 ]]
}
test_directory "StateDirectory" "/var/lib"
test_directory "RuntimeDirectory" "/run"
test_directory "CacheDirectory" "/var/cache"
test_directory "LogsDirectory" "/var/log"
test_check_writable
test_check_idmapped_mounts
test_check_idmapped_mounts_root
systemd-analyze log-level info