Re: mm/mremap: F_SEAL_FUTURE_WRITE can be bypassed via mremap() VMA expansion/duplication

From: Junyoung Jang

Date: Sat Nov 07 2026 - 20:28:14 EST


Hi,

Thank you for the detailed explanation!

You are not missing anything—as I briefly noted in my initial report,
ordinary expansion with old_len=page indeed gives the exact same result. I
only highlighted old_len=0 in my previous reply because the mremap(2)
wording ("creating a new mapping") overlapped with the F_ADD_SEALS(2const)
wording ("create a new writable mapping").

Your explanation makes the design philosophy completely clear:

1. F_SEAL_FUTURE_WRITE is meant to block new writable file mappings rather
than restrict page/folio ranges within an already-mapped file. Since the
process already holds a pre-seal writable VMA, modifying or expanding
that existing VMA via mremap() is expected to work.
2. In contrast, fresh mmap() or pwrite() calls only take an fd, so allowing
them for a process that already holds a writable mapping would require
scanning existing VMAs—which is unnecessary overhead and complexity, so
they are simply rejected outright.

That clears up my confusion completely. Thank you very much for taking the
time to clarify this!

Best regards,
Junyoung Jang <graypanda.inzag@xxxxxxxxx>

On Sun, Nov 8, 2026 at 3:16 AM David Hildenbrand (Arm) <david@xxxxxxxxxx> wrote:
>
> On 11/7/26 16:19, Junyoung Jang wrote:
> > On 11/7/26 04:07, ... wrote:
> >> 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?
> >
> > Hi,
>
> Hi :)
>
> >
> > Thank you for the quick feedback.
> >
> > My understanding came from the following documentation and commit history:
> >
> > 1. Linux man-pages: F_ADD_SEALS(2const) / fcntl(2) (under "File Sealing" ->
> > "F_SEAL_FUTURE_WRITE"):
> >
> > "The effect of this seal is similar to F_SEAL_WRITE, but the contents
> > of the file can still be modified via shared writable mappings that
> > were created prior to the seal being set. Any attempt to create a new
> > writable mapping on the file via mmap(2) will fail with EPERM.
> > Similarly, an attempt to write to the file via write(2) will fail
> > with EPERM."
>
> Note that "mapping" here means "file mapping" not "folio/page mapping".
>
> >
> > (Reference: https://man7.org/linux/man-pages/man2/F_ADD_SEALS.2const.html)
> >
> > 2. The original commit introducing F_SEAL_FUTURE_WRITE, commit ab3948f58ff8
> > ("mm/memfd: add an F_SEAL_FUTURE_WRITE seal to memfd"), which states
> > that the seal:
> >
> > "...prevents any future mmap and write syscalls from succeeding while
> > keeping the existing mmap active."
>
> Right. The thing is that somebody that didn't have write access to the file
> before (through an existing mmap()) won't be able to gain write access (through
> a new mmap/write) later.
>
> Expanding mmap() or mremap() modify an *existing* mapping, they don't create a
> new one. I think we don't guarantee that expanding mmap works, but mremap() must
> work.
>
> >
> > (Reference: https://git.kernel.org/linus/ab3948f58ff8)
> >
> > 3. Linux man-pages: mremap(2) (man2/mremap.2, regarding old_size == 0)
> > and the comment in mm/mremap.c:check_prep_vma():
> >
> > mremap(2) documents that when old_size is 0 on a shareable mapping,
> > it behaves by "creating a new mapping". Similarly, check_prep_vma()
> > notes that "!old_len is a special case where an attempt is made to
> > 'duplicate' the existing mapping".
>
> Right, the relevant part here is:
>
> old = mmap(NULL, page, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
>
> and later (after sealing):
>
> expanded = mremap(old, 0, 2 * page, MREMAP_MAYMOVE);
>
> With your reproducer:
>
> $ sudo ./memfd_future_write_mremap_poc
> writer uid=10001; mmap=EPERM; pwrite=EPERM; mremap write=Z; result=63
> owner uid=10002; file page 2=Z; size=8192; result=255
>
> Assuming I specify instead
>
> expanded = mremap(old, page, 2 * page, MREMAP_MAYMOVE);$
>
> $ sudo ./memfd_future_write_mremap_poc
> writer uid=10001; mmap=EPERM; pwrite=EPERM; mremap write=Z; result=63
> owner uid=10002; file page 2=Z; size=8192; result=255
>
> So same result. What am I missing?
>
> >
> > (Reference: https://man7.org/linux/man-pages/man2/mremap.2.html)
> > > Combining these references, I interpreted the intended invariant to be that
> > post-seal writes are restricted to the pre-existing mapping itself, rather
> > than granting the process blanket write access to the entire memfd.
>
> Again, mapping talks about "file mapping", not "page/folio page-table mapping".
>
> >
> > In particular, two behaviors seemed inconsistent with the idea that the
> > process simply has whole-file write access:
> >
> > - Even when holding a pre-seal writable mapping on page 1, the same process
> > cannot call pwrite(2) or a fresh mmap(2) with PROT_WRITE on page 2 (both
> > fail with -EPERM).
>
> Right, but the main challenge here is rather figuring out whether the process
> already had access. We'd have to scan existing VMAs which is just unwarranted
> complexity for something nobody really needs ;)
>
> > - Calling mremap(2) with old_len=0 creates a brand new VMA covering page 2,
> > which appears to bypass the restriction against creating a new writable
> > mapping after the seal.
> >
> > However, if the intended design in mm is strictly an mmap()-time check—where
> > any pre-existing VM_MAYWRITE VMA implicitly retains the right to be
> > duplicated or expanded over unmapped file ranges via mremap(2)—then I
> > understand why this is considered expected behavior. If that is the case,
> > perhaps clarifying this in the F_ADD_SEALS(2const) man page would be helpful.
>
> Correct me if I'm wrong, the only confusion is about the mremap() doc saying
> "old_len=0 creates a brand new VMA covering page 2".
>
> And you seem to assume that "new VMA" implies "must be rejected", whereby we
> already have a writable VMA there and there is not really a reason to reject it.
>
> --
> Cheers,
>
> David