From 11eef75006819f4e71d04ca2b9a1fb1580854a55 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 21 Oct 2026 13:46:54 +0200 Subject: [PATCH 1/6] statmount: don't copy the reserved empty string past the buffer statmount_string() reserves an empty string at the start of the seq buffer before it calls the handler for the requested string. That's what the offsets of unset strings point to since commit e52e97f09fb6 ("statmount: let unset strings be empty"). If the handler emits nothing the early return from commit 75ead69a7173 ("fs: don't let statmount return empty strings") skips the size check against the caller's buffer. Back then an empty emission meant an empty seq buffer and there was nothing to copy. Now the reserved NUL byte is there and copy_statmount_to_user() copies it to buf + sizeof(struct statmount) no matter how small bufsize is. So request a string that may turn out empty with a buffer that's smaller than struct statmount and the kernel writes one zero byte past the caller's buffer and reports a size that's larger than the buffer. STATMOUNT_MNT_UIDMAP or STATMOUNT_MNT_GIDMAP on a mount that isn't idmapped will do, so will STATMOUNT_FS_SUBTYPE on anything but fuse or STATMOUNT_OPT_ARRAY on a filesystem without ->show_options(). None of this needs privileges. The byte only lands in the caller's own address space but that's still not ok. prepare_kstatmount() already refuses a buffer of exactly sizeof(struct statmount) for string requests. A smaller buffer can't hold a string either and it can't even hold the reserved empty string. So refuse any buffer that isn't larger than the fixed part. That's also what the uapi header says: string requests need space after the fixed part of the structure. Reproducer, unprivileged, on any mount that isn't idmapped: #define _GNU_SOURCE #include #include #include #include #include #include #include #include #define STATMOUNT_FS_SUBTYPE 0x00000100U #define STATMOUNT_MNT_UIDMAP 0x00002000U #define STATX_MNT_ID_UNIQUE 0x00004000U #define SM_SIZE 512 /* sizeof(struct statmount) */ struct mnt_id_req { uint32_t size, mnt_ns_fd; uint64_t mnt_id, param, mnt_ns_id; }; static uint64_t root_mnt_id(void) { unsigned char stx[256]; /* struct statx, stx_mnt_id at offset 144 */ uint64_t id; if (syscall(__NR_statx, AT_FDCWD, "/", 0, STATX_MNT_ID_UNIQUE, stx)) _exit(1); memcpy(&id, stx + 144, sizeof(id)); return id; } static void probe(uint64_t mnt_id, uint64_t mask, int guard) { struct mnt_id_req req = { .size = sizeof(req), .mnt_id = mnt_id, .param = mask, }; long page = sysconf(_SC_PAGESIZE); unsigned char *map, *buf; uint32_t size; long ret; map = mmap(NULL, 2 * page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); memset(map, 0xaa, 2 * page); /* 100 byte buffer ending right at the page boundary */ buf = map + page - 100; if (guard) mprotect(map + page, page, PROT_NONE); errno = 0; ret = syscall(__NR_statmount, &req, buf, 100, 0); printf("mask 0x%04llx%s: ret %ld errno %d", (unsigned long long)mask, guard ? " guard page" : "", ret, errno); if (!guard) { memcpy(&size, buf, sizeof(size)); printf(", size 0x%x, byte[%d] 0x%02x", size, SM_SIZE, buf[SM_SIZE]); } printf("\n"); munmap(map, 2 * page); } int main(void) { uint64_t id = root_mnt_id(); probe(id, STATMOUNT_MNT_UIDMAP, 0); probe(id, STATMOUNT_FS_SUBTYPE, 0); probe(id, STATMOUNT_MNT_UIDMAP, 1); return 0; } Before, on 7.3.0-rc7-00252-g4e3259b08c54 (vfs.fixes) in a VM, the same on a distro 7.1.13: mask 0x2000: ret 0 errno 0, size 0x65, byte[512] 0x00 mask 0x0100: ret 0 errno 0, size 0x65, byte[512] 0x00 mask 0x2000 guard page: ret -1 errno 14 After: mask 0x2000: ret -1 errno 75, size 0xaaaaaaaa, byte[512] 0xaa mask 0x0100: ret -1 errno 75, size 0xaaaaaaaa, byte[512] 0xaa mask 0x2000 guard page: ret -1 errno 75 Reported-by: shaos Link: https://lore.kernel.org/r/20261020110747.959829-1-shawnq1@163.com Fixes: e52e97f09fb6 ("statmount: let unset strings be empty") Cc: stable@vger.kernel.org Link: https://patch.msgid.link/20261021-work-mount-statmount_listns_fixes-v1-1-8917a4c1bd14@kernel.org Reviewed-by: Jeff Layton Signed-off-by: Christian Brauner (Amutable) --- fs/namespace.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/namespace.c b/fs/namespace.c index 9e13ea1e712a..505551013b45 100644 --- a/fs/namespace.c +++ b/fs/namespace.c @@ -6067,7 +6067,8 @@ static int prepare_kstatmount(struct kstatmount *ks, struct mnt_id_req *kreq, ks->bufsize = bufsize; if (ks->mask & STATMOUNT_STRING_REQ) { - if (bufsize == sizeof(ks->sm)) + /* Even the reserved empty string needs room past the fixed part. */ + if (bufsize <= sizeof(ks->sm)) return -EOVERFLOW; ks->seq.buf = kvmalloc(seq_size, GFP_KERNEL_ACCOUNT); From e4ada0bf878ad3d1f348e2fe544ffad25ed8be2b Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 21 Oct 2026 13:46:55 +0200 Subject: [PATCH 2/6] selftests/filesystems: check short buffers for statmount() strings Request strings that end up empty with buffers that aren't larger than the fixed part of struct statmount and check that: - every request fails with EOVERFLOW - nothing is written past the end of the buffer, a guard page right behind it catches that The buffer sizes run from zero up to sizeof(struct statmount) and the requests cover STATMOUNT_FS_TYPE, STATMOUNT_FS_SUBTYPE and STATMOUNT_MNT_UIDMAP. Link: https://patch.msgid.link/20261021-work-mount-statmount_listns_fixes-v1-2-8917a4c1bd14@kernel.org Signed-off-by: Christian Brauner (Amutable) --- .../filesystems/statmount/statmount_test.c | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/tools/testing/selftests/filesystems/statmount/statmount_test.c b/tools/testing/selftests/filesystems/statmount/statmount_test.c index 52544512edc7..b237a95ec9fb 100644 --- a/tools/testing/selftests/filesystems/statmount/statmount_test.c +++ b/tools/testing/selftests/filesystems/statmount/statmount_test.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -578,6 +579,63 @@ static void test_statmount_string(uint64_t mask, size_t off, const char *name) free(sm); } +/* + * A string request needs a buffer that is larger than the fixed part of + * struct statmount. That's true even if the string turns out to be empty + * since the empty string itself is placed past the fixed part. + */ +static void test_statmount_string_short_buffer(void) +{ + const struct { + uint64_t mask; + const char *name; + } reqs[] = { + { STATMOUNT_FS_TYPE, "fs type" }, + { STATMOUNT_FS_SUBTYPE, "fs subtype" }, + { STATMOUNT_MNT_UIDMAP, "mount uidmap" }, + }; + const size_t sizes[] = { 0, 1, 100, sizeof(struct statmount) - 1, + sizeof(struct statmount) }; + long page = sysconf(_SC_PAGESIZE); + unsigned char *map; + size_t i, j; + int ret; + + map = mmap(NULL, 2 * page, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (map == MAP_FAILED) { + ksft_test_result_fail("mmap: %s\n", strerror(errno)); + return; + } + + /* Anything written past the end of the buffer faults. */ + if (mprotect(map + page, page, PROT_NONE)) { + ksft_test_result_fail("mprotect: %s\n", strerror(errno)); + goto out; + } + + for (i = 0; i < ARRAY_SIZE(reqs); i++) { + for (j = 0; j < ARRAY_SIZE(sizes); j++) { + /* The buffer ends right at the guard page. */ + struct statmount *sm = (void *)(map + page - sizes[j]); + + errno = 0; + ret = statmount(root_id, 0, 0, reqs[i].mask, sm, + sizes[j], 0); + if (ret != -1 || errno != EOVERFLOW) { + ksft_test_result_fail("statmount %s with %zu byte buffer: %s\n", + reqs[i].name, sizes[j], + ret ? strerror(errno) : "success"); + goto out; + } + } + } + + ksft_test_result_pass("statmount string short buffer\n"); +out: + munmap(map, 2 * page); +} + static void test_listmount_tree(void) { ssize_t res; @@ -869,7 +927,7 @@ int main(void) setup_namespace(); - ksft_set_plan(17); + ksft_set_plan(18); test_listmount_empty_root(); test_statmount_zero_mask(); test_statmount_mnt_basic(); @@ -884,6 +942,7 @@ int main(void) test_statmount_string(all_mask, str_off(mnt_root), "mount root & all"); test_statmount_string(all_mask, str_off(mnt_point), "mount point & all"); test_statmount_string(all_mask, str_off(fs_type), "fs type & all"); + test_statmount_string_short_buffer(); test_listmount_tree(); test_statmount_by_fd_unmounted(); From 95a5412022e437691f4eb1924ff249d9044f5ed1 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 21 Oct 2026 13:46:56 +0200 Subject: [PATCH 3/6] listmount: don't wrap around with a U64_MAX cursor do_listmount() takes the last mount id the caller saw and looks up the first mount after it by searching for last_mnt_id + 1. With U64_MAX that wraps around to zero and the search finds the first mount in the namespace. So a cursor at the largest possible id restarts the listing from the beginning instead of ending it. A cursor of U64_MAX - 1 correctly returns nothing. No mount will ever have that id. Mount ids come from a counter that starts at MNT_UNIQUE_ID_OFFSET + 1 and a caller that only feeds back ids it got from us can't hit this. It's still wrong. Nothing can come after the largest possible id so return no entries for it, just like for any other cursor past the last mount. The reverse direction isn't affected. A cursor of zero is special-cased and anything else is larger than MNT_UNIQUE_ID_OFFSET so last_mnt_id - 1 can't underflow. Reproducer, unprivileged: #define _GNU_SOURCE #include #include #include #include #include #define LSMT_ROOT 0xffffffffffffffffULL struct mnt_id_req { uint32_t size, mnt_ns_fd; uint64_t mnt_id, param, mnt_ns_id; }; static void list(uint64_t cursor) { struct mnt_id_req req = { .size = sizeof(req), .mnt_id = LSMT_ROOT, .param = cursor, }; uint64_t ids[8]; long ret; errno = 0; ret = syscall(__NR_listmount, &req, ids, 8, 0); printf("cursor %llu: ret %ld errno %d", (unsigned long long)cursor, ret, errno); for (long i = 0; i < ret && i < 1; i++) printf("%s%llu", i ? "," : " ids ", (unsigned long long)ids[i]); printf("%s\n", ret > 1 ? ",..." : ""); } int main(void) { list(0); list(UINT64_MAX - 1); list(UINT64_MAX); return 0; } Before, on 7.3.0-rc7-00252-g4e3259b08c54 (vfs.fixes) in a VM: cursor 0: ret 5 errno 0 ids 2147483651,... cursor 18446744073709551614: ret 0 errno 0 cursor 18446744073709551615: ret 5 errno 0 ids 2147483651,... After: cursor 0: ret 5 errno 0 ids 2147483651,... cursor 18446744073709551614: ret 0 errno 0 cursor 18446744073709551615: ret 0 errno 0 Reported-by: shaos Link: https://lore.kernel.org/r/20261020110732.959552-1-shawnq1@163.com Fixes: b4c2bea8ceaa ("add listmount(2) syscall") Cc: stable@vger.kernel.org Link: https://patch.msgid.link/20261021-work-mount-statmount_listns_fixes-v1-3-8917a4c1bd14@kernel.org Reviewed-by: Jeff Layton Signed-off-by: Christian Brauner (Amutable) --- fs/namespace.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fs/namespace.c b/fs/namespace.c index 505551013b45..e5feb4e48290 100644 --- a/fs/namespace.c +++ b/fs/namespace.c @@ -6262,6 +6262,8 @@ static ssize_t do_listmount(struct klistmount *kls, bool reverse) } else { if (reverse) first = mnt_find_id_at_reverse(ns, last_mnt_id - 1); + else if (last_mnt_id == U64_MAX) + first = NULL; /* nothing comes after the largest id */ else first = mnt_find_id_at(ns, last_mnt_id + 1); } From 8c407f829ac7b344ba0a8f43eb7b0c0e40b67afa Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 21 Oct 2026 13:46:57 +0200 Subject: [PATCH 4/6] selftests/filesystems: check listmount() with a U64_MAX cursor Pass the largest possible mount id as the cursor and check that listmount() returns no entries instead of starting over at the first mount. Link: https://patch.msgid.link/20261021-work-mount-statmount_listns_fixes-v1-4-8917a4c1bd14@kernel.org Signed-off-by: Christian Brauner (Amutable) --- .../selftests/filesystems/statmount/listmount_test.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tools/testing/selftests/filesystems/statmount/listmount_test.c b/tools/testing/selftests/filesystems/statmount/listmount_test.c index 8bc82f38c42f..e603525ce17b 100644 --- a/tools/testing/selftests/filesystems/statmount/listmount_test.c +++ b/tools/testing/selftests/filesystems/statmount/listmount_test.c @@ -63,4 +63,15 @@ TEST(listmount_backward) } } +/* A cursor at the largest possible mount id must not wrap around. */ +TEST(listmount_cursor_max) +{ + uint64_t list[LISTMNT_BUFFER]; + ssize_t nr_mounts; + + nr_mounts = listmount(LSMT_ROOT, 0, UINT64_MAX, list, + LISTMNT_BUFFER, 0); + ASSERT_EQ(nr_mounts, 0); +} + TEST_HARNESS_MAIN From 2a1887000e2d4adc8cb3933b53431dea6d5e310b Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 21 Oct 2026 13:46:58 +0200 Subject: [PATCH 5/6] listns: don't wrap around with a U64_MAX cursor do_listns() and do_listns_userns() take the last namespace id the caller saw and look up the first namespace after it by searching the rbtree for last_ns_id + 1. With U64_MAX that wraps around to zero and the search returns the namespace with the smallest id. So a cursor at the largest possible id restarts the listing from the beginning instead of failing with ENOENT like a cursor of U64_MAX - 1 does. That's the case for the unified tree, the type-specific trees and the owner tree alike. No namespace will ever have that id. They come from a counter and a caller that only feeds back ids it got from us can't hit this. It's still wrong. Instead of adding one to the cursor and searching for the smallest id that's greater or equal make the lookup helpers search for the smallest id that's strictly greater than the cursor. That's what both callers want anyway and there's nothing left to overflow. Reproducer, unprivileged: #define _GNU_SOURCE #include #include #include #include #include #ifndef __NR_listns #define __NR_listns 470 #endif #define CLONE_NEWUSER 0x10000000U #define LISTNS_CURRENT_USER 0xffffffffffffffffULL struct ns_id_req { uint32_t size, spare; uint64_t ns_id; uint32_t ns_type, spare2; uint64_t user_ns_id; }; static void list(const char *tree, uint32_t type, uint64_t owner, uint64_t cursor) { struct ns_id_req req = { .size = sizeof(req), .ns_id = cursor, .ns_type = type, .user_ns_id = owner, }; uint64_t ids[8]; long ret; errno = 0; ret = syscall(__NR_listns, &req, ids, 8, 0); printf("%s cursor %llu: ret %ld errno %d", tree, (unsigned long long)cursor, ret, errno); for (long i = 0; i < ret && i < 3; i++) printf("%s%llu", i ? "," : " ids ", (unsigned long long)ids[i]); printf("%s\n", ret > 3 ? ",..." : ""); } int main(void) { static const uint64_t cursors[] = { 0, UINT64_MAX - 1, UINT64_MAX }; for (int i = 0; i < 3; i++) list("unified", 0, 0, cursors[i]); for (int i = 0; i < 3; i++) list("userns", CLONE_NEWUSER, 0, cursors[i]); for (int i = 0; i < 3; i++) list("owner", 0, LISTNS_CURRENT_USER, cursors[i]); return 0; } Before, on 7.3.0-rc7-00252-g4e3259b08c54 (vfs.fixes) in a VM: unified cursor 0: ret 8 errno 0 ids 1,2,3,... unified cursor 18446744073709551614: ret -1 errno 2 unified cursor 18446744073709551615: ret 8 errno 0 ids 1,2,3,... userns cursor 0: ret 1 errno 0 ids 3 userns cursor 18446744073709551614: ret -1 errno 2 userns cursor 18446744073709551615: ret 1 errno 0 ids 3 owner cursor 0: ret 8 errno 0 ids 1,2,4,... owner cursor 18446744073709551614: ret -1 errno 2 owner cursor 18446744073709551615: ret 8 errno 0 ids 1,2,4,... After: unified cursor 0: ret 8 errno 0 ids 1,2,3,... unified cursor 18446744073709551614: ret -1 errno 2 unified cursor 18446744073709551615: ret -1 errno 2 userns cursor 0: ret 1 errno 0 ids 3 userns cursor 18446744073709551614: ret -1 errno 2 userns cursor 18446744073709551615: ret -1 errno 2 owner cursor 0: ret 8 errno 0 ids 1,2,4,... owner cursor 18446744073709551614: ret -1 errno 2 owner cursor 18446744073709551615: ret -1 errno 2 Reported-by: shaos Link: https://lore.kernel.org/r/20261020110732.959552-1-shawnq1@163.com Fixes: 76b6f5dfb3fd ("nstree: add listns()") Cc: stable@vger.kernel.org Link: https://patch.msgid.link/20261021-work-mount-statmount_listns_fixes-v1-5-8917a4c1bd14@kernel.org Reviewed-by: Jeff Layton Signed-off-by: Christian Brauner (Amutable) --- kernel/nstree.c | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/kernel/nstree.c b/kernel/nstree.c index 831f279d174a..58b2b55fa8d2 100644 --- a/kernel/nstree.c +++ b/kernel/nstree.c @@ -459,10 +459,10 @@ static inline int prepare_klistns(struct klistns *kls, struct ns_id_req *kreq, } /* - * Lookup a namespace owned by owner with id >= ns_id. - * Returns the namespace with the smallest id that is >= ns_id. + * Lookup a namespace owned by owner with id > ns_id. + * Returns the namespace with the smallest id that is > ns_id. */ -static struct ns_common *lookup_ns_owner_at(u64 ns_id, struct ns_common *owner) +static struct ns_common *lookup_ns_owner_after(u64 ns_id, struct ns_common *owner) { struct ns_common *ret = NULL; struct rb_node *node; @@ -476,10 +476,8 @@ static struct ns_common *lookup_ns_owner_at(u64 ns_id, struct ns_common *owner) struct ns_common *ns; ns = node_to_ns_owner(node); - if (ns_id <= ns->ns_id) { + if (ns_id < ns->ns_id) { ret = ns; - if (ns_id == ns->ns_id) - break; node = node->rb_left; } else { node = node->rb_right; @@ -565,7 +563,7 @@ static ssize_t do_listns_userns(struct klistns *kls) * then use it's list entry to iterate from there. */ if (kls->last_ns_id) { - kls->first_ns = lookup_ns_owner_at(kls->last_ns_id + 1, ns); + kls->first_ns = lookup_ns_owner_after(kls->last_ns_id, ns); if (!kls->first_ns) return -ENOENT; first_ns = kls->first_ns; @@ -613,10 +611,10 @@ static ssize_t do_listns_userns(struct klistns *kls) } /* - * Lookup a namespace with id >= ns_id in either the unified tree or a type-specific tree. - * Returns the namespace with the smallest id that is >= ns_id. + * Lookup a namespace with id > ns_id in either the unified tree or a type-specific tree. + * Returns the namespace with the smallest id that is > ns_id. */ -static struct ns_common *lookup_ns_id_at(u64 ns_id, int ns_type) +static struct ns_common *lookup_ns_id_after(u64 ns_id, int ns_type) { struct ns_common *ret = NULL; struct ns_tree_root *ns_tree = NULL; @@ -643,13 +641,8 @@ static struct ns_common *lookup_ns_id_at(u64 ns_id, int ns_type) else ns = node_to_ns_unified(node); - if (ns_id <= ns->ns_id) { - if (ns_type) - ret = node_to_ns(node); - else - ret = node_to_ns_unified(node); - if (ns_id == ns->ns_id) - break; + if (ns_id < ns->ns_id) { + ret = ns; node = node->rb_left; } else { node = node->rb_right; @@ -708,7 +701,7 @@ static ssize_t do_listns(struct klistns *kls) } if (kls->last_ns_id) { - kls->first_ns = lookup_ns_id_at(kls->last_ns_id + 1, ns_type); + kls->first_ns = lookup_ns_id_after(kls->last_ns_id, ns_type); if (!kls->first_ns) return -ENOENT; first_ns = kls->first_ns; From 486b257357c23743d9729ea715136d105fff461e Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 21 Oct 2026 13:46:59 +0200 Subject: [PATCH 6/6] selftests/namespaces: check listns() with a U64_MAX cursor Pass the largest possible namespace id as the cursor and check that listns() fails with ENOENT instead of starting over: - in the unified tree - in a type-specific tree - in the owner tree of the caller's user namespace Nothing can come after the largest possible id. Link: https://patch.msgid.link/20261021-work-mount-statmount_listns_fixes-v1-6-8917a4c1bd14@kernel.org Signed-off-by: Christian Brauner (Amutable) --- .../selftests/namespaces/listns_test.c | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tools/testing/selftests/namespaces/listns_test.c b/tools/testing/selftests/namespaces/listns_test.c index 8a95789d6a87..538d60f908b4 100644 --- a/tools/testing/selftests/namespaces/listns_test.c +++ b/tools/testing/selftests/namespaces/listns_test.c @@ -676,4 +676,38 @@ TEST(listns_error_cases) } } +/* + * A cursor at the largest possible namespace id must not wrap around and + * restart the listing from the beginning. + */ +TEST(listns_cursor_max) +{ + struct ns_id_req req = { + .size = sizeof(req), + .ns_id = ULLONG_MAX, + }; + __u64 ns_ids[10]; + ssize_t ret; + + /* unified tree */ + ret = sys_listns(&req, ns_ids, ARRAY_SIZE(ns_ids), 0); + if (ret < 0 && errno == ENOSYS) + SKIP(return, "listns() not supported"); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, ENOENT); + + /* type-specific tree */ + req.ns_type = CLONE_NEWUSER; + ret = sys_listns(&req, ns_ids, ARRAY_SIZE(ns_ids), 0); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, ENOENT); + + /* owner tree */ + req.ns_type = 0; + req.user_ns_id = LISTNS_CURRENT_USER; + ret = sys_listns(&req, ns_ids, ARRAY_SIZE(ns_ids), 0); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, ENOENT); +} + TEST_HARNESS_MAIN