Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
From: Uladzislau Rezki <urezki@gmail.com>
To: Nickolai Zeldovich <nickolai@csail.mit.edu>
Cc: Andrew Morton <akpm@linux-foundation.org>,
	Uladzislau Rezki <urezki@gmail.com>,
	Baoquan He <baoquan.he@linux.dev>,
	Lorenzo Stoakes <ljs@kernel.org>,
	linux-mm@kvack.org, linux-kernel@vger.kernel.org
Subject: Re: [PATCH v2] mm/vmalloc: read vm_map_ram block regions from the requested offset
Date: Sun, 18 Oct 2026 11:10:03 +0200	[thread overview]
Message-ID: <atSM69faDNgmLNVJ@milan> (raw)
In-Reply-To: <20261017231325.796986-1-nickolai@csail.mit.edu>

On Sat, Oct 17, 2026 at 07:13:25PM -0400, Nickolai Zeldovich wrote:
> Reading a vm_map_ram() mapping through /proc/kcore returns the wrong
> memory for every page except the first page of the first used region in
> its vmap block.  read_kcore_iter() calls vread_iter() one page at a
> time, so a root user reading a live system with drgn, crash or gdb gets
> that first page in place of every later page of the mapping, and in
> place of the zeroes of the unused pages after it.  This affects every
> vm_map_ram() mapping that fits in a vmap block (up to BITS_PER_LONG
> pages): erofs and f2fs decompression buffers, videobuf2 vmalloc/dma-sg
> buffers, udmabuf vmaps, fuse readdir pages.  Kernels since v6.3 are
> affected.
> 
> vmap_ram_vread_iter() walks the used regions of the block and, for each
> region, zero-fills up to its start and then copies from
> start + offset_in_page(addr).  A read starting in a later page of a
> region therefore copies the region's first page, and a read starting
> past the end of a region still copies from that region.
> 
> Skip regions that end at or before addr, and copy from addr itself,
> i.e. use addr - start as the offset into the region.  The region is
> re - rs pages long, not re - rs + 1: for_each_set_bitrange() returns
> [rs, re) with re the first clear bit.  The extra page is one whose
> used_map bit is clear, and vb_free() clears the bits before it unmaps
> the pages, so a read racing with vm_unmap_ram() copied the page being
> freed instead of zero-filling it.
> 
> Tested on x86_64 by queueing a 29-page V4L2_MEMORY_USERPTR buffer on a
> vivid capture device, which videobuf2-vmalloc maps with vm_map_ram(),
> page i starting with "userptr page i", and reading every page of its
> vmap block through /proc/kcore.  Before, all 1024 pages of the block
> read as page 0.  After, pages 0..28 read back in order and the rest of
> the block carries no tag.  With a second buffer in the same block and a
> 100-page buffer outside the block path, reads at unaligned offsets,
> across page and region boundaries, into and within the unused part of
> the block, and of the whole block return the true contents.  With an
> msleep() inserted between the bitmap_clear() and the
> vunmap_range_noflush() of vb_free(), a read of a one-page buffer being
> unmapped between two mapped ones returns zeroes.
> 
> Fixes: 06c8994626d1 ("mm/vmalloc.c: allow vread() to read out vm_map_ram areas")
> Assisted-by: LLM
> Signed-off-by: Nickolai Zeldovich <nickolai@csail.mit.edu>
> ---
> 
> Notes:
>     Changes in v2:
>       - region length is (re - rs) pages, not (re - rs + 1); suggested by
>         Andrew Morton.  The extra page was shown with an msleep() added to
>         vb_free() between its bitmap_clear() and its vunmap_range_noflush():
>         a one-page buffer being unmapped between two mapped ones then read
>         back its own bytes through /proc/kcore with v1 and zeroes with v2.
>     
>     Reproducer (CONFIG_PROC_KCORE=y, CONFIG_VIDEO_VIVID=y or =m, root):
>     
>       /* short.c: argv[1] = vivid capture node */
>       #include <elf.h>
>       #include <fcntl.h>
>       #include <linux/videodev2.h>
>       #include <stdio.h>
>       #include <string.h>
>       #include <sys/ioctl.h>
>       #include <sys/mman.h>
>       #include <unistd.h>
>     
>       int main(int argc, char **argv)
>       {
>       	struct v4l2_format fmt = { .type = V4L2_BUF_TYPE_VIDEO_CAPTURE };
>       	struct v4l2_requestbuffers req = { .count = 1,
>       		.type = V4L2_BUF_TYPE_VIDEO_CAPTURE, .memory = V4L2_MEMORY_USERPTR };
>       	struct v4l2_buffer b = { .type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
>       		.memory = V4L2_MEMORY_USERPTR };
>       	static Elf64_Phdr ph[64];
>       	static char buf[4096], line[256];
>       	unsigned long lo, hi, a;
>       	int fd = open(argc > 1 ? argv[1] : "/dev/video0", O_RDWR);
>       	int kc = open("/proc/kcore", O_RDONLY), i, n, np, c;
>       	Elf64_Ehdr eh;
>       	FILE *f;
>       	char *p;
>     
>       	ioctl(fd, VIDIOC_G_FMT, &fmt);
>       	fmt.fmt.pix.width = 320;
>       	fmt.fmt.pix.height = 180;
>       	ioctl(fd, VIDIOC_S_FMT, &fmt);
>       	np = (fmt.fmt.pix.sizeimage + 4095) / 4096;	/* 29 for YUYV */
>       	p = mmap(NULL, np * 4096, PROT_READ | PROT_WRITE,
>       		 MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
>       	for (i = 0; i < np; i++)
>       		sprintf(p + i * 4096, "userptr page %d", i);
>       	b.m.userptr = (unsigned long)p;
>       	b.length = np * 4096;
>       	if (ioctl(fd, VIDIOC_REQBUFS, &req) || ioctl(fd, VIDIOC_QBUF, &b))
>       		return perror("QBUF"), 1;	/* buffer now vm_map_ram()ed */
>     
>       	f = fopen("/proc/sys/kernel/kptr_restrict", "w");	/* raw %pK */
>       	fputs("1", f);
>       	fclose(f);
>       	if (pread(kc, &eh, sizeof(eh), 0) != sizeof(eh) || eh.e_phnum > 64 ||
>       	    pread(kc, ph, eh.e_phnum * sizeof(*ph), eh.e_phoff) < 0)
>       		return perror("/proc/kcore"), 1;
>       	f = fopen("/proc/vmallocinfo", "r");
>       	while (fgets(line, sizeof(line), f)) {
>       		if (!strstr(line, "vm_map_ram") ||
>       		    sscanf(line, "0x%lx-0x%lx", &lo, &hi) != 2)
>       			continue;
>       		printf("%lx:", lo);
>       		for (c = 0, a = lo; a < hi; a += 4096)
>       			for (i = 0; i < eh.e_phnum; i++)
>       				if (ph[i].p_type == PT_LOAD && a >= ph[i].p_vaddr &&
>       				    a < ph[i].p_vaddr + ph[i].p_memsz &&
>       				    pread(kc, buf, 4096, ph[i].p_offset +
>       					  a - ph[i].p_vaddr) == 4096 &&
>       				    sscanf(buf, "userptr page %d", &n) == 1 &&
>       				    c++ < 32)
>       					printf(" %d", n);
>       		printf("%s (%d pages carry a tag)\n", c > 32 ? " ..." : "", c);
>       	}
>       	return 0;
>       }
>     
>       $ gcc -static -o short short.c
>       # modprobe vivid; ./short /dev/videoN     # N = vivid capture node
>     
>     Before:
>       ff32776280800000: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... (1024 pages carry a tag)
>     After:
>       ff636d6c00800000: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 (29 pages carry a tag)
> 
>  mm/vmalloc.c | 10 +++++++---
>  1 file changed, 7 insertions(+), 3 deletions(-)
> 
> diff --git a/mm/vmalloc.c b/mm/vmalloc.c
> index bea9f76ed7e7..643299c9ca15 100644
> --- a/mm/vmalloc.c
> +++ b/mm/vmalloc.c
> @@ -4588,7 +4588,7 @@ static size_t vmap_ram_vread_iter(struct iov_iter *iter, const char *addr,
>  	struct xarray *xa;
>  	unsigned long offset;
>  	unsigned int rs, re;
> -	size_t remains, n;
> +	size_t remains, n, size;
>  
>  	/*
>  	 * If it's area created by vm_map_ram() interface directly, but
> @@ -4622,6 +4622,10 @@ static size_t vmap_ram_vread_iter(struct iov_iter *iter, const char *addr,
>  			goto finished;
>  
>  		start = vmap_block_vaddr(vb->va->va_start, rs);
> +		size = (re - rs) << PAGE_SHIFT;
> +
> +		if (addr >= start + size)
> +			continue;
>  
>  		if (addr < start) {
>  			size_t to_zero = min_t(size_t, start - addr, remains);
> @@ -4635,8 +4639,8 @@ static size_t vmap_ram_vread_iter(struct iov_iter *iter, const char *addr,
>  		}
>  
>  		/*it could start reading from the middle of used region*/
> -		offset = offset_in_page(addr);
> -		n = ((re - rs + 1) << PAGE_SHIFT) - offset;
> +		offset = addr - start;
> +		n = size - offset;
>  		if (n > remains)
>  			n = remains;
>  
> 
> base-commit: 70dd4ee72eb2d5793c62e71c14e3aba78e5a4e56
> -- 
> 2.53.0
> 
IMO, it should be improved by dropping more and make the code more
cleaner. For example offset. Basically what we need here is to
have a payload start address and its end.

The addr is within that range(skip if addr >= end), so we have
a clear picture what we need to read.

how much to read is: end - addr, read starts from addr.

--
Uladzislau Rezki


  reply	other threads:[~2026-10-18  9:10 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-17 15:41 [PATCH v1] mm/vmalloc: read vm_map_ram block regions from the requested offset Nickolai Zeldovich
2026-10-17 19:52 ` Andrew Morton
2026-10-17 23:13   ` [PATCH v2] " Nickolai Zeldovich
2026-10-18  9:10     ` Uladzislau Rezki [this message]
2026-10-17 23:14   ` [PATCH v1] " Nickolai Zeldovich
2026-10-18 15:41 ` sashiko-bot

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=atSM69faDNgmLNVJ@milan \
    --to=urezki@gmail.com \
    --cc=akpm@linux-foundation.org \
    --cc=baoquan.he@linux.dev \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=ljs@kernel.org \
    --cc=nickolai@csail.mit.edu \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox