diff --git a/man/systemd-nsresourced.service.xml b/man/systemd-nsresourced.service.xml
index 120027aab0..e3ac1eb0c3 100644
--- a/man/systemd-nsresourced.service.xml
+++ b/man/systemd-nsresourced.service.xml
@@ -40,9 +40,15 @@
Allocations of UIDs/GIDs this way are transient: when a user namespace goes away, its UID/GID range
is returned to the pool of available ranges. In order to ensure that clients cannot gain persistency in
- their transient UID/GID range a BPF-LSM based policy is enforced that ensures that user namespaces set up
- this way can only write to file systems they allocate themselves or that are explicitly allowlisted via
- systemd-nsresourced.
+ their transient UID/GID range a BPF-LSM based policy is enforced that refuses to create inodes — or
+ change ownership of them — that would end up owned by a transient UID/GID on a file system outliving the
+ user namespace the range was assigned to. Writes to file systems the user namespace allocated itself are
+ hence permitted, as those are torn down together with it, and so are writes through id-mapped mounts that
+ translate the transient range into something else before it reaches the disk. As a stacking file system
+ such as overlayfs writes to a separate upper layer that would not be covered by the
+ checks on the overlay mount itself, mounting a writable overlayfs instance is refused
+ for these user namespaces unless its upper layer, too, is torn down together with the user namespace, or
+ is an id-mapped mount that translates the transient range away before it reaches the disk.
systemd-nsresourced automatically ensures that any registered UID ranges show up
in the system's NSS database via the User/Group Record
@@ -73,7 +79,7 @@
the user namespace. When this option is enabled, the foreign UID range is mapped 1:1 into the user
namespace, allowing processes inside to access and manipulate files owned by the foreign UID range.
- The service provides API calls to allowlist mounts (referenced via their mount file descriptors as
+ The service provides API calls to delegate mounts (referenced via their mount file descriptors as
per Linux fsmount() API), to pass ownership of a cgroup subtree to the user
namespace and to delegate a virtual Ethernet device pair to the user namespace. When used in combination
this is sufficient to implement fully unprivileged container environments, as implemented by
diff --git a/src/bpf/userns-restrict.bpf.c b/src/bpf/userns-restrict.bpf.c
index 54473d7210..35856a5330 100644
--- a/src/bpf/userns-restrict.bpf.c
+++ b/src/bpf/userns-restrict.bpf.c
@@ -30,13 +30,22 @@
void* bpf_rdonly_cast(const void *, __u32) __ksym;
#endif
-/* BPF module that implements an allowlist of mounts (identified by mount ID) for user namespaces (identified
- * by their inode number in nsfs) that restricts creation of inodes (which would inherit the callers UID/GID)
- * or changing of ownership (similar).
+/* BPF module that keeps user namespaces provisioned by nsresourced (identified by their inode number in
+ * nsfs) from leaving objects owned by their transient UID/GID range behind on file systems that outlive the
+ * namespace. Transient ranges are recycled, so any such object would later be owned by an unrelated client.
*
* This hooks into the various path-based LSM entrypoints that control inode creation as well as chown(), and
- * then looks up the calling process' user namespace in a global map of namespaces, which points us to
- * another map that is simply a list of allowed mnt_ids. */
+ * for each asks the only two questions that matter:
+ *
+ * a) Does the file system die together with the user namespace? Superblocks record the user namespace they
+ * were mounted in, so this is simply whether sb->s_user_ns sits at or below the provisioned namespace.
+ *
+ * b) Will the transient ID actually reach the disk? An idmapped mount may translate it into something
+ * else entirely, in which case nothing recyclable is recorded anywhere.
+ *
+ * Both are answered from fields the kernel stamps itself and that a client cannot influence: unlike the
+ * mount namespace a mount happens to sit in, neither a superblock's user namespace nor a mount's idmapping
+ * can be forged with unshare(), and both are preserved when a mount is cloned. */
// FIXME: ACL adjustments are currently not blocked. There's no path-based LSM hook available in the kernel
// for setting xattrs or ACLs, hence we cannot easily block them, even though we want that. We can get away
@@ -45,22 +54,26 @@ void* bpf_rdonly_cast(const void *, __u32) __ksym;
// not be reachable through it. It still sucks though that a user can persistently add an ACL entry to a file
// with their transient UIDs/GIDs.
-/* kernel currently enforces a maximum usernamespace nesting depth of 32, see create_user_ns() in the kernel sources */
-#define USER_NAMESPACE_DEPTH_MAX 32U
+/* create_user_ns() in the kernel sources refuses to nest below a user namespace whose level is above 32,
+ * hence the deepest reachable level is 33, i.e. a chain of 34 user namespaces including the initial one. */
+#define USER_NAMESPACE_DEPTH_MAX 34U
-struct mnt_id_map {
- __uint(type, BPF_MAP_TYPE_HASH);
- __uint(max_entries, 1); /* placeholder, configured otherwise by nsresourced */
- __type(key, int);
- __type(value, int);
-};
+/* Mirrors UID_GID_MAP_MAX_BASE_EXTENTS/UID_GID_MAP_MAX_EXTENTS, see include/linux/user_namespace.h */
+#define UID_GID_MAP_MAX_BASE_EXTENTS 5U
+#define UID_GID_MAP_MAX_EXTENTS 340U
+
+/* The kernel's INVALID_UID/INVALID_GID, i.e. what an unmapped translation results in */
+#define UID_GID_INVALID ((uid_t) -1)
+
+/* From include/uapi/linux/magic.h, which vmlinux.h does not provide. */
+#define OVERLAYFS_SUPER_MAGIC 0x794c7630
struct {
- __uint(type, BPF_MAP_TYPE_HASH_OF_MAPS);
+ __uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1); /* placeholder, configured otherwise by nsresourced */
__type(key, unsigned); /* userns inode */
- __array(values, struct mnt_id_map);
-} userns_mnt_id_hash SEC(".maps");
+ __type(value, int); /* dummy value */
+} userns_managed SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
@@ -74,10 +87,6 @@ struct {
__uint(max_entries, 4096);
} userns_ringbuf SEC(".maps");
-static inline struct mount *real_mount(struct vfsmount *mnt) {
- return container_of(mnt, struct mount, mnt);
-}
-
static inline bool uid_is_dynamic(uid_t uid) {
return DYNAMIC_UID_MIN <= uid && uid <= DYNAMIC_UID_MAX;
}
@@ -90,17 +99,43 @@ static inline bool uid_is_transient(uid_t uid) {
return uid_is_dynamic(uid) || uid_is_container(uid);
}
-static int userns_owns_mount(struct user_namespace *userns, struct vfsmount *v) {
- struct user_namespace *mount_userns, *p;
- struct mount *m;
+/* Finds the closest user namespace at or above the passed one that nsresourced provisioned. Walking upwards
+ * is what keeps a nested user namespace from shaking off the policy. Returns 1 and stores it in the return
+ * argument if found, 0 if the whole chain was walked without a match, and a negative error if the chain is
+ * longer than the kernel permits (in which case we refuse rather than fail open, as we cannot rule out a
+ * managed ancestor). */
+static int find_managed_userns(struct user_namespace *userns, struct user_namespace **ret) {
+ struct user_namespace *p = userns;
- /* Get user namespace from vfsmount */
- m = bpf_rdonly_cast(real_mount(v), bpf_core_type_id_kernel(struct mount));
- mount_userns = m->mnt_ns->user_ns;
+ /* All callers pass a valid pointer; the guard just satisfies the pointer-deref checker. */
+ if (!ret)
+ return -EINVAL;
- p = mount_userns;
for (unsigned i = 0; i < USER_NAMESPACE_DEPTH_MAX; i++) {
- if (p == userns)
+ unsigned inode;
+
+ inode = p->ns.inum;
+ if (bpf_map_lookup_elem(&userns_managed, &inode)) {
+ *ret = p;
+ return true;
+ }
+
+ p = p->parent;
+ if (!p) /* Walked the whole chain up to the initial namespace without a match. */
+ return false;
+ }
+
+ /* Chain longer than the kernel permits: refuse rather than fail open, as we cannot rule out a
+ * managed ancestor. */
+ return -EPERM;
+}
+
+/* Is the second user namespace the first one itself, or nested below it? */
+static int userns_is_below(struct user_namespace *ancestor, struct user_namespace *ns) {
+ struct user_namespace *p = ns;
+
+ for (unsigned i = 0; i < USER_NAMESPACE_DEPTH_MAX; i++) {
+ if (p == ancestor)
return true;
p = p->parent;
@@ -108,7 +143,7 @@ static int userns_owns_mount(struct user_namespace *userns, struct vfsmount *v)
break;
}
- /* Hmm, something is fishy if there's more than 32 levels of namespaces involved. Let's better be
+ /* Hmm, something is fishy if there's more than 34 levels of namespaces involved. Let's better be
* safe than sorry, and refuse. */
if (p)
return -EPERM;
@@ -116,68 +151,132 @@ static int userns_owns_mount(struct user_namespace *userns, struct vfsmount *v)
return false;
}
-static int validate_mount(struct vfsmount *v, int ret) {
- struct user_namespace *task_userns;
- unsigned task_userns_inode;
- const struct cred *cred;
- struct task_struct *task;
- void *mnt_id_map;
- struct mount *m;
- int mnt_id, r;
+/* Resolves a uid_gid_map's extent array for the requested direction. Small maps carry their extents inline,
+ * larger ones keep two arrays instead, each sorted by one end (the kernel bisects them, a linear scan
+ * arrives at the same answer). Those two pointers share storage with the inline extent array; the verifier
+ * resolves the overlap to the latter and then refuses an 8 byte load in the middle of it, so we read the
+ * wanted pointer out explicitly. The bpf_core_field_offset() operands have to stay compile time constants,
+ * hence the branch per direction. always_inline so each caller keeps the hand-inlined shape the verifier
+ * already accepts (a register that is a BTF pointer on one branch and a scalar on the other). Returns NULL
+ * for an empty map or on a read failure. */
+static inline __attribute__((always_inline))
+struct uid_gid_extent *uid_gid_map_extents(struct uid_gid_map *map, bool up) {
+ struct uid_gid_extent *array;
- if (ret != 0) /* propagate earlier error */
- return ret;
+ if (map->nr_extents == 0)
+ return NULL;
- /* Get user namespace from task */
- task = (struct task_struct*) bpf_get_current_task_btf();
- cred = task->cred;
- if (!cred)
- return -EPERM;
- task_userns = cred->user_ns;
+ if (map->nr_extents <= UID_GID_MAP_MAX_BASE_EXTENTS)
+ return map->extent;
- /* fsuid/fsgid are the UID/GID in the initial user namespace, before any idmapped mounts have been
- * applied. There is no way (yet) to figure out what the UID/GID that will be written to disk will be
- * after idmapped mounts are taken into account, hence we have to rely on an allowlist of mounts
- * populated by userspace which tells us if a mount has an appropriate uid mapping in place to
- * translate the transient UID range to something else. For other UIDs/GIDs, there's no need to do
- * these checks as we don't insist on idmapped mounts or such for UIDs/GIDs outside the transient
- * ranges. */
- if (!uid_is_transient(cred->fsuid.val) && !uid_is_transient((uid_t) cred->fsgid.val))
- return 0;
+ if (up) {
+ if (bpf_probe_read_kernel(&array, sizeof(array),
+ (const void*) map + bpf_core_field_offset(struct uid_gid_map, reverse)) != 0)
+ return NULL;
+ } else {
+ if (bpf_probe_read_kernel(&array, sizeof(array),
+ (const void*) map + bpf_core_field_offset(struct uid_gid_map, forward)) != 0)
+ return NULL;
+ }
- r = userns_owns_mount(task_userns, v);
- if (r < 0)
- return r;
- /* Is the file on a mount that belongs to our own user namespace or a child of it? If so, say
- * yes immediately. */
- if (r > 0)
- return 0;
-
- /* This is a mount foreign to our task's user namespace, let's consult our allow list */
- task_userns_inode = task_userns->ns.inum;
-
- mnt_id_map = bpf_map_lookup_elem(&userns_mnt_id_hash, &task_userns_inode);
- if (!mnt_id_map) /* No rules installed for this userns? Then say yes, too! */
- return 0;
-
- m = bpf_rdonly_cast(real_mount(v), bpf_core_type_id_kernel(struct mount));
- mnt_id = m->mnt_id;
-
- /* Otherwise, say yes if the mount ID is allowlisted */
- if (bpf_map_lookup_elem(mnt_id_map, &mnt_id))
- return 0;
-
- return -EPERM;
+ return array;
}
-SEC("lsm/path_chown")
-int BPF_PROG(userns_restrict_path_chown, struct path *path, unsigned long long uid, unsigned long long gid, int ret) {
- struct user_namespace *task_userns;
- unsigned task_userns_inode;
+/* Mirrors map_id_range_up()/map_id_range_down() from kernel/user_namespace.c: translates the passed ID
+ * through the map, where the up flag selects which end of the extents is searched. Stores UID_GID_INVALID if
+ * the ID is not mapped, and returns negative if the map could not be read, which callers must keep apart: an
+ * unmapped ID records nothing on disk, whereas a map we failed to read tells us nothing at all. */
+static int uid_gid_map_translate(struct uid_gid_map *map, uid_t id, bool up, uid_t *ret) {
+ struct uid_gid_extent *array;
+ unsigned n;
+
+ /* All callers pass a valid pointer; the guard just satisfies the pointer-deref checker. */
+ if (!ret)
+ return -EINVAL;
+
+ n = map->nr_extents;
+ if (n == 0) { /* Empty map: nothing is mapped, and that is an answer, not a failure. */
+ *ret = UID_GID_INVALID;
+ return 0;
+ }
+
+ array = uid_gid_map_extents(map, up);
+ if (!array) /* Non-empty map whose extents we could not resolve. */
+ return -EPERM;
+
+ /* Copy each extent out instead of indexing the array in place: a variable index into a kernel
+ * pointer is something the verifier refuses. */
+ for (unsigned i = 0; i < UID_GID_MAP_MAX_EXTENTS; i++) {
+ struct uid_gid_extent e;
+ uid_t first;
+
+ if (i >= n)
+ break;
+
+ if (bpf_probe_read_kernel(&e, sizeof(e), array + i) != 0)
+ return -EPERM;
+
+ first = up ? e.lower_first : e.first;
+ if (id >= first && id - first < e.count) {
+ *ret = (up ? e.first : e.lower_first) + (id - first);
+ return 0;
+ }
+ }
+
+ *ret = UID_GID_INVALID;
+ return 0;
+}
+
+/* Mirrors from_vfsuid()/from_vfsgid() from fs/mnt_idmapping.c: the ID that is actually recorded on disk when
+ * the passed ID creates an object on the given mount. The gid flag selects the GID maps over the UID maps.
+ * Stores UID_GID_INVALID if the ID does not map, in which case the kernel refuses the operation with
+ * EOVERFLOW anyway, see fsuidgid_has_mapping(). Returns negative if a map could not be read. */
+static int mount_map_id(struct vfsmount *mnt, struct user_namespace *fs_userns, uid_t id, bool gid, uid_t *ret) {
+ struct mnt_idmap *idmap;
+ struct uid_gid_map *m;
+ int r;
+
+ /* All callers pass a valid pointer; the guard just satisfies the pointer-deref checker. */
+ if (!ret)
+ return -EINVAL;
+
+ idmap = mnt->mnt_idmap;
+ if (!idmap) {
+ *ret = id;
+ return 0;
+ }
+
+ m = gid ? &idmap->gid_map : &idmap->uid_map;
+
+ /* Both nop_mnt_idmap and invalid_mnt_idmap carry empty maps. For the former that is an identity
+ * mapping; for the latter the operation fails later on regardless, so treating it as identity only
+ * ever makes us stricter. */
+ if (m->nr_extents > 0) {
+ r = uid_gid_map_translate(m, id, /* up= */ true, &id);
+ if (r < 0)
+ return r;
+ if (id == UID_GID_INVALID) {
+ *ret = UID_GID_INVALID;
+ return 0;
+ }
+ }
+
+ /* initial_idmapping(): the initial user namespace is the only one without a parent. */
+ if (!fs_userns->parent) {
+ *ret = id;
+ return 0;
+ }
+
+ return uid_gid_map_translate(gid ? &fs_userns->gid_map : &fs_userns->uid_map, id, /* up= */ false, ret);
+}
+
+/* Common tail of the inode creation hooks: would the object the calling task is about to create end up
+ * owned by a transient ID on a file system that outlives the user namespace that ID was handed to? */
+static int validate_mount(struct vfsmount *v, int ret) {
+ struct user_namespace *managed, *fs_userns;
const struct cred *cred;
struct task_struct *task;
- struct vfsmount *v;
- void *mnt_id_map;
+ uid_t uid, gid;
int r;
if (ret != 0) /* propagate earlier error */
@@ -188,34 +287,79 @@ int BPF_PROG(userns_restrict_path_chown, struct path *path, unsigned long long u
cred = task->cred;
if (!cred)
return -EPERM;
- task_userns = cred->user_ns;
- v = path->mnt;
- r = userns_owns_mount(task_userns, v);
+ /* fsuid/fsgid are the UID/GID in the initial user namespace. Anything outside the transient ranges
+ * is never recycled by us, hence there's nothing to protect. */
+ if (!uid_is_transient(cred->fsuid.val) && !uid_is_transient((uid_t) cred->fsgid.val))
+ return 0;
+
+ r = find_managed_userns(cred->user_ns, &managed);
+ if (r < 0)
+ return r;
+ if (r == 0) /* Not provisioned by nsresourced? Then this is none of our business. */
+ return 0;
+
+ fs_userns = v->mnt_sb->s_user_ns;
+
+ /* Does the file system die together with the user namespace? Then nothing it carries can outlive
+ * the transient range, and we can allow this. */
+ r = userns_is_below(managed, fs_userns);
if (r < 0)
return r;
- /* Is the file on a mount that belongs to our own user namespace or a child of it? If so, say
- * yes immediately. */
if (r > 0)
return 0;
- /* This is a mount foreign to our task's user namespace, if the user namespace was provisioned by
- * nsresourced, refuse any UIDs/GIDs in the transient ranges. Note that we can only do this check in
- * the chown() hook because it receives the UID/GID with idmapped mounts already taken into account,
- * unlike the other hooks where we cannot (yet) figure out the UID/GID after idmapped mounts are
- * applied. Hence in the other hooks we have to rely on the mount allowlist to ensure the transient
- * fsuid/fsgid will be translated to something else when written to disk but in the chown() hook we
- * can check the provided UID/GID directly to see if it is transient or not. */
+ /* Refuse if we cannot resolve where the IDs land: an ID we failed to translate is not an ID we know
+ * to be harmless. */
+ r = mount_map_id(v, fs_userns, cred->fsuid.val, /* gid= */ false, &uid);
+ if (r < 0)
+ return r;
+
+ r = mount_map_id(v, fs_userns, (uid_t) cred->fsgid.val, /* gid= */ true, &gid);
+ if (r < 0)
+ return r;
+
+ /* If the mount's idmapping translates our transient IDs into something else, nothing recyclable is
+ * recorded on disk, and we are fine. */
+ if (!uid_is_transient(uid) && !uid_is_transient(gid))
+ return 0;
+
+ return -EPERM;
+}
+
+SEC("lsm/path_chown")
+int BPF_PROG(userns_restrict_path_chown, struct path *path, unsigned long long uid, unsigned long long gid, int ret) {
+ struct user_namespace *managed;
+ const struct cred *cred;
+ struct task_struct *task;
+ int r;
+
+ if (ret != 0) /* propagate earlier error */
+ return ret;
+
+ /* Get user namespace from task */
+ task = (struct task_struct*) bpf_get_current_task_btf();
+ cred = task->cred;
+ if (!cred)
+ return -EPERM;
/* User namespaces that were not provisioned by nsresourced can still write to the transient ranges
* so that we don't break use cases like systemd-nspawn's --private-users=pick switch. */
-
- task_userns_inode = task_userns->ns.inum;
-
- mnt_id_map = bpf_map_lookup_elem(&userns_mnt_id_hash, &task_userns_inode);
- if (!mnt_id_map) /* No rules installed for this userns? Then say yes, too! */
+ r = find_managed_userns(cred->user_ns, &managed);
+ if (r < 0)
+ return r;
+ if (r == 0)
return 0;
+ r = userns_is_below(managed, path->mnt->mnt_sb->s_user_ns);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ return 0;
+
+ /* Unlike the creation hooks this one is handed the IDs with the mount's idmapping already applied,
+ * i.e. the ones that end up on disk, hence we can look at them directly. See chown_common() in the
+ * kernel sources. */
if (uid_is_transient((uid_t) uid) || uid_is_transient((uid_t) gid))
return -EPERM;
@@ -244,6 +388,173 @@ int BPF_PROG(userns_restrict_path_link, struct dentry *old_dentry, const struct
return validate_mount(new_dir->mnt, ret);
}
+/* overlayfs' struct ovl_fs and struct ovl_layer are private to the module and hence absent from vmlinux.h
+ * whenever overlayfs is built as a module. We declare the two fields we need as CO-RE "type flavors": libbpf
+ * strips the ___local suffix and relocates these against the kernel's real ovl_fs/ovl_layer. This both
+ * avoids clashing with a vmlinux.h that does carry them (built-in overlayfs) and, combined with
+ * bpf_core_type_exists() below, lets the program still load when overlayfs is entirely unavailable.
+ * systemd-nsresourced.service orders itself after modprobe@overlay.service so the module's BTF is present by
+ * the time this program is loaded. @mnt is documented in the kernel to be the first member of ovl_layer. */
+struct ovl_layer___local {
+ struct vfsmount *mnt;
+} __attribute__((preserve_access_index));
+
+struct ovl_fs___local {
+ struct ovl_layer___local *layers;
+} __attribute__((preserve_access_index));
+
+/* True if the inclusive range [a, b] overlaps either transient id range. */
+static bool range_hits_transient(uint64_t a, uint64_t b) {
+ if (a <= DYNAMIC_UID_MAX && DYNAMIC_UID_MIN <= b)
+ return true;
+ if (a <= CONTAINER_UID_MAX && CONTAINER_UID_MIN <= b)
+ return true;
+ return false;
+}
+
+/* Returns true if the map (a mount idmap's uid_map or gid_map) never records a transient id on disk, so
+ * nothing recyclable can reach the disk through it. Read in the "up" direction (i.e. as a mount idmap) an
+ * extent maps input ids [lower_first, lower_first+count) to on-disk ids [first, first+count); we refuse if
+ * any extent's on-disk range is transient, no matter which input reaches it. Ids no extent covers are left
+ * unmapped, which fails the write with EOVERFLOW, so they are fine too. An empty map is the identity
+ * mapping and hence neutralizes nothing. */
+static bool idmap_map_neutralizes_transient(struct uid_gid_map *map) {
+ struct uid_gid_extent *array;
+ unsigned n;
+
+ n = map->nr_extents;
+
+ array = uid_gid_map_extents(map, /* up= */ true);
+ /* NULL means an empty map (identity mapping, neutralizes nothing) or a read failure; refuse either. */
+ if (!array)
+ return false;
+
+ for (unsigned i = 0; i < UID_GID_MAP_MAX_EXTENTS; i++) {
+ struct uid_gid_extent e;
+
+ if (i >= n)
+ break;
+ if (bpf_probe_read_kernel(&e, sizeof(e), array + i) != 0)
+ return false;
+
+ if (e.count == 0)
+ continue;
+
+ /* The on-disk ids this extent can record are [first, first+count). If any is transient,
+ * some write through this mount is recyclable, so the map does not neutralize. */
+ if (range_hits_transient(e.first, (uint64_t) e.first + e.count - 1))
+ return false;
+ }
+
+ return true;
+}
+
+/* True if the mount's idmapping maps the whole transient UID and GID ranges away, so a transient client
+ * writing through it leaves nothing recyclable on disk. A non-idmapped mount maps them through unchanged. */
+static bool mount_neutralizes_transient(struct vfsmount *mnt) {
+ struct mnt_idmap *idmap;
+
+ idmap = mnt->mnt_idmap;
+ if (!idmap)
+ return false;
+
+ return idmap_map_neutralizes_transient(&idmap->uid_map) &&
+ idmap_map_neutralizes_transient(&idmap->gid_map);
+}
+
+/* overlayfs would let a managed user namespace sidestep all of the above: a write to an overlay mount
+ * creates the real inode on the upper layer via vfs_create(), which reaches only the inode-based LSM hooks,
+ * not the path-based ones we attach to. The single path hook that does fire sees the overlay mount itself,
+ * whose superblock belongs to the (managed) mounting namespace and so looks like it dies with it, while the
+ * upper may be a persistent file system on which our transient IDs then persist. So we judge the upper
+ * here, at mount time, the only point we can see it. We permit the overlay if its upper dies with the
+ * managed namespace (e.g. a tmpfs the client set up itself), or if the upper is a persistent file system
+ * whose mount idmapping maps the transient ranges away so nothing recyclable reaches its disk (as
+ * mountfsd-style idmapped mounts do); everything else, and any overlay owned by a namespace we do not
+ * manage, is refused.
+ *
+ * We key on whether a writable upper exists (layers[0].mnt), which is fixed for the superblock's lifetime,
+ * not on the mutable SB_RDONLY flag: an overlay mounted read-only but with an upper can be remounted
+ * read-write later without re-entering this hook, so it is judged now. A genuinely read-only overlay has no
+ * upper (layers[0].mnt is NULL) and cannot record anything, so it is left alone. Refusing the mount cannot
+ * undo the work/index dirs overlayfs already stamped onto the upper during ovl_fill_super(), which runs
+ * before this hook; it stops all further writes, but those mount-time dirs remain a residual leak. Reaching
+ * it takes an upperdir and a workdir that already exist on the same persistent, non-idmapped file system and
+ * that the transient IDs may write to: the client cannot create them itself, since that is what the path
+ * hooks deny.
+ *
+ * We only look at the upper: overlayfs requires the workdir to be under the same mount as the upperdir
+ * (ovl_get_workdir()), and stamps work/index using the upper mount's idmapping (ovl_upper_mnt_idmap() ==
+ * mnt_idmap(layers[0].mnt)), so the upper's superblock and idmapping already govern the workdir too. */
+SEC("lsm/sb_kern_mount")
+int BPF_PROG(userns_restrict_sb_kern_mount, struct super_block *sb, int ret) {
+ struct ovl_layer___local *layers;
+ struct user_namespace *managed, *fs_userns;
+ struct ovl_fs___local *ofs;
+ struct vfsmount *upper;
+ void *upper_addr;
+ int r;
+
+ if (ret != 0) /* propagate earlier error */
+ return ret;
+
+ if (sb->s_magic != OVERLAYFS_SUPER_MAGIC)
+ return 0;
+
+ r = find_managed_userns(sb->s_user_ns, &managed);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 0;
+
+ /* If we cannot read overlayfs' private layout (module not loaded, built without overlayfs at all,
+ * or the fields we need went away), we cannot locate the upper, so refuse conservatively. */
+ if (!bpf_core_type_exists(struct ovl_fs___local) ||
+ !bpf_core_field_exists(struct ovl_fs___local, layers) ||
+ !bpf_core_field_exists(struct ovl_layer___local, mnt))
+ return -EPERM;
+
+ /* Walk to the upper one field at a time: a failed read comes back as NULL just like a genuinely
+ * absent upper does, and only the latter may be allowed. A mounted overlay always has both its
+ * private data and its layer array, so NULL there means we failed to read them. */
+ ofs = (struct ovl_fs___local*) BPF_CORE_READ(sb, s_fs_info);
+ if (!ofs)
+ return -EPERM;
+
+ layers = BPF_CORE_READ(ofs, layers);
+ if (!layers)
+ return -EPERM;
+
+ upper_addr = BPF_CORE_READ(layers, mnt); /* layers[0] is the upper; .mnt is its first member */
+ if (!upper_addr) /* no upper: a genuinely read-only overlay, which can record nothing */
+ return 0;
+
+ /* BPF_CORE_READ() lowers to bpf_probe_read_kernel(), so its result is a raw address the verifier
+ * tracks as a scalar. Re-type it as a BTF pointer, as the retire_userns_sysctls kprobe does with
+ * its argument, so we may walk and dereference it directly below. */
+ upper = bpf_rdonly_cast(upper_addr, bpf_core_type_id_kernel(struct vfsmount));
+ fs_userns = upper->mnt_sb->s_user_ns;
+
+ /* Upper dies together with the managed user namespace? Then nothing it carries can outlive the
+ * transient range. */
+ r = userns_is_below(managed, fs_userns);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ return 0;
+
+ /* The upper outlives the namespace. It is still safe if it is a persistent file system in the
+ * initial user namespace whose mount idmapping maps the transient ranges away. We refuse anything
+ * we cannot prove this for, including any non-initial-namespace upper. */
+ if (fs_userns->parent)
+ return -EPERM;
+
+ if (mount_neutralizes_transient(upper))
+ return 0;
+
+ return -EPERM;
+}
+
SEC("lsm/task_fix_setgroups")
int BPF_PROG(userns_restrict_task_fix_setgroups, struct cred *new_cred, const struct cred *old, int ret) {
struct user_namespace *p;
@@ -253,27 +564,26 @@ int BPF_PROG(userns_restrict_task_fix_setgroups, struct cred *new_cred, const st
return ret;
/* Walk the task's user namespace and its ancestors to find the first one managed by nsresourced
- * (i.e. present in either the setgroups deny map or the mount ID hash map). This is necessary
+ * (i.e. present in either the setgroups deny map or the managed userns map). This is necessary
* because a task could otherwise trivially bypass the setgroups() restriction by unsharing the user
* namespace and mapping the same users and groups. */
p = new_cred->user_ns;
for (unsigned i = 0; i < USER_NAMESPACE_DEPTH_MAX; i++) {
- if (!p)
- break;
-
inode = p->ns.inum;
if (bpf_map_lookup_elem(&userns_setgroups_deny, &inode))
return -EPERM;
- if (bpf_map_lookup_elem(&userns_mnt_id_hash, &inode))
+ if (bpf_map_lookup_elem(&userns_managed, &inode))
return 0;
p = p->parent;
+ if (!p) /* Walked the whole chain up to the initial namespace without a match, allow. */
+ return 0;
}
- /* No nsresourced-managed ancestor found, allow. */
- return 0;
+ /* Chain longer than the kernel permits: refuse rather than fail open, like find_managed_userns(). */
+ return -EPERM;
}
SEC("kprobe/retire_userns_sysctls")
@@ -287,7 +597,7 @@ int BPF_KPROBE(userns_restrict_retire_userns_sysctls, struct user_namespace *use
/* Check each map separately to avoid the compiler merging the two lookups into a pointer OR
* operation, which the BPF verifier rejects. */
- if (bpf_map_lookup_elem(&userns_mnt_id_hash, &inode))
+ if (bpf_map_lookup_elem(&userns_managed, &inode))
goto notify;
if (bpf_map_lookup_elem(&userns_setgroups_deny, &inode))
diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c
index 55f36b1d86..7815f9b3bc 100644
--- a/src/dissect/dissect.c
+++ b/src/dissect/dissect.c
@@ -1563,7 +1563,7 @@ static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopD
output_fd = STDOUT_FILENO;
}
- r = tar_c(dfd, output_fd, arg_target, /* flags= */ 0);
+ r = tar_c(dfd, output_fd, arg_target, /* hardlink_db_fd= */ -EBADF, /* flags= */ 0);
if (r < 0)
return r;
diff --git a/src/import/import-common.c b/src/import/import-common.c
index ef2b3e26ba..0b63828869 100644
--- a/src/import/import-common.c
+++ b/src/import/import-common.c
@@ -137,6 +137,15 @@ int import_fork_tar_c(int tree_fd, int userns_fd, PidRef *ret_pid) {
if (unshare(CLONE_NEWNET) < 0)
log_debug_errno(errno, "Failed to lock tar into network namespace, ignoring: %m");
+ /* Allocate the hardlink db before dropping privileges: it lives on a tmpfs superblock of our
+ * own, which needs CAP_SYS_ADMIN to set up. Doing it later would leave tar_c() to fall back
+ * to a directory in /var/tmp/, which is refused for the transient UID range we run under
+ * here, and would leave transient owned inodes behind on a persistent file system if it
+ * weren't. */
+ _cleanup_close_ int hardlink_db_fd = tar_hardlink_db_new();
+ if (hardlink_db_fd < 0)
+ log_debug_errno(hardlink_db_fd, "Failed to allocate hardlink db, ignoring: %m");
+
r = capability_bounding_set_drop(retain, true);
if (r < 0)
log_debug_errno(r, "Failed to drop capabilities, ignoring: %m");
@@ -145,7 +154,7 @@ int import_fork_tar_c(int tree_fd, int userns_fd, PidRef *ret_pid) {
if (r < 0)
log_warning_errno(r, "Failed to enable PR_SET_NO_NEW_PRIVS, ignoring: %m");
- if (tar_c(tree_fd, pipefd[1], /* filename= */ NULL, flags) < 0)
+ if (tar_c(tree_fd, pipefd[1], /* filename= */ NULL, hardlink_db_fd, flags) < 0)
_exit(EXIT_FAILURE);
_exit(EXIT_SUCCESS);
diff --git a/src/import/test-tar.c b/src/import/test-tar.c
index b7cfed9bf3..6fca4b89cb 100644
--- a/src/import/test-tar.c
+++ b/src/import/test-tar.c
@@ -46,7 +46,7 @@ static int run(int argc, char **argv) {
return log_error_errno(fd2, "Cannot open %s: %m", argv[3]);
if (create)
- r = tar_c(fd2, fd1, argv[2], /* flags= */ TAR_SELINUX);
+ r = tar_c(fd2, fd1, argv[2], /* hardlink_db_fd= */ -EBADF, /* flags= */ TAR_SELINUX);
else
r = tar_x(fd1, fd2, /* flags= */ TAR_SELINUX);
if (r < 0)
diff --git a/src/nsresourced/nsresourced-manager.c b/src/nsresourced/nsresourced-manager.c
index df12e352b0..41436e5552 100644
--- a/src/nsresourced/nsresourced-manager.c
+++ b/src/nsresourced/nsresourced-manager.c
@@ -372,6 +372,52 @@ static void manager_release_userns_by_inode(Manager *m, uint64_t inode) {
userns_registry_release_by_userns_inode(manager_bpf(m), m->registry_fd, inode);
}
+/* Re-adds a registered, still-alive user namespace to the BPF maps. This is idempotent when the pinned
+ * maps already carry the entry (the common restart case), and load-bearing after an nsresourced upgrade
+ * that had to replace the pinned maps because their schema changed: the new maps come up empty, so
+ * without this the namespaces still alive from before the upgrade would silently lose their write
+ * restriction (and, for "self" allocations, their setgroups() denial). */
+/* Borrows the registry entry if the caller already has it at hand, and loads its own copy otherwise. */
+static void manager_restore_userns_policy(Manager *m, uint64_t inode, UserNamespaceInfo *info) {
+ _cleanup_(userns_info_freep) UserNamespaceInfo *loaded = NULL;
+ struct userns_restrict_bpf *bpf;
+ int r;
+
+ assert(m);
+ assert(inode != 0);
+
+ bpf = manager_bpf(m);
+ if (!bpf) /* BPF disabled at build time or its setup failed — nothing to restore. */
+ return;
+
+ if (!info) {
+ r = userns_registry_load_by_userns_inode(m->registry_fd, inode, &loaded);
+ if (r < 0)
+ return (void) log_debug_errno(r, "Failed to load registry entry for user namespace %" PRIu64 " while restoring BPF policy, ignoring: %m", inode);
+
+ info = loaded;
+ }
+
+ r = userns_restrict_register_by_inode(bpf, inode);
+ if (r < 0)
+ return (void) log_debug_errno(r, "Failed to restore write restriction for user namespace %" PRIu64 ", ignoring: %m", inode);
+
+ /* Entries written before we recorded this carry no answer, and those are precisely the entries an
+ * upgrade finds. Fall back to what the flag mirrors: a "self" allocation maps the caller's own UID
+ * instead of a transient range, so it is the one whose start UID is its owner and outside the
+ * ranges we hand out. Without that last condition a managed size-1 allocation that happened to be
+ * given its own caller's UID would be mistaken for one, and we would deny setgroups() on a
+ * namespace that never had it denied. */
+ bool deny = info->setgroups_deny >= 0 ? info->setgroups_deny :
+ (info->size == 1 && info->start_uid == info->owner && !uid_is_transient(info->start_uid));
+
+ if (deny) {
+ r = userns_restrict_setgroups_deny_by_inode(bpf, inode);
+ if (r < 0)
+ log_debug_errno(r, "Failed to restore setgroups() denial for user namespace %" PRIu64 ", ignoring: %m", inode);
+ }
+}
+
static int manager_scan_registry(Manager *m, Set **registry_inodes) {
_cleanup_free_ DirectoryEntries *de = NULL;
int r;
@@ -602,11 +648,32 @@ static int manager_setup_bpf(Manager *m) {
return 0;
}
+
+/* Puts the programs loaded by manager_setup_bpf() in effect. Deliberately separate, so that the maps are
+ * already seeded from the registry by the time the first hook runs. */
+static int manager_attach_bpf(Manager *m) {
+ int r;
+
+ assert(m);
+
+ if (!m->userns_restrict_bpf) /* Setup failed earlier, and we already told the user. */
+ return 0;
+
+ r = userns_restrict_attach(m->userns_restrict_bpf, /* pin= */ true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach BPF programs: %m");
+
+ return 0;
+}
#else
static int manager_setup_bpf(Manager *m) {
log_notice("Not setting up BPF subsystem, as functionality has been disabled at compile time.");
return 0;
}
+
+static int manager_attach_bpf(Manager *m) {
+ return 0;
+}
#endif
int manager_startup(Manager *m) {
@@ -647,33 +714,55 @@ int manager_startup(Manager *m) {
manager_release_userns_by_inode(m, inode);
}
- /* Look for registry entries whose user namespace has died without us getting a BPF
- * notification — e.g. because the BPF ring buffer overflowed, the kprobe is missing, or
- * something else dropped the fd store entry without going through our cleanup path. Each
- * registry entry stores the kernel's unique namespace identifier; ask the kernel to open
- * the namespace by that identifier and release the entry if the lookup fails. Entries
- * written by older versions don't carry the identifier, and old kernels (or running
- * outside the initial user namespace) don't support lookup by it — in those cases we leave
- * the entry alone. */
+ /* Walk the registry entries for two purposes: reap the dead ones, and re-establish the
+ * BPF-LSM policy for the live ones.
+ *
+ * A namespace may have died without us getting a BPF notification — e.g. because the BPF ring
+ * buffer overflowed, the kprobe is missing, or something else dropped the fd store entry without
+ * going through our cleanup path. Each registry entry stores the kernel's unique namespace
+ * identifier; ask the kernel to open the namespace by that identifier and release the entry if
+ * the lookup fails. Entries written by older versions don't carry the identifier, and old kernels
+ * (or running outside the initial user namespace) don't support lookup by it — in those cases we
+ * leave the entry alone.
+ *
+ * Everything that isn't reaped is (re-)added to the BPF maps. Normally the pinned maps already
+ * carry these entries and the update is a no-op, but after an upgrade that replaced the pinned
+ * maps with an incompatible schema the new maps start empty, and this is what keeps namespaces
+ * that predate the upgrade protected. */
+ bool can_probe = true;
SET_FOREACH(p, registry_inodes) {
+ _cleanup_(userns_info_freep) UserNamespaceInfo *info = NULL;
uint64_t inode = PTR_TO_UINT32(p);
- r = userns_registry_reap_if_dead(manager_bpf(m), m->registry_fd, inode);
- if (r < 0) {
- log_debug_errno(r, "Failed to probe liveness of user namespace %" PRIu64 ", ignoring: %m", inode);
- continue;
+ if (can_probe) {
+ r = userns_registry_reap_if_dead(manager_bpf(m), m->registry_fd, inode, &info);
+ if (r < 0)
+ log_debug_errno(r, "Failed to probe liveness of user namespace %" PRIu64 ", assuming alive: %m", inode);
+ else if (r == USERNS_REAP_RELEASED)
+ continue; /* Dead and released — nothing to restore. */
+ else if (r == USERNS_REAP_UNSUPPORTED) {
+ /* Can't look namespaces up by id at all here (old kernel, or not in the
+ * initial user namespace). Stop probing (and logging) once per entry, but
+ * still restore the policy for this and the remaining entries below. */
+ log_debug("Cannot detect stale registry entries, restoring the rest unconditionally.");
+ can_probe = false;
+ }
+ /* USERNS_REAP_ALIVE or _INDETERMINATE fall through to the restore below. */
}
- if (r == USERNS_REAP_UNSUPPORTED) {
- /* Can't look namespaces up by id at all here (old kernel, or not in the
- * initial user namespace) — no entry is probeable, so stop rather than
- * continuing to probe (and log) once per entry. */
- log_debug("Cannot detect stale registry entries, skipping the rest.");
- break;
- }
- /* USERNS_REAP_RELEASED, _ALIVE, or _INDETERMINATE — nothing more to do for this entry. */
+
+ /* Re-establish the BPF-LSM policy for the still-live namespace. Idempotent when the pinned
+ * maps already carry it, essential after an upgrade replaced them with empty ones. */
+ manager_restore_userns_policy(m, inode, info);
}
+ /* The maps now describe every namespace we are responsible for, so the policy can go into
+ * effect. Until this point the previous version's programs, if any, were still attached and
+ * enforcing, so no namespace was ever left unguarded. */
+ r = manager_attach_bpf(m);
+ if (r < 0)
+ return r;
+
r = manager_make_listen_socket(m);
if (r < 0)
return r;
diff --git a/src/nsresourced/nsresourcework.c b/src/nsresourced/nsresourcework.c
index b8d68cbb42..6138f9cd0f 100644
--- a/src/nsresourced/nsresourcework.c
+++ b/src/nsresourced/nsresourcework.c
@@ -405,7 +405,7 @@ static int registry_range_is_available(
if (r < 0)
return r;
- r = userns_registry_reap_if_dead(bpf, registry_dir_fd, owner->userns_inode);
+ r = userns_registry_reap_if_dead(bpf, registry_dir_fd, owner->userns_inode, /* ret_info= */ NULL);
if (r < 0)
return r;
if (r == USERNS_REAP_RELEASED)
@@ -465,7 +465,7 @@ static int delegation_range_is_available(
/* Owned by some other namespace. If that namespace is dead, reclaim it (restoring the
* range to its ancestor) and loop to re-evaluate; otherwise the range is genuinely
* taken. */
- r = userns_registry_reap_if_dead(bpf, registry_dir_fd, delegation.userns_inode);
+ r = userns_registry_reap_if_dead(bpf, registry_dir_fd, delegation.userns_inode, /* ret_info= */ NULL);
if (r < 0)
return r;
if (r == USERNS_REAP_RELEASED)
@@ -1457,6 +1457,9 @@ static int vl_method_allocate_user_range(sd_varlink *link, sd_json_variant *para
userns_info->size = p.size;
userns_info->target_uid = p.target;
userns_info->target_gid = (gid_t) p.target;
+ /* For "self" allocations we deny setgroups() via the BPF-LSM (see below). Record it so the manager
+ * can re-establish the denial after a restart that reset the BPF maps. */
+ userns_info->setgroups_deny = p.type == ALLOCATE_USER_RANGE_SELF;
if (p.type == ALLOCATE_USER_RANGE_SELF) {
/* The start UID/GID will be mapped to the parent userns in write_userns(). If a self
@@ -1515,17 +1518,12 @@ static int vl_method_allocate_user_range(sd_varlink *link, sd_json_variant *para
if (r < 0)
return r;
- /* Register the userns in the BPF map with an empty allowlist */
- r = userns_restrict_put_by_fd(
- c->bpf,
- userns_fd,
- /* replace= */ true,
- /* mount_fds= */ NULL,
- /* n_mount_fds= */ 0);
+ /* Subject the userns to the BPF-LSM policy that keeps its transient range off persistent file systems */
+ r = userns_restrict_register_by_fd(c->bpf, userns_fd);
if (r < 0)
goto fail;
- if (p.type == ALLOCATE_USER_RANGE_SELF) {
+ if (userns_info->setgroups_deny > 0) {
/* For "self" allocations we deny setgroups() via the BPF LSM. We can't use
* /proc/self/setgroups for this as that is transitive and also applies to child user
* namespaces. The BPF LSM hook only applies to the specific user namespace. */
@@ -1742,13 +1740,8 @@ static int vl_method_register_user_namespace(sd_varlink *link, sd_json_variant *
if (r < 0)
return log_debug_errno(r, "Failed to update userns registry: %m");
- /* Register the userns in the BPF map with an empty allowlist */
- r = userns_restrict_put_by_fd(
- c->bpf,
- userns_fd,
- /* replace= */ true,
- /* mount_fds= */ NULL,
- /* n_mount_fds= */ 0);
+ /* Subject the userns to the BPF-LSM policy that keeps its transient range off persistent file systems */
+ r = userns_restrict_register_by_fd(c->bpf, userns_fd);
if (r < 0)
goto fail;
@@ -1887,22 +1880,17 @@ static int vl_method_add_mount_to_user_namespace(sd_varlink *link, sd_json_varia
if (r < 0)
return r;
- /* Add this mount to the user namespace's BPF map allowlist entry. */
- r = userns_restrict_put_by_fd(
- c->bpf,
- userns_fd,
- /* replace= */ false,
- &mount_fd,
- 1);
- if (r < 0)
- return r;
+ /* There's no mount allowlist anymore: the BPF-LSM policy decides per operation whether the ID that
+ * would be recorded on disk is a transient one, which needs no per-mount knowledge. We keep
+ * accepting and validating the call for the sake of older clients, and keep the mount pinned as
+ * before, but there is nothing left to allowlist. */
if (DEBUG_LOGGING) {
if (userns_info->size > 0)
- log_debug("Granting access to mount %i to user namespace " INO_FMT " ('%s' @ UID " UID_FMT ")",
+ log_debug("Delegated mount %i to user namespace " INO_FMT " ('%s' @ UID " UID_FMT ")",
mnt_id, userns_st.st_ino, userns_info->name, userns_info->start_uid);
else
- log_debug("Granting access to mount %i to user namespace " INO_FMT " ('%s')",
+ log_debug("Delegated mount %i to user namespace " INO_FMT " ('%s')",
mnt_id, userns_st.st_ino, userns_info->name);
}
diff --git a/src/nsresourced/test-userns-restrict.c b/src/nsresourced/test-userns-restrict.c
index 29125a84a8..69e70eabaf 100644
--- a/src/nsresourced/test-userns-restrict.c
+++ b/src/nsresourced/test-userns-restrict.c
@@ -11,12 +11,15 @@
#include "fd-util.h"
#include "fileio.h"
#include "namespace-util.h"
+#include "path-util.h"
#include "pidref.h"
#include "process-util.h"
#include "rm-rf.h"
+#include "string-util.h"
#include "tests.h"
#include "tmpfile-util.h"
#include "uid-classification.h"
+#include "user-util.h"
#include "userns-restrict.h"
static int make_tmpfs_fsmount(void) {
@@ -30,6 +33,22 @@ static int make_tmpfs_fsmount(void) {
return TAKE_FD(mntfd);
}
+/* A tmpfs on the host, but idmapped through the passed user namespace, i.e. the equivalent of what mountfsd
+ * hands to clients: our transient UID lands on disk as UID 0, so nothing recyclable is recorded. */
+static int make_idmapped_tmpfs_fsmount(int userns_fd) {
+ _cleanup_close_ int mntfd = -EBADF;
+
+ mntfd = ASSERT_OK(make_tmpfs_fsmount());
+
+ ASSERT_OK_ERRNO(mount_setattr(mntfd, "", AT_EMPTY_PATH,
+ &(struct mount_attr) {
+ .attr_set = MOUNT_ATTR_IDMAP,
+ .userns_fd = userns_fd,
+ }, sizeof(struct mount_attr)));
+
+ return TAKE_FD(mntfd);
+}
+
static struct userns_restrict_bpf *bpf_obj = NULL;
STATIC_DESTRUCTOR_REGISTER(bpf_obj, userns_restrict_bpf_freep);
@@ -43,11 +62,14 @@ static int intro(void) {
return log_tests_skipped("Lacking privileges");
ASSERT_OK(r);
+ ASSERT_OK(userns_restrict_attach(bpf_obj, /* pin= */ false));
+
return 0;
}
TEST(userns_restrict) {
- _cleanup_close_ int userns_fd = -EBADF, host_fd1 = -EBADF, host_tmpfs = -EBADF, afd = -EBADF, bfd = -EBADF;
+ _cleanup_close_ int userns_fd = -EBADF, host_fd1 = -EBADF, host_tmpfs = -EBADF,
+ idmapped_tmpfs = -EBADF;
_cleanup_(rm_rf_physical_and_freep) char *t = NULL;
_cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
int r;
@@ -64,15 +86,9 @@ TEST(userns_restrict) {
ASSERT_OK(asprintf(&idmap, "0 "UID_FMT" 1", CONTAINER_UID_MIN));
userns_fd = ASSERT_OK(userns_acquire(idmap, idmap, /* setgroups_deny= */ true));
- ASSERT_OK(userns_restrict_put_by_fd(
- bpf_obj,
- userns_fd,
- /* replace= */ true,
- /* mount_fds= */ NULL,
- /* n_mount_fds= */ 0));
+ idmapped_tmpfs = ASSERT_OK(make_idmapped_tmpfs_fsmount(userns_fd));
- afd = ASSERT_OK_ERRNO(eventfd(0, EFD_CLOEXEC));
- bfd = ASSERT_OK_ERRNO(eventfd(0, EFD_CLOEXEC));
+ ASSERT_OK(userns_restrict_register_by_fd(bpf_obj, userns_fd));
r = ASSERT_OK(pidref_safe_fork("(test)", FORK_DEATHSIG_SIGKILL, &pidref));
if (r == 0) {
@@ -81,10 +97,10 @@ TEST(userns_restrict) {
ASSERT_OK(namespace_enter(-EBADF, -EBADF, -EBADF, userns_fd, -EBADF));
ASSERT_OK_ERRNO(unshare(CLONE_NEWNS));
- /* Allocate tmpfs locally */
+ /* Allocate tmpfs locally, i.e. a file system that dies together with our user namespace */
private_tmpfs = make_tmpfs_fsmount();
- /* These two host mounts should be inaccessible */
+ /* These two host mounts would record our transient UID on disk, hence are inaccessible */
ASSERT_ERROR_ERRNO(openat(host_fd1, "test", O_RDWR|O_CREAT|O_CLOEXEC, 0666), EPERM);
ASSERT_ERROR_ERRNO(openat(host_tmpfs, "xxx", O_RDWR|O_CREAT|O_CLOEXEC, 0666), EPERM);
ASSERT_ERROR_ERRNO(mkdirat(host_fd1, "test2", 0666), EPERM);
@@ -94,61 +110,242 @@ TEST(userns_restrict) {
safe_close(ASSERT_OK_ERRNO(openat(private_tmpfs, "yyy", O_RDWR|O_CREAT|O_CLOEXEC, 0666)));
ASSERT_OK_ERRNO(mkdirat(private_tmpfs, "yyy2", 0666));
- /* Let's sync with the parent, so that it allowlists more stuff for us */
- ASSERT_OK_ERRNO(eventfd_write(afd, 1));
- uint64_t x;
- ASSERT_OK_ERRNO(eventfd_read(bfd, &x));
+ /* And so should the idmapped host mount: it translates our transient UID to UID 0 before
+ * anything reaches the disk */
+ safe_close(ASSERT_OK_ERRNO(openat(idmapped_tmpfs, "zzz", O_RDWR|O_CREAT|O_CLOEXEC, 0666)));
+ ASSERT_OK_ERRNO(mkdirat(idmapped_tmpfs, "zzz2", 0666));
- /* And now we should also have access to the host tmpfs */
- safe_close(ASSERT_OK_ERRNO(openat(host_tmpfs, "zzz", O_RDWR|O_CREAT|O_CLOEXEC, 0666)));
+ /* chown() is subject to the same policy. Unlike the creation hooks it is handed the ID that
+ * ends up on disk, so it needs no idmap arithmetic of its own. Setting a group the inode
+ * already has still reaches the hook, before the change is applied. Giving the host
+ * directory (which outlives us) our transient GID would leave something recyclable behind,
+ * hence is refused... */
+ ASSERT_ERROR_ERRNO(fchownat(host_fd1, "", GID_INVALID, 0, AT_EMPTY_PATH), EPERM);
+
+ /* ...while the same chown() is fine on the file system that dies with us, and on the
+ * idmapped mount where our transient GID is translated to 0 before it reaches the disk. */
+ ASSERT_OK_ERRNO(fchownat(private_tmpfs, "yyy", GID_INVALID, 0, 0));
+ ASSERT_OK_ERRNO(fchownat(idmapped_tmpfs, "zzz", GID_INVALID, 0, 0));
+
+ /* Unsharing a mount namespace hands us clones of every mount we can see, owned by our own
+ * user namespace. That must not buy us anything.
+ *
+ * Note that this has to go through a path, not through the directory fd opened above: an
+ * already open fd pins the original mount, while a path lookup lands on the clone, and it
+ * is the clone that used to be mistaken for one of ours. */
+ ASSERT_OK_ERRNO(unshare(CLONE_NEWNS));
+
+ _cleanup_free_ char *p1 = NULL, *p2 = NULL;
+ ASSERT_NOT_NULL(p1 = path_join(t, "newns"));
+ ASSERT_NOT_NULL(p2 = path_join(t, "newns2"));
+
+ ASSERT_ERROR_ERRNO(open(p1, O_RDWR|O_CREAT|O_CLOEXEC, 0666), EPERM);
+ ASSERT_ERROR_ERRNO(mkdir(p2, 0666), EPERM);
+ ASSERT_ERROR_ERRNO(openat(host_fd1, "newns3", O_RDWR|O_CREAT|O_CLOEXEC, 0666), EPERM);
+ ASSERT_ERROR_ERRNO(openat(host_tmpfs, "newns4", O_RDWR|O_CREAT|O_CLOEXEC, 0666), EPERM);
+
+ /* And chown() through the clone must stay refused too, not just inode creation. */
+ ASSERT_ERROR_ERRNO(fchownat(AT_FDCWD, t, GID_INVALID, 0, 0), EPERM);
+
+ /* Neither must nesting a user namespace, which is not in the managed map itself */
+ ASSERT_OK_ERRNO(unshare(CLONE_NEWUSER));
+
+ _cleanup_free_ char *p3 = NULL, *p4 = NULL;
+ ASSERT_NOT_NULL(p3 = path_join(t, "newuser"));
+ ASSERT_NOT_NULL(p4 = path_join(t, "newuser2"));
+
+ ASSERT_ERROR_ERRNO(open(p3, O_RDWR|O_CREAT|O_CLOEXEC, 0666), EPERM);
+ ASSERT_ERROR_ERRNO(mkdir(p4, 0666), EPERM);
+ ASSERT_ERROR_ERRNO(openat(host_fd1, "newuser3", O_RDWR|O_CREAT|O_CLOEXEC, 0666), EPERM);
+ ASSERT_ERROR_ERRNO(openat(host_tmpfs, "newuser4", O_RDWR|O_CREAT|O_CLOEXEC, 0666), EPERM);
+
+ /* …while what was legitimately writable before stays writable */
safe_close(ASSERT_OK_ERRNO(openat(private_tmpfs, "aaa", O_RDWR|O_CREAT|O_CLOEXEC, 0666)));
- ASSERT_OK_ERRNO(mkdirat(host_tmpfs, "zzz2", 0666));
- ASSERT_OK_ERRNO(mkdirat(private_tmpfs, "aaa2", 0666));
-
- /* But this one should still fail */
- ASSERT_ERROR_ERRNO(openat(host_fd1, "bbb", O_RDWR|O_CREAT|O_CLOEXEC, 0666), EPERM);
- ASSERT_ERROR_ERRNO(mkdirat(host_fd1, "bbb2", 0666), EPERM);
-
- /* Sync again, to get more stuff allowlisted */
- ASSERT_OK_ERRNO(eventfd_write(afd, 1));
- ASSERT_OK_ERRNO(eventfd_read(bfd, &x));
-
- /* Everything should now be allowed */
- safe_close(ASSERT_OK_ERRNO(openat(host_tmpfs, "ccc", O_RDWR|O_CREAT|O_CLOEXEC, 0666)));
- safe_close(ASSERT_OK_ERRNO(openat(host_fd1, "ddd", O_RDWR|O_CREAT|O_CLOEXEC, 0666)));
- safe_close(ASSERT_OK_ERRNO(openat(private_tmpfs, "eee", O_RDWR|O_CREAT|O_CLOEXEC, 0666)));
- ASSERT_OK_ERRNO(mkdirat(host_tmpfs, "ccc2", 0666));
- safe_close(ASSERT_OK_ERRNO(openat(host_fd1, "ddd2", O_RDWR|O_CREAT|O_CLOEXEC, 0666)));
- ASSERT_OK_ERRNO(mkdirat(private_tmpfs, "eee2", 0666));
+ safe_close(ASSERT_OK_ERRNO(openat(idmapped_tmpfs, "bbb", O_RDWR|O_CREAT|O_CLOEXEC, 0666)));
_exit(EXIT_SUCCESS);
}
- uint64_t x;
- ASSERT_OK_ERRNO(eventfd_read(afd, &x));
-
- ASSERT_OK(userns_restrict_put_by_fd(
- bpf_obj,
- userns_fd,
- /* replace= */ false,
- &host_tmpfs,
- 1));
-
- ASSERT_OK_ERRNO(eventfd_write(bfd, 1));
- ASSERT_OK_ERRNO(eventfd_read(afd, &x));
-
- ASSERT_OK(userns_restrict_put_by_fd(
- bpf_obj,
- userns_fd,
- /* replace= */ false,
- &host_fd1,
- 1));
-
- ASSERT_OK_ERRNO(eventfd_write(bfd, 1));
-
ASSERT_OK(pidref_wait_for_terminate_and_check("(test)", &pidref, WAIT_LOG));
}
+TEST(userns_restrict_overlayfs) {
+ _cleanup_close_ int userns_fd = -EBADF, idmapped_upper = -EBADF, bad_userns_fd = -EBADF,
+ bad_upper = -EBADF;
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL, *th = NULL, *ti = NULL, *tb = NULL;
+ _cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
+ _cleanup_free_ char *idmap = NULL, *bad_idmap = NULL, *hlower = NULL, *hupper = NULL, *hwork = NULL,
+ *hmerged = NULL;
+ int r;
+
+ /* overlay writes land on its upper layer via vfs_create(), which the path-based hooks never see, so
+ * a transient-owned inode could persist there. The policy judges the upper at mount time. An upper
+ * is allowed when it dies with the namespace (a tmpfs the client set up) or is idmapped to map the
+ * transient range away; it is refused when it outlives the namespace and records the transient range
+ * on disk (a plain init-owned upper, or one idmapped so the transient range still reaches its disk).
+ * We also check that an overlay mounted read-only but with such an upper is refused, since it could
+ * otherwise be remounted read-write afterwards without re-entering the hook. */
+
+ ASSERT_OK(mkdtemp_malloc(NULL, &t));
+ ASSERT_OK_ERRNO(chown(t, CONTAINER_UID_MIN, CONTAINER_UID_MIN));
+
+ ASSERT_OK(asprintf(&idmap, "0 "UID_FMT" 1", CONTAINER_UID_MIN));
+ userns_fd = ASSERT_OK(userns_acquire(idmap, idmap, /* setgroups_deny= */ true));
+
+ ASSERT_OK(userns_restrict_register_by_fd(bpf_obj, userns_fd));
+
+ /* Set up the layer dirs for the refuse case on the init-owned file system backing our temporary dir,
+ * i.e. one that outlives the user namespace. This has to happen here as root: the managed client
+ * cannot create these itself, as the path hooks already block a transient-owned inode on a file
+ * system that outlives it. */
+ ASSERT_OK(mkdtemp_malloc(NULL, &th));
+ ASSERT_OK_ERRNO(chown(th, CONTAINER_UID_MIN, CONTAINER_UID_MIN));
+ ASSERT_NOT_NULL(hlower = path_join(th, "lower"));
+ ASSERT_NOT_NULL(hupper = path_join(th, "upper"));
+ ASSERT_NOT_NULL(hwork = path_join(th, "work"));
+ ASSERT_NOT_NULL(hmerged = path_join(th, "merged"));
+ ASSERT_OK_ERRNO(mkdir(hlower, 0755));
+ ASSERT_OK_ERRNO(mkdir(hupper, 0755));
+ ASSERT_OK_ERRNO(mkdir(hwork, 0755));
+ ASSERT_OK_ERRNO(mkdir(hmerged, 0755));
+ ASSERT_OK_ERRNO(chown(hlower, CONTAINER_UID_MIN, CONTAINER_UID_MIN));
+ ASSERT_OK_ERRNO(chown(hupper, CONTAINER_UID_MIN, CONTAINER_UID_MIN));
+ ASSERT_OK_ERRNO(chown(hwork, CONTAINER_UID_MIN, CONTAINER_UID_MIN));
+ ASSERT_OK_ERRNO(chown(hmerged, CONTAINER_UID_MIN, CONTAINER_UID_MIN));
+
+ /* And an idmapped upper for the third case: a tmpfs created here, i.e. in the initial user namespace
+ * so that it outlives the managed one, but idmapped through it so our transient range lands on its
+ * disk as non-transient ids. Set it up as root and attach it at a path the client can point
+ * upperdir= at. */
+ idmapped_upper = ASSERT_OK(make_idmapped_tmpfs_fsmount(userns_fd));
+ ASSERT_OK(mkdtemp_malloc(NULL, &ti));
+ ASSERT_OK_ERRNO(move_mount(idmapped_upper, "", -EBADF, ti, MOVE_MOUNT_F_EMPTY_PATH));
+
+ /* And a fourth upper, for the refuse path of the idmap check: an init-owned tmpfs idmapped so that
+ * it records our transient range on disk unchanged (identity over it), i.e. one that does NOT
+ * neutralize the range. We create and chown the layer dirs before idmapping it, since afterwards
+ * neither we (uid 0, left unmapped by the narrow idmap) nor the client (whose transient-owned
+ * writes to a persistent file system are refused) could. This overlay must be refused. */
+ ASSERT_OK(asprintf(&bad_idmap, UID_FMT" "UID_FMT" 1", CONTAINER_UID_MIN, CONTAINER_UID_MIN));
+ bad_userns_fd = ASSERT_OK(userns_acquire(bad_idmap, bad_idmap, /* setgroups_deny= */ false));
+ bad_upper = ASSERT_OK(make_tmpfs_fsmount());
+ ASSERT_OK_ERRNO(mkdirat(bad_upper, "upper", 0755));
+ ASSERT_OK_ERRNO(mkdirat(bad_upper, "work", 0755));
+ ASSERT_OK_ERRNO(fchownat(bad_upper, "upper", CONTAINER_UID_MIN, CONTAINER_UID_MIN, 0));
+ ASSERT_OK_ERRNO(fchownat(bad_upper, "work", CONTAINER_UID_MIN, CONTAINER_UID_MIN, 0));
+ ASSERT_OK_ERRNO(mount_setattr(bad_upper, "", AT_EMPTY_PATH,
+ &(struct mount_attr) {
+ .attr_set = MOUNT_ATTR_IDMAP,
+ .userns_fd = bad_userns_fd,
+ }, sizeof(struct mount_attr)));
+ ASSERT_OK(mkdtemp_malloc(NULL, &tb));
+ ASSERT_OK_ERRNO(move_mount(bad_upper, "", -EBADF, tb, MOVE_MOUNT_F_EMPTY_PATH));
+
+ r = ASSERT_OK(pidref_safe_fork("(test-ovl)", FORK_DEATHSIG_SIGKILL, &pidref));
+ if (r == 0) {
+ ASSERT_OK(namespace_enter(-EBADF, -EBADF, -EBADF, userns_fd, -EBADF));
+ ASSERT_OK_ERRNO(unshare(CLONE_NEWNS));
+ ASSERT_OK_ERRNO(mount(NULL, "/", NULL, MS_SLAVE|MS_REC, NULL));
+
+ /* Hold the overlay layers on a tmpfs of our own, so nothing touches the host. */
+ ASSERT_OK_ERRNO(mount("tmpfs", t, "tmpfs", 0, NULL));
+
+ _cleanup_free_ char *lower = NULL, *lower2 = NULL, *upper = NULL, *work = NULL,
+ *merged = NULL;
+ ASSERT_NOT_NULL(lower = path_join(t, "lower"));
+ ASSERT_NOT_NULL(lower2 = path_join(t, "lower2"));
+ ASSERT_NOT_NULL(upper = path_join(t, "upper"));
+ ASSERT_NOT_NULL(work = path_join(t, "work"));
+ ASSERT_NOT_NULL(merged = path_join(t, "merged"));
+ ASSERT_OK_ERRNO(mkdir(lower, 0755));
+ ASSERT_OK_ERRNO(mkdir(lower2, 0755));
+ ASSERT_OK_ERRNO(mkdir(upper, 0755));
+ ASSERT_OK_ERRNO(mkdir(work, 0755));
+ ASSERT_OK_ERRNO(mkdir(merged, 0755));
+
+ /* Control: a read-only overlay (no upper layer) is harmless and must be allowed. The kernel
+ * rejects a single lowerdir with no upper, so stack two lowers. If even this fails,
+ * unprivileged overlayfs is unavailable on this kernel — treat as a skip. */
+ _cleanup_free_ char *ro_opts = NULL;
+ ASSERT_NOT_NULL(ro_opts = strjoin("lowerdir=", lower, ":", lower2));
+ if (mount("overlay", merged, "overlay", MS_RDONLY, ro_opts) < 0) {
+ /* A read-only overlay has no upper and must be allowed. If our hook (rather than the
+ * kernel) refused it we would see EPERM, which is a bug, not a reason to skip. */
+ ASSERT_TRUE(errno != EPERM);
+ log_notice_errno(errno, "Unprivileged overlayfs unavailable, skipping: %m");
+ _exit(EXIT_SUCCESS);
+ }
+ ASSERT_OK_ERRNO(umount(merged));
+
+ /* The writable overlay's upper is on the tmpfs above, which dies with our user namespace, so
+ * the BPF-LSM must allow the mount. */
+ _cleanup_free_ char *rw_opts = NULL;
+ ASSERT_NOT_NULL(rw_opts = strjoin("lowerdir=", lower, ",upperdir=", upper,
+ ",workdir=", work));
+ ASSERT_OK_ERRNO(mount("overlay", merged, "overlay", 0, rw_opts));
+
+ /* A write through the overlay really lands on the upper (our tmpfs), which the path hooks
+ * never see directly. chown() over the merged mount reaches path_chown, which sees the
+ * overlay superblock (ours, so it dies with us) and allows it, even to a transient gid. */
+ _cleanup_free_ char *f = NULL, *uf = NULL;
+ ASSERT_NOT_NULL(f = path_join(merged, "f"));
+ ASSERT_NOT_NULL(uf = path_join(upper, "f"));
+ safe_close(ASSERT_OK_ERRNO(open(f, O_RDWR|O_CREAT|O_CLOEXEC, 0666)));
+ ASSERT_OK_ERRNO(access(uf, F_OK));
+ ASSERT_OK_ERRNO(fchownat(AT_FDCWD, f, UID_INVALID, 0, 0));
+
+ ASSERT_OK_ERRNO(umount(merged));
+
+ /* The same writable overlay, but with its upper on the init-owned host file system that
+ * outlives our user namespace: the policy must refuse it. Overlayfs sets the upper up via
+ * vfs_mkdir()/vfs_create(), reaching only the inode hooks, so nothing blocks it before
+ * sb_kern_mount runs, and the kernel itself permits the mount (verified without the BPF-LSM
+ * loaded) — hence the EPERM here is sb_kern_mount refusing it. */
+ _cleanup_free_ char *bad_opts = NULL;
+ ASSERT_NOT_NULL(bad_opts = strjoin("lowerdir=", hlower, ",upperdir=", hupper,
+ ",workdir=", hwork));
+ ASSERT_ERROR_ERRNO(mount("overlay", hmerged, "overlay", 0, bad_opts), EPERM);
+
+ /* The same persistent upper, but mounted read-only, must be refused too: it still has an
+ * upper, so allowing it on the strength of the (mutable) read-only flag would let the client
+ * remount it read-write afterwards, which does not re-enter this hook. */
+ ASSERT_ERROR_ERRNO(mount("overlay", hmerged, "overlay", MS_RDONLY, bad_opts), EPERM);
+
+ /* Then the idmapped upper: its superblock outlives us, but its idmapping maps our
+ * transient range away, so nothing recyclable reaches its disk and the mount is allowed
+ * again. The layer dirs are created by us, the transient client, which the idmapping permits
+ * the same way it permits writes through the mount. */
+ _cleanup_free_ char *iupper = NULL, *iwork = NULL, *idmapped_opts = NULL;
+ ASSERT_NOT_NULL(iupper = path_join(ti, "upper"));
+ ASSERT_NOT_NULL(iwork = path_join(ti, "work"));
+ ASSERT_OK_ERRNO(mkdir(iupper, 0755));
+ ASSERT_OK_ERRNO(mkdir(iwork, 0755));
+ ASSERT_NOT_NULL(idmapped_opts = strjoin("lowerdir=", lower, ",upperdir=", iupper,
+ ",workdir=", iwork));
+ ASSERT_OK_ERRNO(mount("overlay", merged, "overlay", 0, idmapped_opts));
+ ASSERT_OK_ERRNO(umount(merged));
+
+ /* Finally the idmapped upper whose idmapping does NOT map our transient range away (it
+ * records it on disk unchanged): the mount reaches sb_kern_mount, whose idmap check must
+ * refuse it. */
+ _cleanup_free_ char *bupper = NULL, *bwork = NULL, *bad_idmapped_opts = NULL;
+ ASSERT_NOT_NULL(bupper = path_join(tb, "upper"));
+ ASSERT_NOT_NULL(bwork = path_join(tb, "work"));
+ ASSERT_NOT_NULL(bad_idmapped_opts = strjoin("lowerdir=", lower, ",upperdir=", bupper,
+ ",workdir=", bwork));
+ ASSERT_ERROR_ERRNO(mount("overlay", merged, "overlay", 0, bad_idmapped_opts), EPERM);
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ ASSERT_OK(pidref_wait_for_terminate_and_check("(test-ovl)", &pidref, WAIT_LOG));
+
+ /* Detach the extra uppers we attached in our (initial) mount namespace before the temp dirs are
+ * removed. */
+ (void) umount(ti);
+ (void) umount(tb);
+}
+
static void write_child_mappings(PidRef *child, int parent_userns_fd) {
/* The kernel requires uid_map/gid_map to be written from the parent user namespace of the
* target namespace. Fork a helper that joins the parent userns and writes the mappings from
@@ -184,23 +381,13 @@ TEST(setgroups_deny) {
* denial specifically. */
deny_userns_fd = ASSERT_OK(userns_acquire(idmap, idmap, /* setgroups_deny= */ false));
- ASSERT_OK(userns_restrict_put_by_fd(
- bpf_obj,
- deny_userns_fd,
- /* replace= */ true,
- /* mount_fds= */ NULL,
- /* n_mount_fds= */ 0));
+ ASSERT_OK(userns_restrict_register_by_fd(bpf_obj, deny_userns_fd));
ASSERT_OK(userns_restrict_setgroups_deny_by_fd(bpf_obj, deny_userns_fd));
- /* Create a userns that is managed (in mount ID hash) but does NOT have setgroups() denied */
+ /* Create a userns that is managed but does NOT have setgroups() denied */
allow_userns_fd = ASSERT_OK(userns_acquire(idmap, idmap, /* setgroups_deny= */ false));
- ASSERT_OK(userns_restrict_put_by_fd(
- bpf_obj,
- allow_userns_fd,
- /* replace= */ true,
- /* mount_fds= */ NULL,
- /* n_mount_fds= */ 0));
+ ASSERT_OK(userns_restrict_register_by_fd(bpf_obj, allow_userns_fd));
afd = ASSERT_OK_ERRNO(eventfd(0, EFD_CLOEXEC));
bfd = ASSERT_OK_ERRNO(eventfd(0, EFD_CLOEXEC));
@@ -247,7 +434,7 @@ TEST(setgroups_deny) {
ASSERT_OK(pidref_wait_for_terminate_and_check("(test-deny)", &pidref, WAIT_LOG));
}
- /* Test 2: setgroups() should be allowed in the managed-only userns (mount ID hash but no setgroups
+ /* Test 2: setgroups() should be allowed in the managed-only userns (managed map but no setgroups
* deny entry), including in a child user namespace. */
{
_cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
@@ -258,11 +445,11 @@ TEST(setgroups_deny) {
ASSERT_OK_ERRNO(setresgid(0, 0, 0));
ASSERT_OK_ERRNO(setresuid(0, 0, 0));
- /* setgroups() should succeed since this userns is only in the mount ID hash */
+ /* setgroups() should succeed since this userns is only in the managed map */
ASSERT_OK_ERRNO(setgroups(0, NULL));
/* Also should work in a child userns since the ancestor walk finds the
- * mount ID hash entry (not the setgroups deny entry) */
+ * managed map entry (not the setgroups deny entry) */
ASSERT_OK_ERRNO(unshare(CLONE_NEWUSER));
ASSERT_OK_ERRNO(eventfd_write(afd, 1));
uint64_t x;
diff --git a/src/nsresourced/userns-registry.c b/src/nsresourced/userns-registry.c
index 0da6a6a7c6..26d8c28e57 100644
--- a/src/nsresourced/userns-registry.c
+++ b/src/nsresourced/userns-registry.c
@@ -83,6 +83,7 @@ UserNamespaceInfo* userns_info_new(void) {
.target_uid = UID_INVALID,
.start_gid = GID_INVALID,
.target_gid = GID_INVALID,
+ .setgroups_deny = -1,
};
return info;
@@ -241,18 +242,19 @@ static int dispatch_ancestor_userns_array(const char *name, sd_json_variant *var
static int userns_registry_load(int dir_fd, const char *fn, UserNamespaceInfo **ret) {
static const sd_json_dispatch_field dispatch_table[] = {
- { "owner", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, owner), SD_JSON_MANDATORY },
- { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(UserNamespaceInfo, name), SD_JSON_MANDATORY },
- { "userns", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint64, offsetof(UserNamespaceInfo, userns_inode), SD_JSON_MANDATORY },
- { "usernsId", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint64, offsetof(UserNamespaceInfo, userns_id), 0 },
- { "size", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint32, offsetof(UserNamespaceInfo, size), 0 },
- { "start", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, start_uid), 0 },
- { "target", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, target_uid), 0 },
- { "startGid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, start_gid), 0 },
- { "targetGid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, target_gid), 0 },
- { "cgroups", SD_JSON_VARIANT_ARRAY, dispatch_cgroups_array, 0, 0 },
- { "netifs", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(UserNamespaceInfo, netifs), 0 },
- { "delegates", SD_JSON_VARIANT_ARRAY, dispatch_delegates_array, 0, 0 },
+ { "owner", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, owner), SD_JSON_MANDATORY },
+ { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(UserNamespaceInfo, name), SD_JSON_MANDATORY },
+ { "userns", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint64, offsetof(UserNamespaceInfo, userns_inode), SD_JSON_MANDATORY },
+ { "usernsId", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint64, offsetof(UserNamespaceInfo, userns_id), 0 },
+ { "size", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint32, offsetof(UserNamespaceInfo, size), 0 },
+ { "start", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, start_uid), 0 },
+ { "target", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, target_uid), 0 },
+ { "startGid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, start_gid), 0 },
+ { "targetGid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, target_gid), 0 },
+ { "setgroupsDeny", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(UserNamespaceInfo, setgroups_deny), 0 },
+ { "cgroups", SD_JSON_VARIANT_ARRAY, dispatch_cgroups_array, 0, 0 },
+ { "netifs", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(UserNamespaceInfo, netifs), 0 },
+ { "delegates", SD_JSON_VARIANT_ARRAY, dispatch_delegates_array, 0, 0 },
{}
};
@@ -539,13 +541,16 @@ void userns_registry_release_by_userns_inode(struct userns_restrict_bpf *bpf, in
release_userns_inode_resources(bpf, inode);
}
-int userns_registry_reap_if_dead(struct userns_restrict_bpf *bpf, int dir_fd, uint64_t inode) {
+int userns_registry_reap_if_dead(struct userns_restrict_bpf *bpf, int dir_fd, uint64_t inode, UserNamespaceInfo **ret_info) {
_cleanup_(userns_info_freep) UserNamespaceInfo *info = NULL;
int r;
assert(dir_fd >= 0);
assert(inode != 0);
+ if (ret_info)
+ *ret_info = NULL;
+
/* This is the shared engine behind both the runtime allocation hot path and the startup
* registry sweep: it reclaims ranges blocked by a dead namespace that no BPF death event
* cleaned up, without waiting for the manager to restart. */
@@ -558,19 +563,30 @@ int userns_registry_reap_if_dead(struct userns_restrict_bpf *bpf, int dir_fd, ui
if (r < 0)
return r;
- if (info->userns_id == 0)
- return USERNS_REAP_INDETERMINATE; /* Entry predates ns id tracking, can't probe authoritatively. */
+ if (info->userns_id == 0) {
+ /* Entry predates ns id tracking, can't probe authoritatively. */
+ if (ret_info)
+ *ret_info = TAKE_PTR(info);
+ return USERNS_REAP_INDETERMINATE;
+ }
_cleanup_close_ int probe_fd = namespace_open_by_id(info->userns_id);
- if (probe_fd >= 0)
- return USERNS_REAP_ALIVE; /* Still alive (or the inode was recycled for a live namespace). */
- if (ERRNO_IS_NEG_PRIVILEGE(probe_fd) || ERRNO_IS_NEG_NOT_SUPPORTED(probe_fd) || probe_fd == -EINVAL)
+ if (probe_fd >= 0) {
+ /* Still alive (or the inode was recycled for a live namespace). */
+ if (ret_info)
+ *ret_info = TAKE_PTR(info);
+ return USERNS_REAP_ALIVE;
+ }
+ if (ERRNO_IS_NEG_PRIVILEGE(probe_fd) || ERRNO_IS_NEG_NOT_SUPPORTED(probe_fd) || probe_fd == -EINVAL) {
/* EPERM/EACCES (not in the initial user namespace, missing CAP_SYS_ADMIN), or
* ENOTSUP/ENOSYS/EINVAL (kernel new enough for NS_GET_ID but too old for
* open_by_handle_at() lookup by id on nsfs) — we can't confirm death for this or any
* other entry. The userns_id == 0 case is already handled above, so -EINVAL here means
* the lookup is unsupported rather than a bad id. */
+ if (ret_info)
+ *ret_info = TAKE_PTR(info);
return USERNS_REAP_UNSUPPORTED;
+ }
if (probe_fd != -ESTALE)
return probe_fd; /* Unexpected probe error. */
@@ -693,6 +709,7 @@ int userns_registry_store(int dir_fd, UserNamespaceInfo *info) {
SD_JSON_BUILD_PAIR_CONDITION(uid_is_valid(info->target_uid), "target", SD_JSON_BUILD_UNSIGNED(info->target_uid)),
SD_JSON_BUILD_PAIR_CONDITION(gid_is_valid(info->start_gid), "startGid", SD_JSON_BUILD_UNSIGNED(info->start_gid)),
SD_JSON_BUILD_PAIR_CONDITION(gid_is_valid(info->target_gid), "targetGid", SD_JSON_BUILD_UNSIGNED(info->target_gid)),
+ SD_JSON_BUILD_PAIR_CONDITION(info->setgroups_deny >= 0, "setgroupsDeny", SD_JSON_BUILD_BOOLEAN(info->setgroups_deny > 0)),
SD_JSON_BUILD_PAIR_CONDITION(!!cgroup_array, "cgroups", SD_JSON_BUILD_VARIANT(cgroup_array)),
JSON_BUILD_PAIR_STRV_NON_EMPTY("netifs", info->netifs),
SD_JSON_BUILD_PAIR_CONDITION(!!delegates_array, "delegates", SD_JSON_BUILD_VARIANT(delegates_array)));
diff --git a/src/nsresourced/userns-registry.h b/src/nsresourced/userns-registry.h
index 4c88b45cfd..802307d7eb 100644
--- a/src/nsresourced/userns-registry.h
+++ b/src/nsresourced/userns-registry.h
@@ -37,6 +37,10 @@ typedef struct UserNamespaceInfo {
uid_t target_uid;
gid_t start_gid;
gid_t target_gid;
+ int setgroups_deny; /* Whether setgroups() is denied for this namespace via the BPF-LSM, so the
+ * denial can be re-established after a restart that reset the BPF maps.
+ * Negative in entries written before we recorded this, and stays that way
+ * when such an entry is updated, so the answer is never faked. */
uint64_t *cgroups;
size_t n_cgroups;
char **netifs;
@@ -93,8 +97,10 @@ typedef enum UserNamespaceReapStatus {
* namespace id and, if it is authoritatively dead, releases its registry entry (restoring any ranges
* it received via delegation to their ancestors). Returns a non-negative UserNamespaceReapStatus
* describing what happened, or a negative errno on genuine failure. The caller must hold the registry
- * lock (or otherwise be free of concurrent writers). */
-int userns_registry_reap_if_dead(struct userns_restrict_bpf *bpf, int dir_fd, uint64_t inode);
+ * lock (or otherwise be free of concurrent writers). If ret_info is passed it receives the entry this
+ * had to load anyway, so that callers that go on to work on the entry need not parse it a second time.
+ * It is set to NULL whenever no entry survives the call. */
+int userns_registry_reap_if_dead(struct userns_restrict_bpf *bpf, int dir_fd, uint64_t inode, UserNamespaceInfo **ret_info);
int userns_registry_store(int dir_fd, UserNamespaceInfo *info);
int userns_registry_remove(int dir_fd, UserNamespaceInfo *info);
diff --git a/src/nsresourced/userns-restrict.c b/src/nsresourced/userns-restrict.c
index 99f5223671..0f6f021e1d 100644
--- a/src/nsresourced/userns-restrict.c
+++ b/src/nsresourced/userns-restrict.c
@@ -8,21 +8,27 @@
#include "bpf-util.h"
#include "bpf-link.h"
-#include "fd-util.h"
#include "log.h"
#include "lsm-util.h"
#include "mkdir.h"
-#include "mountpoint-util.h"
#include "namespace-util.h"
#include "path-util.h"
+#include "rm-rf.h"
#include "string-util.h"
+#include "strv.h"
#include "userns-restrict.h"
#define USERNS_MAX (16U*1024U)
-#define MOUNTS_MAX 4096U
-#define PROGRAM_LINK_PREFIX "/sys/fs/bpf/systemd/userns-restrict/programs"
-#define MAP_LINK_PREFIX "/sys/fs/bpf/systemd/userns-restrict/maps"
+/* No dots in any of the path components: bpffs reserves those for its own use and fails every lookup that
+ * contains one with EPERM, see bpf_lookup() in the kernel sources. */
+#define LINK_PREFIX "/sys/fs/bpf/systemd/userns-restrict-v2"
+#define PROGRAM_LINK_PREFIX LINK_PREFIX "/programs"
+#define MAP_LINK_PREFIX LINK_PREFIX "/maps"
+
+/* Programs pinned here enforce the mount allowlist policy we used until v261, which is bypassable with a
+ * single unshare(). They'd stay attached next to ours, hence get rid of them. */
+#define OBSOLETE_LINK_PREFIX "/sys/fs/bpf/systemd/userns-restrict"
struct userns_restrict_bpf *userns_restrict_bpf_free(struct userns_restrict_bpf *obj) {
#if HAVE_VMLINUX_H
@@ -32,31 +38,12 @@ struct userns_restrict_bpf *userns_restrict_bpf_free(struct userns_restrict_bpf
}
-#if HAVE_VMLINUX_H
-static int make_inner_hash_map(void) {
- int fd;
-
- fd = compat_bpf_map_create(
- BPF_MAP_TYPE_HASH,
- NULL,
- sizeof(int),
- sizeof(uint32_t),
- MOUNTS_MAX,
- NULL);
- if (fd < 0)
- return log_debug_errno(errno, "Failed to allocate inner BPF map: %m");
-
- return fd;
-}
-#endif
-
int userns_restrict_install(
bool pin,
struct userns_restrict_bpf **ret) {
#if HAVE_VMLINUX_H
_cleanup_(userns_restrict_bpf_freep) struct userns_restrict_bpf *obj = NULL;
- _cleanup_close_ int dummy_mnt_id_hash_fd = -EBADF;
int r;
r = lsm_supported("bpf");
@@ -84,9 +71,13 @@ int userns_restrict_install(
if (pin) {
struct bpf_map *map;
- /* libbpf will only create one level of dirs. Let's create the rest */
- (void) mkdir_p(MAP_LINK_PREFIX, 0755);
- (void) mkdir_p(PROGRAM_LINK_PREFIX, 0755);
+ /* libbpf will only create one level of dirs. Let's create the rest. Failing here means the
+ * pinning below fails too, so complain rather than run into that without a clue. */
+ FOREACH_STRING(d, MAP_LINK_PREFIX, PROGRAM_LINK_PREFIX) {
+ r = mkdir_p(d, 0755);
+ if (r < 0)
+ log_warning_errno(r, "Failed to create '%s', ignoring: %m", d);
+ }
map = sym_bpf_object__next_map(obj->obj, NULL);
while (map) {
@@ -104,9 +95,9 @@ int userns_restrict_install(
}
}
- r = sym_bpf_map__set_max_entries(obj->maps.userns_mnt_id_hash, USERNS_MAX);
+ r = sym_bpf_map__set_max_entries(obj->maps.userns_managed, USERNS_MAX);
if (r < 0)
- return log_error_errno(r, "Failed to size userns/mnt_id hash table: %m");
+ return log_error_errno(r, "Failed to size managed userns hash table: %m");
r = sym_bpf_map__set_max_entries(obj->maps.userns_ringbuf, USERNS_MAX * sizeof(unsigned));
if (r < 0)
@@ -116,19 +107,34 @@ int userns_restrict_install(
if (r < 0)
return log_error_errno(r, "Failed to size userns setgroups deny hash table: %m");
- /* Dummy map to satisfy the verifier */
- dummy_mnt_id_hash_fd = make_inner_hash_map();
- if (dummy_mnt_id_hash_fd < 0)
- return dummy_mnt_id_hash_fd;
-
- r = sym_bpf_map__set_inner_map_fd(obj->maps.userns_mnt_id_hash, dummy_mnt_id_hash_fd);
- if (r < 0)
- return log_error_errno(r, "Failed to set inner BPF map: %m");
-
r = userns_restrict_bpf__load(obj);
if (r < 0)
return log_error_errno(r, "Failed to load BPF object: %m");
+ /* Pin the maps already here: the programs are not attached yet, so nothing enforces anything, but
+ * the caller may now seed the maps before userns_restrict_attach() puts the policy in effect. */
+ if (pin) {
+ r = sym_bpf_object__pin_maps(obj->obj, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to pin BPF maps: %m");
+ }
+
+ if (ret)
+ *ret = TAKE_PTR(obj);
+
+ return 0;
+#else
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "User Namespace Restriction BPF support disabled.");
+#endif
+}
+
+int userns_restrict_attach(struct userns_restrict_bpf *obj, bool pin) {
+
+#if HAVE_VMLINUX_H
+ int r;
+
+ assert(obj);
+
for (int i = 0; i < obj->skeleton->prog_cnt; i++) {
_cleanup_(bpf_link_freep) struct bpf_link *link = NULL;
struct bpf_prog_skeleton *ps = obj->skeleton->progs + i;
@@ -176,14 +182,11 @@ int userns_restrict_install(
*ps->link = TAKE_PTR(link);
}
- if (pin) {
- r = sym_bpf_object__pin_maps(obj->obj, NULL);
- if (r < 0)
- return log_error_errno(r, "Failed to pin BPF maps: %m");
- }
-
- if (ret)
- *ret = TAKE_PTR(obj);
+ /* Only now that our own programs enforce the policy, drop the ones the previous version left
+ * pinned. Removing their links detaches them, so doing this any earlier would hand the namespaces
+ * that predate an upgrade a stretch with no policy attached at all. */
+ if (pin)
+ (void) rm_rf(OBSOLETE_LINK_PREFIX, REMOVE_PHYSICAL);
return 0;
#else
@@ -191,95 +194,35 @@ int userns_restrict_install(
#endif
}
-int userns_restrict_put_by_inode(
+int userns_restrict_register_by_inode(
struct userns_restrict_bpf *obj,
- uint64_t userns_inode,
- bool replace,
- const int mount_fds[],
- size_t n_mount_fds) {
+ uint64_t userns_inode) {
#if HAVE_VMLINUX_H
- _cleanup_close_ int inner_map_fd = -EBADF;
- _cleanup_free_ int *mnt_ids = NULL;
- uint64_t ino = userns_inode;
- int r, outer_map_fd;
+ uint32_t dummy_value = 1;
+ int map_fd, r;
+ unsigned ino;
assert(obj);
assert(userns_inode != 0);
- assert(n_mount_fds == 0 || mount_fds);
- /* The BPF map type BPF_MAP_TYPE_HASH_OF_MAPS only supports 32bit keys, and user namespace inode
- * numbers are 32bit too, even though ino_t is 64bit these days. Should we ever run into a 64bit
- * inode let's refuse early, we can't support this with the current BPF code for now. */
+ /* The BPF map only supports 32bit keys, and user namespace inode numbers are 32bit too, even though
+ * ino_t is 64bit these days. Should we ever run into a 64bit inode let's refuse early, we can't
+ * support this with the current BPF code for now. */
if (userns_inode > UINT32_MAX)
return -EINVAL;
- mnt_ids = new(int, n_mount_fds);
- if (!mnt_ids)
- return -ENOMEM;
+ ino = (unsigned) userns_inode;
- for (size_t i = 0; i < n_mount_fds; i++) {
- r = path_get_mnt_id_at(mount_fds[i], "", mnt_ids + i);
- if (r < 0)
- return log_debug_errno(r, "Failed to get mount ID: %m");
- }
+ map_fd = sym_bpf_map__fd(obj->maps.userns_managed);
+ if (map_fd < 0)
+ return log_debug_errno(map_fd, "Failed to get managed userns BPF map fd: %m");
- outer_map_fd = sym_bpf_map__fd(obj->maps.userns_mnt_id_hash);
- if (outer_map_fd < 0)
- return log_debug_errno(outer_map_fd, "Failed to get outer BPF map fd: %m");
+ r = sym_bpf_map_update_elem(map_fd, &ino, &dummy_value, BPF_ANY);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to add userns inode to managed map: %m");
- if (replace) {
- /* Add if missing, replace if already exists */
- inner_map_fd = make_inner_hash_map();
- if (inner_map_fd < 0)
- return inner_map_fd;
-
- r = sym_bpf_map_update_elem(outer_map_fd, &ino, &inner_map_fd, BPF_ANY);
- if (r < 0)
- return log_debug_errno(r, "Failed to replace map in inode hash: %m");
- } else {
- /* Let's add an entry for this userns inode if missing. If it exists just extend the existing map. We
- * might race against each other, hence we try a couple of times */
- for (size_t n_try = 10;; n_try--) {
- uint32_t innermap_id;
-
- if (n_try == 0)
- return log_debug_errno(SYNTHETIC_ERRNO(EEXIST),
- "Still cannot create inode entry in BPF map after 10 tries.");
-
- r = sym_bpf_map_lookup_elem(outer_map_fd, &ino, &innermap_id);
- if (r >= 0) {
- inner_map_fd = sym_bpf_map_get_fd_by_id(innermap_id);
- if (inner_map_fd < 0)
- return log_debug_errno(inner_map_fd, "Failed to get file descriptor for inner map: %m");
-
- break;
- }
- if (errno != ENOENT)
- return log_debug_errno(errno, "Failed to look up inode hash entry: %m");
-
- /* No entry for this user namespace yet. Let's create one */
- inner_map_fd = make_inner_hash_map();
- if (inner_map_fd < 0)
- return inner_map_fd;
-
- r = sym_bpf_map_update_elem(outer_map_fd, &ino, &inner_map_fd, BPF_NOEXIST);
- if (r >= 0)
- break;
- if (errno != EEXIST)
- return log_debug_errno(errno, "Failed to add mount ID list to inode hash: %m");
- }
- }
-
- FOREACH_ARRAY(mntid, mnt_ids, n_mount_fds) {
- uint32_t dummy_value = 1;
-
- r = sym_bpf_map_update_elem(inner_map_fd, mntid, &dummy_value, BPF_ANY);
- if (r < 0)
- return log_debug_errno(r, "Failed to add mount ID to map: %m");
-
- log_debug("Allowing mount %i on userns inode %" PRIu64, *mntid, ino);
- }
+ log_debug("Restricting persistent writes for userns inode %" PRIu64, userns_inode);
return 0;
#else
@@ -287,12 +230,9 @@ int userns_restrict_put_by_inode(
#endif
}
-int userns_restrict_put_by_fd(
+int userns_restrict_register_by_fd(
struct userns_restrict_bpf *obj,
- int userns_fd,
- bool replace,
- const int mount_fds[],
- size_t n_mount_fds) {
+ int userns_fd) {
#if HAVE_VMLINUX_H
struct stat st;
@@ -300,7 +240,6 @@ int userns_restrict_put_by_fd(
assert(obj);
assert(userns_fd >= 0);
- assert(n_mount_fds == 0 || mount_fds);
r = fd_is_namespace(userns_fd, NAMESPACE_USER);
if (r < 0)
@@ -311,12 +250,7 @@ int userns_restrict_put_by_fd(
if (fstat(userns_fd, &st) < 0)
return log_debug_errno(errno, "Failed to fstat() user namespace: %m");
- return userns_restrict_put_by_inode(
- obj,
- st.st_ino,
- replace,
- mount_fds,
- n_mount_fds);
+ return userns_restrict_register_by_inode(obj, st.st_ino);
#else
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "User Namespace Restriction BPF support disabled.");
#endif
@@ -325,7 +259,7 @@ int userns_restrict_put_by_fd(
int userns_restrict_reset_by_inode(struct userns_restrict_bpf *obj, uint64_t userns_inode) {
#if HAVE_VMLINUX_H
- int r, outer_map_fd, setgroups_deny_fd;
+ int r, managed_fd, setgroups_deny_fd;
unsigned u;
assert(obj);
@@ -334,15 +268,17 @@ int userns_restrict_reset_by_inode(struct userns_restrict_bpf *obj, uint64_t use
if (userns_inode > UINT32_MAX) /* inodes larger than 32bit are definitely not included in our map, exit early */
return 0;
- outer_map_fd = sym_bpf_map__fd(obj->maps.userns_mnt_id_hash);
- if (outer_map_fd < 0)
- return log_debug_errno(outer_map_fd, "Failed to get outer BPF map fd: %m");
+ managed_fd = sym_bpf_map__fd(obj->maps.userns_managed);
+ if (managed_fd < 0)
+ return log_debug_errno(managed_fd, "Failed to get managed userns BPF map fd: %m");
u = (uint32_t) userns_inode;
- r = sym_bpf_map_delete_elem(outer_map_fd, &u);
- if (r < 0)
- return log_debug_errno(r, "Failed to remove entry for inode %" PRIu64 " from outer map: %m", userns_inode);
+ /* A missing entry is an ordinary state here (the maps come up empty after a schema change, and
+ * registration is idempotent), so don't let it cut the setgroups deny cleanup below short. */
+ r = sym_bpf_map_delete_elem(managed_fd, &u);
+ if (r < 0 && r != -ENOENT)
+ return log_debug_errno(r, "Failed to remove entry for inode %" PRIu64 " from managed map: %m", userns_inode);
setgroups_deny_fd = sym_bpf_map__fd(obj->maps.userns_setgroups_deny);
if (setgroups_deny_fd < 0)
diff --git a/src/nsresourced/userns-restrict.h b/src/nsresourced/userns-restrict.h
index 87457bf368..a229bd64bf 100644
--- a/src/nsresourced/userns-restrict.h
+++ b/src/nsresourced/userns-restrict.h
@@ -5,11 +5,18 @@
struct userns_restrict_bpf;
+/* Bringing the policy up is split in two so that callers can seed the maps in between: the programs only
+ * start enforcing once userns_restrict_attach() ran, and whatever they find in the maps at that point is
+ * what they enforce from their very first invocation. Attaching first and filling the maps afterwards would
+ * leave a window in which every namespace looks unmanaged and the policy fails open. Callers that only
+ * manipulate the maps of an already running policy (i.e. the workers) stop after the install step. */
int userns_restrict_install(bool pin, struct userns_restrict_bpf **ret);
+int userns_restrict_attach(struct userns_restrict_bpf *obj, bool pin);
+
struct userns_restrict_bpf *userns_restrict_bpf_free(struct userns_restrict_bpf *obj);
-int userns_restrict_put_by_fd(struct userns_restrict_bpf *obj, int userns_fd, bool replace, const int mount_fds[], size_t n_mount_fds);
-int userns_restrict_put_by_inode(struct userns_restrict_bpf *obj, uint64_t userns_inode, bool replace, const int mount_fds[], size_t n_mount_fds);
+int userns_restrict_register_by_fd(struct userns_restrict_bpf *obj, int userns_fd);
+int userns_restrict_register_by_inode(struct userns_restrict_bpf *obj, uint64_t userns_inode);
int userns_restrict_reset_by_inode(struct userns_restrict_bpf *obj, uint64_t userns_inode);
diff --git a/src/shared/tar-util.c b/src/shared/tar-util.c
index e2603680bd..c60054b2d1 100644
--- a/src/shared/tar-util.c
+++ b/src/shared/tar-util.c
@@ -1083,7 +1083,7 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) {
return 0;
}
-static int make_tmpfs(void) {
+int tar_hardlink_db_new(void) {
/* Creates a tmpfs superblock to store our hardlink db in. We can do this if we run in our own
* userns, or if we are privileged. This is preferable, since it means the db is cleaned up
* automatically once we are done. Moreover, since this is a new superblock owned by us, we do not
@@ -1111,6 +1111,7 @@ struct make_archive_data {
TarFlags flags;
int hardlink_db_fd;
+ bool hardlink_db_fd_owned; /* Whether we allocated the db ourselves, i.e. whether it is ours to close */
char *hardlink_db_path;
int have_unique_mount_id;
};
@@ -1199,7 +1200,7 @@ static int hardlink_lookup(
/* We first try to create our own superblock, which works if we are in a userns, and which
* doesn't require explicit clean-up */
- d->hardlink_db_fd = make_tmpfs();
+ d->hardlink_db_fd = tar_hardlink_db_new();
if (d->hardlink_db_fd < 0) {
log_debug_errno(d->hardlink_db_fd, "Failed to allocate tmpfs superblock for hardlink db, falling back to temporary directory: %m");
@@ -1216,6 +1217,8 @@ static int hardlink_lookup(
if (d->hardlink_db_fd < 0)
return log_error_errno(d->hardlink_db_fd, "Failed to make hardlink database directory: %m");
}
+
+ d->hardlink_db_fd_owned = true;
} else {
_cleanup_free_ char *p = NULL;
r = readlinkat_malloc(d->hardlink_db_fd, n, &p);
@@ -1585,13 +1588,17 @@ static int archive_item(
static void make_archive_data_done(struct make_archive_data *d) {
assert(d);
- if (d->hardlink_db_fd >= 0)
- (void) rm_rf_children(d->hardlink_db_fd, REMOVE_PHYSICAL, /* root_dev= */ NULL);
+ /* Only tear down a db we allocated ourselves: rm_rf_children() consumes the fd it is given, and one
+ * handed to us by the caller is theirs to close. */
+ if (d->hardlink_db_fd_owned && d->hardlink_db_fd >= 0)
+ (void) rm_rf_children(TAKE_FD(d->hardlink_db_fd), REMOVE_PHYSICAL, /* root_dev= */ NULL);
unlink_and_free(d->hardlink_db_path);
}
-int tar_c(int tree_fd, int output_fd, const char *filename, TarFlags flags) {
+/* The hardlink db fd is borrowed, not consumed: tar_c() neither closes it nor empties it again, so whoever
+ * passed it in stays responsible for both. */
+int tar_c(int tree_fd, int output_fd, const char *filename, int hardlink_db_fd, TarFlags flags) {
int r;
assert(tree_fd >= 0);
@@ -1617,7 +1624,7 @@ int tar_c(int tree_fd, int output_fd, const char *filename, TarFlags flags) {
_cleanup_(make_archive_data_done) struct make_archive_data data = {
.archive = a,
.flags = flags,
- .hardlink_db_fd = -EBADF,
+ .hardlink_db_fd = hardlink_db_fd,
.have_unique_mount_id = -1,
};
@@ -1648,11 +1655,15 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) {
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "libarchive support not available.");
}
-int tar_c(int tree_fd, int output_fd, const char *filename, TarFlags flags) {
+int tar_c(int tree_fd, int output_fd, const char *filename, int hardlink_db_fd, TarFlags flags) {
assert(tree_fd >= 0);
assert(output_fd >= 0);
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "libarchive support not available.");
}
+int tar_hardlink_db_new(void) {
+ return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "libarchive support not available.");
+}
+
#endif
diff --git a/src/shared/tar-util.h b/src/shared/tar-util.h
index 4ce8c57377..3e41ef1305 100644
--- a/src/shared/tar-util.h
+++ b/src/shared/tar-util.h
@@ -7,5 +7,9 @@ typedef enum TarFlags {
TAR_OCI_WHITEOUTS = 1 << 2, /* Turn OCI/aufs whiteout inodes into overlayfs whiteouts */
} TarFlags;
+/* Allocates a tmpfs superblock for tar_c() to keep its hardlink database in. Requires CAP_SYS_ADMIN in the
+ * caller's user namespace, hence callers that drop privileges must do this first and pass the fd along. */
+int tar_hardlink_db_new(void);
+
int tar_x(int input_fd, int tree_fd, TarFlags flags);
-int tar_c(int tree_fd, int output_fd, const char *filename, TarFlags flags);
+int tar_c(int tree_fd, int output_fd, const char *filename, int hardlink_db_fd, TarFlags flags);
diff --git a/units/systemd-nsresourced.service.in b/units/systemd-nsresourced.service.in
index 143f9a9a3d..771fdec531 100644
--- a/units/systemd-nsresourced.service.in
+++ b/units/systemd-nsresourced.service.in
@@ -13,8 +13,8 @@ Documentation=man:systemd-nsresourced.service(8)
Requires=systemd-nsresourced.socket
Conflicts=shutdown.target
Before=sysinit.target shutdown.target
-Wants=modprobe@tun.service
-After=modprobe@tun.service
+Wants=modprobe@tun.service modprobe@overlay.service
+After=modprobe@tun.service modprobe@overlay.service
DefaultDependencies=no
[Service]