* [PATCH 1/6] statmount: don't copy the reserved empty string past the buffer
2026-10-21 11:46 [PATCH 0/6] fs: fixes for statmount() short buffers and listmount()/listns() cursors Christian Brauner
@ 2026-10-21 11:46 ` Christian Brauner
2026-10-21 11:52 ` Jeff Layton
2026-10-21 15:18 ` Jan Kara
2026-10-21 11:46 ` [PATCH 2/6] selftests/filesystems: check short buffers for statmount() strings Christian Brauner
` (5 subsequent siblings)
6 siblings, 2 replies; 14+ messages in thread
From: Christian Brauner @ 2026-10-21 11:46 UTC (permalink / raw)
To: linux-fsdevel
Cc: Alexander Viro, Jan Kara, Miklos Szeredi, Jeff Layton, shaos,
linux-kernel, stable, Christian Brauner (Amutable)
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 <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <unistd.h>
#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 <shawnq1@163.com>
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
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
fs/namespace.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/fs/namespace.c b/fs/namespace.c
index c30f063742f4..6c9c0cdbe890 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -6058,7 +6058,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);
--
2.53.0
^ permalink raw reply [flat|nested] 14+ messages in thread* Re: [PATCH 1/6] statmount: don't copy the reserved empty string past the buffer
2026-10-21 11:46 ` [PATCH 1/6] statmount: don't copy the reserved empty string past the buffer Christian Brauner
@ 2026-10-21 11:52 ` Jeff Layton
2026-10-21 15:18 ` Jan Kara
1 sibling, 0 replies; 14+ messages in thread
From: Jeff Layton @ 2026-10-21 11:52 UTC (permalink / raw)
To: Christian Brauner, linux-fsdevel
Cc: Alexander Viro, Jan Kara, Miklos Szeredi, shaos, linux-kernel, stable
On Wed, 2026-10-21 at 13:46 +0200, Christian Brauner wrote:
> 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 <errno.h>
> #include <fcntl.h>
> #include <stdint.h>
> #include <stdio.h>
> #include <string.h>
> #include <sys/mman.h>
> #include <sys/syscall.h>
> #include <unistd.h>
> #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 <shawnq1@163.com>
> 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
> Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
> ---
> fs/namespace.c | 3 ++-
> 1 file changed, 2 insertions(+), 1 deletion(-)
>
> diff --git a/fs/namespace.c b/fs/namespace.c
> index c30f063742f4..6c9c0cdbe890 100644
> --- a/fs/namespace.c
> +++ b/fs/namespace.c
> @@ -6058,7 +6058,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);
Reviewed-by: Jeff Layton <jlayton@kernel.org>
^ permalink raw reply [flat|nested] 14+ messages in thread* Re: [PATCH 1/6] statmount: don't copy the reserved empty string past the buffer
2026-10-21 11:46 ` [PATCH 1/6] statmount: don't copy the reserved empty string past the buffer Christian Brauner
2026-10-21 11:52 ` Jeff Layton
@ 2026-10-21 15:18 ` Jan Kara
1 sibling, 0 replies; 14+ messages in thread
From: Jan Kara @ 2026-10-21 15:18 UTC (permalink / raw)
To: Christian Brauner
Cc: linux-fsdevel, Alexander Viro, Jan Kara, Miklos Szeredi,
Jeff Layton, shaos, linux-kernel, stable
On Wed 21-10-26 13:46:54, Christian Brauner wrote:
> 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 <errno.h>
> #include <fcntl.h>
> #include <stdint.h>
> #include <stdio.h>
> #include <string.h>
> #include <sys/mman.h>
> #include <sys/syscall.h>
> #include <unistd.h>
> #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 <shawnq1@163.com>
> 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
> Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
Looks good but FWIW I've already reviewed the same fix here:
https://lore.kernel.org/all/e6ysb6lyld5remaojkqqbaffvlsxwhr4tmurifp7mhbqrj7l54@6yqqwu3d7e3y
Feel free to add:
Reviewed-by: Jan Kara <jack@suse.cz>
Honza
> ---
> fs/namespace.c | 3 ++-
> 1 file changed, 2 insertions(+), 1 deletion(-)
>
> diff --git a/fs/namespace.c b/fs/namespace.c
> index c30f063742f4..6c9c0cdbe890 100644
> --- a/fs/namespace.c
> +++ b/fs/namespace.c
> @@ -6058,7 +6058,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);
>
> --
> 2.53.0
>
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH 2/6] selftests/filesystems: check short buffers for statmount() strings
2026-10-21 11:46 [PATCH 0/6] fs: fixes for statmount() short buffers and listmount()/listns() cursors Christian Brauner
2026-10-21 11:46 ` [PATCH 1/6] statmount: don't copy the reserved empty string past the buffer Christian Brauner
@ 2026-10-21 11:46 ` Christian Brauner
2026-10-21 11:46 ` [PATCH 3/6] listmount: don't wrap around with a U64_MAX cursor Christian Brauner
` (4 subsequent siblings)
6 siblings, 0 replies; 14+ messages in thread
From: Christian Brauner @ 2026-10-21 11:46 UTC (permalink / raw)
To: linux-fsdevel
Cc: Alexander Viro, Jan Kara, Miklos Szeredi, Jeff Layton, shaos,
linux-kernel, Christian Brauner (Amutable)
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.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
.../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 60c2c544db6a..2947546a9367 100644
--- a/tools/testing/selftests/filesystems/statmount/statmount_test.c
+++ b/tools/testing/selftests/filesystems/statmount/statmount_test.c
@@ -7,6 +7,7 @@
#include <sched.h>
#include <fcntl.h>
#include <sys/param.h>
+#include <sys/mman.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/statfs.h>
@@ -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();
--
2.53.0
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH 3/6] listmount: don't wrap around with a U64_MAX cursor
2026-10-21 11:46 [PATCH 0/6] fs: fixes for statmount() short buffers and listmount()/listns() cursors Christian Brauner
2026-10-21 11:46 ` [PATCH 1/6] statmount: don't copy the reserved empty string past the buffer Christian Brauner
2026-10-21 11:46 ` [PATCH 2/6] selftests/filesystems: check short buffers for statmount() strings Christian Brauner
@ 2026-10-21 11:46 ` Christian Brauner
2026-10-21 11:52 ` Jeff Layton
2026-10-21 15:25 ` Jan Kara
2026-10-21 11:46 ` [PATCH 4/6] selftests/filesystems: check listmount() " Christian Brauner
` (3 subsequent siblings)
6 siblings, 2 replies; 14+ messages in thread
From: Christian Brauner @ 2026-10-21 11:46 UTC (permalink / raw)
To: linux-fsdevel
Cc: Alexander Viro, Jan Kara, Miklos Szeredi, Jeff Layton, shaos,
linux-kernel, stable, Christian Brauner (Amutable)
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 <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/syscall.h>
#include <unistd.h>
#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 <shawnq1@163.com>
Link: https://lore.kernel.org/r/20261020110732.959552-1-shawnq1@163.com
Fixes: b4c2bea8ceaa ("add listmount(2) syscall")
Cc: stable@vger.kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
fs/namespace.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/fs/namespace.c b/fs/namespace.c
index 6c9c0cdbe890..dd60123a0cb0 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -6253,6 +6253,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);
}
--
2.53.0
^ permalink raw reply [flat|nested] 14+ messages in thread* Re: [PATCH 3/6] listmount: don't wrap around with a U64_MAX cursor
2026-10-21 11:46 ` [PATCH 3/6] listmount: don't wrap around with a U64_MAX cursor Christian Brauner
@ 2026-10-21 11:52 ` Jeff Layton
2026-10-21 15:25 ` Jan Kara
1 sibling, 0 replies; 14+ messages in thread
From: Jeff Layton @ 2026-10-21 11:52 UTC (permalink / raw)
To: Christian Brauner, linux-fsdevel
Cc: Alexander Viro, Jan Kara, Miklos Szeredi, shaos, linux-kernel, stable
On Wed, 2026-10-21 at 13:46 +0200, Christian Brauner wrote:
> 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 <errno.h>
> #include <stdint.h>
> #include <stdio.h>
> #include <sys/syscall.h>
> #include <unistd.h>
> #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 <shawnq1@163.com>
> Link: https://lore.kernel.org/r/20261020110732.959552-1-shawnq1@163.com
> Fixes: b4c2bea8ceaa ("add listmount(2) syscall")
> Cc: stable@vger.kernel.org
> Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
> ---
> fs/namespace.c | 2 ++
> 1 file changed, 2 insertions(+)
>
> diff --git a/fs/namespace.c b/fs/namespace.c
> index 6c9c0cdbe890..dd60123a0cb0 100644
> --- a/fs/namespace.c
> +++ b/fs/namespace.c
> @@ -6253,6 +6253,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);
> }
Reviewed-by: Jeff Layton <jlayton@kernel.org>
^ permalink raw reply [flat|nested] 14+ messages in thread* Re: [PATCH 3/6] listmount: don't wrap around with a U64_MAX cursor
2026-10-21 11:46 ` [PATCH 3/6] listmount: don't wrap around with a U64_MAX cursor Christian Brauner
2026-10-21 11:52 ` Jeff Layton
@ 2026-10-21 15:25 ` Jan Kara
1 sibling, 0 replies; 14+ messages in thread
From: Jan Kara @ 2026-10-21 15:25 UTC (permalink / raw)
To: Christian Brauner
Cc: linux-fsdevel, Alexander Viro, Jan Kara, Miklos Szeredi,
Jeff Layton, shaos, linux-kernel, stable
On Wed 21-10-26 13:46:56, Christian Brauner wrote:
> 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 <errno.h>
> #include <stdint.h>
> #include <stdio.h>
> #include <sys/syscall.h>
> #include <unistd.h>
> #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 <shawnq1@163.com>
> Link: https://lore.kernel.org/r/20261020110732.959552-1-shawnq1@163.com
> Fixes: b4c2bea8ceaa ("add listmount(2) syscall")
> Cc: stable@vger.kernel.org
> Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
Looks good. Feel free to add:
Reviewed-by: Jan Kara <jack@suse.cz>
Honza
> ---
> fs/namespace.c | 2 ++
> 1 file changed, 2 insertions(+)
>
> diff --git a/fs/namespace.c b/fs/namespace.c
> index 6c9c0cdbe890..dd60123a0cb0 100644
> --- a/fs/namespace.c
> +++ b/fs/namespace.c
> @@ -6253,6 +6253,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);
> }
>
> --
> 2.53.0
>
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH 4/6] selftests/filesystems: check listmount() with a U64_MAX cursor
2026-10-21 11:46 [PATCH 0/6] fs: fixes for statmount() short buffers and listmount()/listns() cursors Christian Brauner
` (2 preceding siblings ...)
2026-10-21 11:46 ` [PATCH 3/6] listmount: don't wrap around with a U64_MAX cursor Christian Brauner
@ 2026-10-21 11:46 ` Christian Brauner
2026-10-21 11:46 ` [PATCH 5/6] listns: don't wrap around " Christian Brauner
` (2 subsequent siblings)
6 siblings, 0 replies; 14+ messages in thread
From: Christian Brauner @ 2026-10-21 11:46 UTC (permalink / raw)
To: linux-fsdevel
Cc: Alexander Viro, Jan Kara, Miklos Szeredi, Jeff Layton, shaos,
linux-kernel, Christian Brauner (Amutable)
Pass the largest possible mount id as the cursor and check that
listmount() returns no entries instead of starting over at the first
mount.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
.../testing/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
--
2.53.0
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH 5/6] listns: don't wrap around with a U64_MAX cursor
2026-10-21 11:46 [PATCH 0/6] fs: fixes for statmount() short buffers and listmount()/listns() cursors Christian Brauner
` (3 preceding siblings ...)
2026-10-21 11:46 ` [PATCH 4/6] selftests/filesystems: check listmount() " Christian Brauner
@ 2026-10-21 11:46 ` Christian Brauner
2026-10-21 11:55 ` Jeff Layton
2026-10-21 15:29 ` Jan Kara
2026-10-21 11:46 ` [PATCH 6/6] selftests/namespaces: check listns() " Christian Brauner
2026-10-21 13:18 ` [PATCH 0/6] fs: fixes for statmount() short buffers and listmount()/listns() cursors Christian Brauner
6 siblings, 2 replies; 14+ messages in thread
From: Christian Brauner @ 2026-10-21 11:46 UTC (permalink / raw)
To: linux-fsdevel
Cc: Alexander Viro, Jan Kara, Miklos Szeredi, Jeff Layton, shaos,
linux-kernel, stable, Christian Brauner (Amutable)
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 <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/syscall.h>
#include <unistd.h>
#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 <shawnq1@163.com>
Link: https://lore.kernel.org/r/20261020110732.959552-1-shawnq1@163.com
Fixes: 76b6f5dfb3fd ("nstree: add listns()")
Cc: stable@vger.kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
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;
--
2.53.0
^ permalink raw reply [flat|nested] 14+ messages in thread* Re: [PATCH 5/6] listns: don't wrap around with a U64_MAX cursor
2026-10-21 11:46 ` [PATCH 5/6] listns: don't wrap around " Christian Brauner
@ 2026-10-21 11:55 ` Jeff Layton
2026-10-21 15:29 ` Jan Kara
1 sibling, 0 replies; 14+ messages in thread
From: Jeff Layton @ 2026-10-21 11:55 UTC (permalink / raw)
To: Christian Brauner, linux-fsdevel
Cc: Alexander Viro, Jan Kara, Miklos Szeredi, shaos, linux-kernel, stable
On Wed, 2026-10-21 at 13:46 +0200, Christian Brauner wrote:
> 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 <errno.h>
> #include <stdint.h>
> #include <stdio.h>
> #include <sys/syscall.h>
> #include <unistd.h>
> #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 <shawnq1@163.com>
> Link: https://lore.kernel.org/r/20261020110732.959552-1-shawnq1@163.com
> Fixes: 76b6f5dfb3fd ("nstree: add listns()")
> Cc: stable@vger.kernel.org
> Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
> ---
> 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;
Reviewed-by: Jeff Layton <jlayton@kernel.org>
^ permalink raw reply [flat|nested] 14+ messages in thread* Re: [PATCH 5/6] listns: don't wrap around with a U64_MAX cursor
2026-10-21 11:46 ` [PATCH 5/6] listns: don't wrap around " Christian Brauner
2026-10-21 11:55 ` Jeff Layton
@ 2026-10-21 15:29 ` Jan Kara
1 sibling, 0 replies; 14+ messages in thread
From: Jan Kara @ 2026-10-21 15:29 UTC (permalink / raw)
To: Christian Brauner
Cc: linux-fsdevel, Alexander Viro, Jan Kara, Miklos Szeredi,
Jeff Layton, shaos, linux-kernel, stable
On Wed 21-10-26 13:46:58, Christian Brauner wrote:
> 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 <errno.h>
> #include <stdint.h>
> #include <stdio.h>
> #include <sys/syscall.h>
> #include <unistd.h>
> #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 <shawnq1@163.com>
> Link: https://lore.kernel.org/r/20261020110732.959552-1-shawnq1@163.com
> Fixes: 76b6f5dfb3fd ("nstree: add listns()")
> Cc: stable@vger.kernel.org
> Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
Looks good! Feel free to add:
Reviewed-by: Jan Kara <jack@suse.cz>
Honza
> ---
> 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;
>
> --
> 2.53.0
>
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH 6/6] selftests/namespaces: check listns() with a U64_MAX cursor
2026-10-21 11:46 [PATCH 0/6] fs: fixes for statmount() short buffers and listmount()/listns() cursors Christian Brauner
` (4 preceding siblings ...)
2026-10-21 11:46 ` [PATCH 5/6] listns: don't wrap around " Christian Brauner
@ 2026-10-21 11:46 ` Christian Brauner
2026-10-21 13:18 ` [PATCH 0/6] fs: fixes for statmount() short buffers and listmount()/listns() cursors Christian Brauner
6 siblings, 0 replies; 14+ messages in thread
From: Christian Brauner @ 2026-10-21 11:46 UTC (permalink / raw)
To: linux-fsdevel
Cc: Alexander Viro, Jan Kara, Miklos Szeredi, Jeff Layton, shaos,
linux-kernel, Christian Brauner (Amutable)
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.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
tools/testing/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
--
2.53.0
^ permalink raw reply [flat|nested] 14+ messages in thread* Re: [PATCH 0/6] fs: fixes for statmount() short buffers and listmount()/listns() cursors
2026-10-21 11:46 [PATCH 0/6] fs: fixes for statmount() short buffers and listmount()/listns() cursors Christian Brauner
` (5 preceding siblings ...)
2026-10-21 11:46 ` [PATCH 6/6] selftests/namespaces: check listns() " Christian Brauner
@ 2026-10-21 13:18 ` Christian Brauner
6 siblings, 0 replies; 14+ messages in thread
From: Christian Brauner @ 2026-10-21 13:18 UTC (permalink / raw)
To: linux-fsdevel, Christian Brauner
Cc: Alexander Viro, Jan Kara, Miklos Szeredi, Jeff Layton, shaos,
linux-kernel, stable
On Wed, 21 Oct 2026 13:46:53 +0200, Christian Brauner wrote:
> shaos reported two issues in [1] and [2]. Both are real.
>
> - statmount() reserves the empty string in the seq buffer before a
> handler runs but the early return for "nothing emitted" never checks
> that byte against the caller's buffer. So ask for STATMOUNT_MNT_UIDMAP
> on a mount that isn't idmapped with a buffer smaller than struct
> statmount and we write one zero byte past the buffer. It's the
> caller's own memory but that's still not ok. I'm fixing this in
> prepare_kstatmount() where the exact-size check already lives so that
> unset strings keep pointing at an empty string.
>
> [...]
Applied to the vfs.fixes branch of the vfs/vfs.git tree.
Patches in the vfs.fixes branch should appear in linux-next soon.
Please report any outstanding bugs that were missed during review in a
new review to the original patch series allowing us to drop it.
It's encouraged to provide Acked-bys and Reviewed-bys even though the
patch has now been applied. If possible patch trailers will be updated.
Note that commit hashes shown below are subject to change due to rebase,
trailer updates or similar. If in doubt, please check the listed branch.
tree: https://git.kernel.org/pub/scm/linux/kernel/git/vfs/vfs.git
branch: vfs.fixes
[1/6] statmount: don't copy the reserved empty string past the buffer
https://git.kernel.org/vfs/vfs/c/6410ba49924f
[2/6] selftests/filesystems: check short buffers for statmount() strings
https://git.kernel.org/vfs/vfs/c/4620c3aa7259
[3/6] listmount: don't wrap around with a U64_MAX cursor
https://git.kernel.org/vfs/vfs/c/db1ad1eac126
[4/6] selftests/filesystems: check listmount() with a U64_MAX cursor
https://git.kernel.org/vfs/vfs/c/7d9ebfa4e4fc
[5/6] listns: don't wrap around with a U64_MAX cursor
https://git.kernel.org/vfs/vfs/c/f98c8e0d1536
[6/6] selftests/namespaces: check listns() with a U64_MAX cursor
https://git.kernel.org/vfs/vfs/c/21f54bd8da86
^ permalink raw reply [flat|nested] 14+ messages in thread