Skip to content

fs: fixes for statmount() short buffers and listmount()/listns() cursors - #2935

Open
vfsci-bot[bot] wants to merge 6 commits into
vfs.base.cifrom
pw/1190450/vfs.base.ci
Open

vfsci-bot[bot] wants to merge 6 commits into
vfs.base.cifrom
pw/1190450/vfs.base.ci

Conversation

@vfsci-bot

@vfsci-bot vfsci-bot Bot commented Oct 21, 2026

Copy link
Copy Markdown

Series: https://patchwork.kernel.org/project/linux-fsdevel/list/?series=1190450
Submitter: Christian Brauner
Version: 1
Patches: 6/6
Message-ID: <20261021-work-mount-statmount_listns_fixes-v1-0-8917a4c1bd14@kernel.org>
Base: vfs.base.ci
Lore: https://lore.kernel.org/linux-fsdevel/20261021-work-mount-statmount_listns_fixes-v1-0-8917a4c1bd14@kernel.org


Automated by ml2pr

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 e52e97f
("statmount: let unset strings be empty").

If the handler emits nothing the early return from commit 75ead69
("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: e52e97f ("statmount: let unset strings be empty")
Cc: stable@vger.kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
Reviewed-by: Jeff Layton <jlayton@kernel.org>
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>
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: b4c2bea ("add listmount(2) syscall")
Cc: stable@vger.kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
Reviewed-by: Jeff Layton <jlayton@kernel.org>
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>
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: 76b6f5d ("nstree: add listns()")
Cc: stable@vger.kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
Reviewed-by: Jeff Layton <jlayton@kernel.org>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant