]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
mountfsd: Relax user namespace checks
authorDaan De Meyer <daan@amutable.com>
Fri, 20 Feb 2026 18:14:55 +0000 (19:14 +0100)
committerDaan De Meyer <daan@amutable.com>
Tue, 24 Feb 2026 17:29:37 +0000 (18:29 +0100)
- Allow foreign UID range
- Allow delegated UID ranges

Both of these can now be mapped by nsresourced into user namespaces
and hence should be accepted by mountfsd.

src/mountfsd/mountwork.c

index bd2ffbf8b79ef25fb80263170225d2dfac52fb0b..905324ffdda1469ce0e2a384459f0777859b6de3 100644 (file)
@@ -1208,30 +1208,60 @@ static int vl_method_mount_directory(
                 uid_t start;
 
                 if (userns_fd >= 0) {
+                        /* Load ranges without coalescing to preserve the 1:1 correspondence
+                         * between inside and outside entries */
                         _cleanup_(uid_range_freep) UIDRange *uid_range_outside = NULL, *uid_range_inside = NULL, *gid_range_outside = NULL, *gid_range_inside = NULL;
-                        r = uid_range_load_userns_by_fd(userns_fd, UID_RANGE_USERNS_OUTSIDE, &uid_range_outside);
+                        r = uid_range_load_userns_by_fd_full(userns_fd, UID_RANGE_USERNS_OUTSIDE, /* coalesce= */ false, &uid_range_outside);
                         if (r < 0)
                                 return log_debug_errno(r, "Failed to load outside UID range of provided userns: %m");
-                        r = uid_range_load_userns_by_fd(userns_fd, UID_RANGE_USERNS_INSIDE, &uid_range_inside);
+
+                        r = uid_range_load_userns_by_fd_full(userns_fd, UID_RANGE_USERNS_INSIDE, /* coalesce= */ false, &uid_range_inside);
                         if (r < 0)
                                 return log_debug_errno(r, "Failed to load inside UID range of provided userns: %m");
-                        r = uid_range_load_userns_by_fd(userns_fd, GID_RANGE_USERNS_OUTSIDE, &gid_range_outside);
+
+                        r = uid_range_load_userns_by_fd_full(userns_fd, GID_RANGE_USERNS_OUTSIDE, /* coalesce= */ false, &gid_range_outside);
                         if (r < 0)
                                 return log_debug_errno(r, "Failed to load outside GID range of provided userns: %m");
-                        r = uid_range_load_userns_by_fd(userns_fd, GID_RANGE_USERNS_INSIDE, &gid_range_inside);
+
+                        r = uid_range_load_userns_by_fd_full(userns_fd, GID_RANGE_USERNS_INSIDE, /* coalesce= */ false, &gid_range_inside);
                         if (r < 0)
                                 return log_debug_errno(r, "Failed to load inside GID range of provided userns: %m");
 
-                        /* Be very strict for now */
+                        /* UID and GID mappings must match */
                         if (!uid_range_equal(uid_range_outside, gid_range_outside) ||
-                            !uid_range_equal(uid_range_inside, gid_range_inside) ||
-                            uid_range_outside->n_entries != 1 ||
-                            uid_range_outside->entries[0].nr != 0x10000 ||
-                            uid_range_inside->n_entries != 1 ||
+                            !uid_range_equal(uid_range_inside, gid_range_inside))
+                                return sd_varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor");
+
+                        /* Must have at least one entry, and inside/outside must have matching entry counts */
+                        if (uid_range_is_empty(uid_range_outside) ||
+                            uid_range_outside->n_entries != uid_range_inside->n_entries)
+                                return sd_varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor");
+
+                        /* The first range must be a root UID in the transient range (i.e. aligned
+                         * to a 64K boundary) and mapped to 0 inside the user namespace (size 65536) */
+                        if (!uid_is_transient(uid_range_outside->entries[0].start) ||
+                            (uid_range_outside->entries[0].start & 0xFFFFU) != 0 ||
+                            uid_range_outside->entries[0].nr != NSRESOURCE_UIDS_64K ||
                             uid_range_inside->entries[0].start != 0 ||
-                            uid_range_inside->entries[0].nr != 0x10000)
+                            uid_range_inside->entries[0].nr != NSRESOURCE_UIDS_64K)
                                 return sd_varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor");
 
+                        /* All remaining entries must also be root UIDs in the transient range and
+                         * mapped 1:1, which identifies them as delegated ranges. The last entry
+                         * may also be the root UID in the foreign UID range. */
+                        for (size_t i = 1; i < uid_range_outside->n_entries; i++) {
+                                bool is_last = i + 1 == uid_range_outside->n_entries;
+                                uid_t entry_start = uid_range_outside->entries[i].start;
+
+                                if (!(uid_is_transient(entry_start) ||
+                                      (is_last && uid_is_foreign(entry_start))) ||
+                                    (entry_start & 0xFFFFU) != 0 ||
+                                    uid_range_outside->entries[i].nr != NSRESOURCE_UIDS_64K ||
+                                    uid_range_outside->entries[i].start != uid_range_inside->entries[i].start ||
+                                    uid_range_outside->entries[i].nr != uid_range_inside->entries[i].nr)
+                                        return sd_varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor");
+                        }
+
                         start = uid_range_outside->entries[0].start;
                 } else
                         start = 0;