Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v6 0/8] seccomp: non-cooperative pinned-memfd argument redirect
@ 2026-07-15 21:19 Cong Wang
  2026-07-15 21:20 ` [PATCH v6 1/8] mm: pass the target mm parameter through get_unmapped_area family Cong Wang
                   ` (7 more replies)
  0 siblings, 8 replies; 9+ messages in thread
From: Cong Wang @ 2026-07-15 21:19 UTC (permalink / raw)
  To: Andy Lutomirski
  Cc: Kees Cook, linux-kernel, Will Drewry, Christian Brauner,
	Andrew Morton, linux-mm, Cong Wang, Cong Wang

The seccomp user-notification SECCOMP_USER_NOTIF_FLAG_CONTINUE response
carries an inherent TOCTOU: once the supervisor decides to let a syscall
continue, the target (or a CLONE_VM peer) can rewrite the memory behind a
pointer argument before the kernel reads it. This is documented in the
UAPI header and is why the notifier "cannot be used to implement a
security policy" today.

The cooperative way around this is for the target to map a shared memfd
and mseal() it during a trusted setup window, so the supervisor can hand
the kernel an immutable buffer. That window does not exist for the common
fork()+execve() sandbox model, where the supervisor wants to confine an
uncooperative (or legacy) binary it did not write.

This series lets the supervisor close the TOCTOU without any target-side
cooperation:

  - The kernel installs a sealed, read-only, MAP_SHARED mapping of a
    supervisor-owned memfd directly into the trapped task's mm
    (SECCOMP_IOCTL_NOTIF_PIN_INSTALL). The mapping is VM_SEALED at
    creation, so neither the target nor a CLONE_VM peer can unmap,
    remap, mprotect or MAP_FIXED-stomp it. The backing memfd must be
    write-sealed (F_SEAL_WRITE / F_SEAL_FUTURE_WRITE), so its bytes
    cannot be rewritten through any other reference either; the
    supervisor stages the argument data through its own pre-seal mapping.

  - The supervisor then resumes the syscall with selected argument
    registers rewritten (SECCOMP_IOCTL_NOTIF_SEND_REDIRECT), the pointer
    ones aimed into a pin. Each pointer substitution is validated so the
    whole access [ptr, ptr+len) lies inside a sealed, read-only pin of
    the supervisor's memfd that still lives in the target's current mm;
    original registers are restored at syscall exit for ABI compliance.

Because the data the kernel acts on lives in an immutable pin, the
target can no longer win the race. execve() is handled as a first-class
case: its pathname is copied from the pin before the old mm is torn
down, and the register-restore is skipped once the program image has
been replaced (detected via self_exec_id).

A redirected syscall is re-validated against the outer filters in the
target's filter chain, so an inner notifier cannot use a redirect to
smuggle a syscall past a policy an outer filter enforces (e.g. redirect
to a blocked unshare()); see patch 6.

sandlock [1], a non-cooperative seccomp sandbox supervisor, will use
this to enforce argument-level policy on uncooperative targets.

[1] https://github.com/multikernel/sandlock

Patch 1 passes the target mm through the get_unmapped_area family, so a
placement can be computed in another task's address space. Patch 2 adds
__do_mmap(), a variant of do_mmap() that targets a caller-supplied mm
(do_mmap() stays a current->mm wrapper, so no existing caller changes),
and vm_mmap_remote()/vm_munmap_remote(), high-level helpers for
installing and removing the sealed pin. Patch 3 adds PIN_INSTALL, patch
4 adds the __NR_seccomp_* syscall aliases, patch 5 adds SEND_REDIRECT,
patch 6 adds the outer-filter re-validation, patch 7 documents the ABI,
and patch 8 adds selftests.

Changes since v5:
  - New patch 1: pass the target mm through the get_unmapped_area
    family, split out of the old mm patch.
  - The __NR_seccomp_* aliases (patch 4) are now gated behind a
    SECCOMP_ARCH_REDIRECT opt-in
  - Reworked the re-validation (patch 6) into a self-contained walk,
    seccomp_redirect_revalidate(), instead of re-entering
    __seccomp_filter() with ugly goto's.
  - Selftests: two new tests. redirect_outer_trace: an attached tracer
    must not receive a PTRACE_EVENT_SECCOMP for the substituted call
    and the target sees ENOSYS. redirect_outer_notify: an outer
    listener-less USER_NOTIF verdict still blocks the substituted call
    with ENOSYS.
  - Fixed other reasonable issues reported by sashiko
  - Fixed document warnings reported by kernel test robot

Changes since v4:
  - Fixed READ_IMPLIES_EXEC and VM warning in patch 1
  - Fixed TRACE re-validation case in patch 5
  - New patch 3: split the __NR_seccomp_* syscall aliases (arch-
    overridable, covering rt_sigreturn and the clone/fork family) into
    their own patch; x86 maps the compat (ia32) numbers.
  - SEND_REDIRECT now also refuses the clone/fork family (clone, clone3,
    fork, vfork) with -EOPNOTSUPP, not just rt_sigreturn: rewriting a
    task-creating syscall's registers has no use case and is unsafe.
  - Renamed vm_mmap_seal_remote() to vm_mmap_remote() and added the
    vm_munmap_remote() counterpart (patch 1).
  - vm_mmap_remote() now bounds the mapping within the target mm's own
    address space with an overflow-safe task_size check, including the
    caller-supplied fixed-address path.
  - Compat: for SECCOMP_ARCH_COMPAT targets, redirected pointer arguments
    are truncated to 32 bits before the pin range is validated.
  - Selftests: two new tests -- redirect_denied_syscalls (rt_sigreturn
    and the clone/fork family are all rejected with -EOPNOTSUPP) and
    redirect_revalidate_chain (the re-validation walks the entire outer
    filter stack, not just the nearest filter); plus error-path hardening
    so a broken kernel fails or skips rather than hanging.

Changes since v3:
  - Split the single seccomp patch into PIN_INSTALL (patch 2) and
    SEND_REDIRECT (patch 4) for reviewability.
  - New patch 5: re-validate a redirected syscall against the outer
    filters in the stack, closing the bypass Andy described (an inner
    notifier redirecting to a syscall an outer filter blocks, e.g.
    unshare()).
  - Signals: the argument restore now runs before signal/restart
    processing. It is queued as task_work with TWA_RESUME -- not the
    TWA_SIGNAL discussed on-list, which makes signal_pending() true for
    the whole redirected syscall and livelocks an interruptible one.
    TWA_RESUME still runs the restore at the top of get_signal(), before
    the signal frame is built and before any -ERESTART* rewind; on a
    restart the syscall re-traps seccomp and the supervisor is notified
    again. rt_sigreturn is refused (-EOPNOTSUPP).
  - At most one redirect-capable notifier may exist in a filter chain
    (-EBUSY); ordinary notifiers are unconstrained. Syscalls with
    complex signal/restart behaviour (nanosleep, futex(FUTEX_WAIT), ...)
    are out of scope and should not have their arguments redirected.
  - PIN_INSTALL: target_addr == 0 lets the kernel pick a free address in
    the target mm (avoids a racy userspace /proc/<pid>/maps scan), and a
    new offset field lets one memfd back several disjoint pins.
  - New patch 6: Documentation/userspace-api/seccomp_filter.rst.
  - Selftests expanded: install into a fresh post-execve mm, stateless
    churn, outer-filter re-validation, ABI/versioning, and a
    signal-ordering regression test.

Changes since v2:
  v3 was a redesign that dropped the v2 SECCOMP_IOCTL_NOTIF_INJECT
  approach (an in-kernel reimplementation of a syscall whitelist) in
  favour of redirecting the real syscall into a sealed pin, as suggested
  by Andy.

Changes since v1:
  v2 was a redesign that dropped the v1 SECCOMP_IOCTL_NOTIF_PIN_ARGS
  approach.

All pinned-memfd and redirect selftests pass (seccomp_bpf: 121/121).

Cong Wang (8):
  mm: pass the target mm parameter through get_unmapped_area family
  mm: add __do_mmap() and vm_mmap_remote()/vm_munmap_remote()
  seccomp: introduce SECCOMP_IOCTL_NOTIF_PIN_INSTALL
  seccomp: add __NR_seccomp_* aliases for rt_sigreturn and clone/fork
  seccomp: add kernel-installed pinned-memfd redirect
  seccomp: re-validate a redirected syscall against outer filters
  docs/seccomp: document pinned-memfd redirect ioctls
  selftests/seccomp: cover non-cooperative pinned-memfd install

 Documentation/filesystems/locking.rst         |    2 +-
 Documentation/filesystems/vfs.rst             |    2 +-
 .../userspace-api/seccomp_filter.rst          |  109 ++
 arch/alpha/kernel/osf_sys.c                   |   21 +-
 arch/arc/mm/mmap.c                            |    7 +-
 arch/arm/mm/mmap.c                            |   19 +-
 arch/csky/abiv1/mmap.c                        |    7 +-
 arch/loongarch/mm/mmap.c                      |   28 +-
 arch/mips/mm/mmap.c                           |   28 +-
 arch/parisc/kernel/sys_parisc.c               |   28 +-
 arch/powerpc/include/asm/book3s/64/slice.h    |    3 +-
 arch/powerpc/mm/book3s64/slice.c              |   28 +-
 arch/s390/mm/mmap.c                           |   25 +-
 arch/sh/mm/mmap.c                             |   21 +-
 arch/sparc/include/asm/pgtable_64.h           |    6 +-
 arch/sparc/kernel/sys_sparc_32.c              |    7 +-
 arch/sparc/kernel/sys_sparc_64.c              |   32 +-
 arch/x86/include/asm/elf.h                    |    2 +-
 arch/x86/include/asm/seccomp.h                |   15 +-
 arch/x86/kernel/cpu/sgx/driver.c              |   13 +-
 arch/x86/kernel/sys_x86_64.c                  |   32 +-
 arch/x86/kernel/uprobes.c                     |    4 +-
 arch/x86/mm/mmap.c                            |    4 +-
 arch/xtensa/kernel/syscall.c                  |    8 +-
 drivers/char/mem.c                            |   15 +-
 drivers/dax/device.c                          |   11 +-
 drivers/gpu/drm/drm_gem.c                     |    9 +-
 drivers/gpu/drm/drm_gem_dma_helper.c          |    4 +-
 drivers/media/v4l2-core/v4l2-dev.c            |    3 +-
 drivers/mtd/mtdchar.c                         |    3 +-
 drivers/video/fbdev/core/fb_chrdev.c          |    3 +-
 fs/cramfs/inode.c                             |    3 +-
 fs/hugetlbfs/inode.c                          |    9 +-
 fs/proc/inode.c                               |   15 +-
 fs/ramfs/file-mmu.c                           |    5 +-
 fs/ramfs/file-nommu.c                         |    6 +-
 fs/romfs/mmap-nommu.c                         |    3 +-
 include/asm-generic/seccomp.h                 |   30 +
 include/drm/drm_gem.h                         |    3 +-
 include/drm/drm_gem_dma_helper.h              |    3 +-
 include/linux/bpf.h                           |    3 +-
 include/linux/fs.h                            |    5 +-
 include/linux/huge_mm.h                       |   18 +-
 include/linux/hugetlb.h                       |    6 +-
 include/linux/mm.h                            |   31 +-
 include/linux/proc_fs.h                       |    5 +-
 include/linux/sched/mm.h                      |   37 +-
 include/linux/seccomp.h                       |   12 +-
 include/linux/shmem_fs.h                      |    5 +-
 include/uapi/linux/seccomp.h                  |   87 +
 io_uring/memmap.c                             |    8 +-
 io_uring/memmap.h                             |    3 +-
 ipc/shm.c                                     |    8 +-
 kernel/bpf/arena.c                            |    5 +-
 kernel/bpf/syscall.c                          |    8 +-
 kernel/seccomp.c                              |  571 +++++-
 mm/huge_memory.c                              |   27 +-
 mm/internal.h                                 |    6 +
 mm/mmap.c                                     |  132 +-
 mm/mprotect.c                                 |    2 +-
 mm/nommu.c                                    |   27 +-
 mm/shmem.c                                    |   13 +-
 mm/util.c                                     |  106 ++
 mm/vma.c                                      |   62 +-
 mm/vma.h                                      |   18 +-
 sound/core/pcm_native.c                       |    3 +-
 tools/testing/selftests/seccomp/seccomp_bpf.c | 1615 +++++++++++++++++
 67 files changed, 3025 insertions(+), 374 deletions(-)


base-commit: 58717b2a1365d06c8c64b72aa948541b53fe31eb
-- 
2.43.0



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

* [PATCH v6 1/8] mm: pass the target mm parameter through get_unmapped_area family
  2026-07-15 21:19 [PATCH v6 0/8] seccomp: non-cooperative pinned-memfd argument redirect Cong Wang
@ 2026-07-15 21:20 ` Cong Wang
  2026-07-15 21:20 ` [PATCH v6 2/8] mm: add __do_mmap() and vm_mmap_remote()/vm_munmap_remote() Cong Wang
                   ` (6 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: Cong Wang @ 2026-07-15 21:20 UTC (permalink / raw)
  To: Andy Lutomirski
  Cc: Kees Cook, linux-kernel, Will Drewry, Christian Brauner,
	Andrew Morton, linux-mm, Cong Wang

From: Cong Wang <cwang@multikernel.io>

Every placement callback in the mmap path implicitly resolves against
current->mm, so a placement cannot be computed in another task's address
space. vm_mmap_remote() in the next patch needs this: without it a remote
install cannot run the file's own ->get_unmapped_area() and file-specific
placement rules are bypassed or rejected at MAP_FIXED validation.

Make the mm an explicit first parameter of the get_unmapped_area family
across the entire tree. It is difficult and not elegant to avoid such a
big change.

The get_unmapped_area() convenience inline keeps its old signature and
passes current->mm, so ordinary callers do not change. So no functional
change.

Assisted-by: Claude:claude-opus-4.8
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
 Documentation/filesystems/locking.rst      |  2 +-
 Documentation/filesystems/vfs.rst          |  2 +-
 arch/alpha/kernel/osf_sys.c                | 21 +++---
 arch/arc/mm/mmap.c                         |  7 +-
 arch/arm/mm/mmap.c                         | 19 +++--
 arch/csky/abiv1/mmap.c                     |  7 +-
 arch/loongarch/mm/mmap.c                   | 28 ++++----
 arch/mips/mm/mmap.c                        | 28 ++++----
 arch/parisc/kernel/sys_parisc.c            | 28 ++++----
 arch/powerpc/include/asm/book3s/64/slice.h |  3 +-
 arch/powerpc/mm/book3s64/slice.c           | 28 ++++----
 arch/s390/mm/mmap.c                        | 25 ++++---
 arch/sh/mm/mmap.c                          | 21 +++---
 arch/sparc/include/asm/pgtable_64.h        |  6 +-
 arch/sparc/kernel/sys_sparc_32.c           |  7 +-
 arch/sparc/kernel/sys_sparc_64.c           | 32 +++++----
 arch/x86/include/asm/elf.h                 |  2 +-
 arch/x86/kernel/cpu/sgx/driver.c           | 13 ++--
 arch/x86/kernel/sys_x86_64.c               | 32 +++++----
 arch/x86/kernel/uprobes.c                  |  4 +-
 arch/x86/mm/mmap.c                         |  4 +-
 arch/xtensa/kernel/syscall.c               |  8 +--
 drivers/char/mem.c                         | 15 ++--
 drivers/dax/device.c                       | 11 +--
 drivers/gpu/drm/drm_gem.c                  |  9 ++-
 drivers/gpu/drm/drm_gem_dma_helper.c       |  4 +-
 drivers/media/v4l2-core/v4l2-dev.c         |  3 +-
 drivers/mtd/mtdchar.c                      |  3 +-
 drivers/video/fbdev/core/fb_chrdev.c       |  3 +-
 fs/cramfs/inode.c                          |  3 +-
 fs/hugetlbfs/inode.c                       |  9 +--
 fs/proc/inode.c                            | 15 ++--
 fs/ramfs/file-mmu.c                        |  5 +-
 fs/ramfs/file-nommu.c                      |  6 +-
 fs/romfs/mmap-nommu.c                      |  3 +-
 include/drm/drm_gem.h                      |  3 +-
 include/drm/drm_gem_dma_helper.h           |  3 +-
 include/linux/bpf.h                        |  3 +-
 include/linux/fs.h                         |  5 +-
 include/linux/huge_mm.h                    | 18 ++---
 include/linux/hugetlb.h                    |  6 +-
 include/linux/mm.h                         | 12 ++--
 include/linux/proc_fs.h                    |  5 +-
 include/linux/sched/mm.h                   | 37 +++++-----
 include/linux/shmem_fs.h                   |  5 +-
 io_uring/memmap.c                          |  8 ++-
 io_uring/memmap.h                          |  3 +-
 ipc/shm.c                                  |  8 +--
 kernel/bpf/arena.c                         |  5 +-
 kernel/bpf/syscall.c                       |  8 ++-
 mm/huge_memory.c                           | 27 +++----
 mm/mmap.c                                  | 83 ++++++++++++----------
 mm/nommu.c                                 |  5 +-
 mm/shmem.c                                 | 13 ++--
 mm/vma.c                                   | 16 +++--
 mm/vma.h                                   |  6 +-
 sound/core/pcm_native.c                    |  3 +-
 57 files changed, 391 insertions(+), 307 deletions(-)

diff --git a/Documentation/filesystems/locking.rst b/Documentation/filesystems/locking.rst
index 08d01bc62c31..b5defd74e302 100644
--- a/Documentation/filesystems/locking.rst
+++ b/Documentation/filesystems/locking.rst
@@ -471,7 +471,7 @@ prototypes::
 	int (*fsync) (struct file *, loff_t start, loff_t end, int datasync);
 	int (*fasync) (int, struct file *, int);
 	int (*lock) (struct file *, int, struct file_lock *);
-	unsigned long (*get_unmapped_area)(struct file *, unsigned long,
+	unsigned long (*get_unmapped_area)(struct mm_struct *, struct file *, unsigned long,
 			unsigned long, unsigned long, unsigned long);
 	int (*check_flags)(int);
 	int (*flock) (struct file *, int, struct file_lock *);
diff --git a/Documentation/filesystems/vfs.rst b/Documentation/filesystems/vfs.rst
index 7c753148af88..4f95a272eab8 100644
--- a/Documentation/filesystems/vfs.rst
+++ b/Documentation/filesystems/vfs.rst
@@ -1023,7 +1023,7 @@ This describes how the VFS can manipulate an open file.  As of kernel
 		int (*fsync) (struct file *, loff_t, loff_t, int datasync);
 		int (*fasync) (int, struct file *, int);
 		int (*lock) (struct file *, int, struct file_lock *);
-		unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
+		unsigned long (*get_unmapped_area)(struct mm_struct *, struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
 		int (*check_flags)(int);
 		int (*flock) (struct file *, int, struct file_lock *);
 		ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
diff --git a/arch/alpha/kernel/osf_sys.c b/arch/alpha/kernel/osf_sys.c
index 7b6543d2cca3..80ae6d03e266 100644
--- a/arch/alpha/kernel/osf_sys.c
+++ b/arch/alpha/kernel/osf_sys.c
@@ -1201,21 +1201,22 @@ SYSCALL_DEFINE1(old_adjtimex, struct timex32 __user *, txc_p)
 /* Get an address range which is currently unmapped. */
 
 static unsigned long
-arch_get_unmapped_area_1(unsigned long addr, unsigned long len,
-		         unsigned long limit)
+arch_get_unmapped_area_1(struct mm_struct *mm, unsigned long addr,
+			 unsigned long len, unsigned long limit)
 {
 	struct vm_unmapped_area_info info = {};
 
 	info.length = len;
 	info.low_limit = addr;
 	info.high_limit = limit;
-	return vm_unmapped_area(&info);
+	return vm_unmapped_area(mm, &info);
 }
 
 unsigned long
-arch_get_unmapped_area(struct file *filp, unsigned long addr,
-		       unsigned long len, unsigned long pgoff,
-		       unsigned long flags, vm_flags_t vm_flags)
+arch_get_unmapped_area(struct mm_struct *mm, struct file *filp,
+		       unsigned long addr, unsigned long len,
+		       unsigned long pgoff, unsigned long flags,
+		       vm_flags_t vm_flags)
 {
 	unsigned long limit = TASK_SIZE;
 
@@ -1236,19 +1237,19 @@ arch_get_unmapped_area(struct file *filp, unsigned long addr,
 	   this feature should be incorporated into all ports?  */
 
 	if (addr) {
-		addr = arch_get_unmapped_area_1 (PAGE_ALIGN(addr), len, limit);
+		addr = arch_get_unmapped_area_1(mm, PAGE_ALIGN(addr), len, limit);
 		if (addr != (unsigned long) -ENOMEM)
 			return addr;
 	}
 
 	/* Next, try allocating at TASK_UNMAPPED_BASE.  */
-	addr = arch_get_unmapped_area_1 (PAGE_ALIGN(TASK_UNMAPPED_BASE),
-					 len, limit);
+	addr = arch_get_unmapped_area_1(mm, PAGE_ALIGN(TASK_UNMAPPED_BASE),
+					len, limit);
 	if (addr != (unsigned long) -ENOMEM)
 		return addr;
 
 	/* Finally, try allocating in low memory.  */
-	addr = arch_get_unmapped_area_1 (PAGE_SIZE, len, limit);
+	addr = arch_get_unmapped_area_1(mm, PAGE_SIZE, len, limit);
 
 	return addr;
 }
diff --git a/arch/arc/mm/mmap.c b/arch/arc/mm/mmap.c
index 2185afe8d59f..d33d5443b616 100644
--- a/arch/arc/mm/mmap.c
+++ b/arch/arc/mm/mmap.c
@@ -22,11 +22,10 @@
  * SHMLBA bytes.
  */
 unsigned long
-arch_get_unmapped_area(struct file *filp, unsigned long addr,
-		unsigned long len, unsigned long pgoff,
+arch_get_unmapped_area(struct mm_struct *mm, struct file *filp,
+		unsigned long addr, unsigned long len, unsigned long pgoff,
 		unsigned long flags, vm_flags_t vm_flags)
 {
-	struct mm_struct *mm = current->mm;
 	struct vm_area_struct *vma;
 	struct vm_unmapped_area_info info = {};
 
@@ -56,7 +55,7 @@ arch_get_unmapped_area(struct file *filp, unsigned long addr,
 	info.low_limit = mm->mmap_base;
 	info.high_limit = TASK_SIZE;
 	info.align_offset = pgoff << PAGE_SHIFT;
-	return vm_unmapped_area(&info);
+	return vm_unmapped_area(mm, &info);
 }
 
 static const pgprot_t protection_map[16] = {
diff --git a/arch/arm/mm/mmap.c b/arch/arm/mm/mmap.c
index 3dbb383c26d5..5d1169539ce3 100644
--- a/arch/arm/mm/mmap.c
+++ b/arch/arm/mm/mmap.c
@@ -27,11 +27,10 @@
  * in the VIVT case, we optimise out the alignment rules.
  */
 unsigned long
-arch_get_unmapped_area(struct file *filp, unsigned long addr,
-		unsigned long len, unsigned long pgoff,
+arch_get_unmapped_area(struct mm_struct *mm, struct file *filp,
+		unsigned long addr, unsigned long len, unsigned long pgoff,
 		unsigned long flags, vm_flags_t vm_flags)
 {
-	struct mm_struct *mm = current->mm;
 	struct vm_area_struct *vma;
 	int do_align = 0;
 	int aliasing = cache_is_vipt_aliasing();
@@ -74,16 +73,16 @@ arch_get_unmapped_area(struct file *filp, unsigned long addr,
 	info.high_limit = TASK_SIZE;
 	info.align_mask = do_align ? (PAGE_MASK & (SHMLBA - 1)) : 0;
 	info.align_offset = pgoff << PAGE_SHIFT;
-	return vm_unmapped_area(&info);
+	return vm_unmapped_area(mm, &info);
 }
 
 unsigned long
-arch_get_unmapped_area_topdown(struct file *filp, const unsigned long addr0,
-		        const unsigned long len, const unsigned long pgoff,
-		        const unsigned long flags, vm_flags_t vm_flags)
+arch_get_unmapped_area_topdown(struct mm_struct *mm, struct file *filp,
+		const unsigned long addr0, const unsigned long len,
+		const unsigned long pgoff, const unsigned long flags,
+		vm_flags_t vm_flags)
 {
 	struct vm_area_struct *vma;
-	struct mm_struct *mm = current->mm;
 	unsigned long addr = addr0;
 	int do_align = 0;
 	int aliasing = cache_is_vipt_aliasing();
@@ -125,7 +124,7 @@ arch_get_unmapped_area_topdown(struct file *filp, const unsigned long addr0,
 	info.high_limit = mm->mmap_base;
 	info.align_mask = do_align ? (PAGE_MASK & (SHMLBA - 1)) : 0;
 	info.align_offset = pgoff << PAGE_SHIFT;
-	addr = vm_unmapped_area(&info);
+	addr = vm_unmapped_area(mm, &info);
 
 	/*
 	 * A failed mmap() very likely causes application failure,
@@ -138,7 +137,7 @@ arch_get_unmapped_area_topdown(struct file *filp, const unsigned long addr0,
 		info.flags = 0;
 		info.low_limit = mm->mmap_base;
 		info.high_limit = TASK_SIZE;
-		addr = vm_unmapped_area(&info);
+		addr = vm_unmapped_area(mm, &info);
 	}
 
 	return addr;
diff --git a/arch/csky/abiv1/mmap.c b/arch/csky/abiv1/mmap.c
index 1047865e82a9..62eaca7a0d50 100644
--- a/arch/csky/abiv1/mmap.c
+++ b/arch/csky/abiv1/mmap.c
@@ -22,11 +22,10 @@
  * We unconditionally provide this function for all cases.
  */
 unsigned long
-arch_get_unmapped_area(struct file *filp, unsigned long addr,
-		unsigned long len, unsigned long pgoff,
+arch_get_unmapped_area(struct mm_struct *mm, struct file *filp,
+		unsigned long addr, unsigned long len, unsigned long pgoff,
 		unsigned long flags, vm_flags_t vm_flags)
 {
-	struct mm_struct *mm = current->mm;
 	struct vm_area_struct *vma;
 	int do_align = 0;
 	struct vm_unmapped_area_info info = {
@@ -68,5 +67,5 @@ arch_get_unmapped_area(struct file *filp, unsigned long addr,
 	}
 
 	info.align_mask = do_align ? (PAGE_MASK & (SHMLBA - 1)) : 0;
-	return vm_unmapped_area(&info);
+	return vm_unmapped_area(mm, &info);
 }
diff --git a/arch/loongarch/mm/mmap.c b/arch/loongarch/mm/mmap.c
index 1df9e99582cc..600943121362 100644
--- a/arch/loongarch/mm/mmap.c
+++ b/arch/loongarch/mm/mmap.c
@@ -18,11 +18,11 @@
 
 enum mmap_allocation_direction {UP, DOWN};
 
-static unsigned long arch_get_unmapped_area_common(struct file *filp,
-	unsigned long addr0, unsigned long len, unsigned long pgoff,
-	unsigned long flags, enum mmap_allocation_direction dir)
+static unsigned long arch_get_unmapped_area_common(struct mm_struct *mm,
+	struct file *filp, unsigned long addr0, unsigned long len,
+	unsigned long pgoff, unsigned long flags,
+	enum mmap_allocation_direction dir)
 {
-	struct mm_struct *mm = current->mm;
 	struct vm_area_struct *vma;
 	unsigned long addr = addr0;
 	int do_color_align;
@@ -74,7 +74,7 @@ static unsigned long arch_get_unmapped_area_common(struct file *filp,
 		info.flags = VM_UNMAPPED_AREA_TOPDOWN;
 		info.low_limit = PAGE_SIZE;
 		info.high_limit = mm->mmap_base;
-		addr = vm_unmapped_area(&info);
+		addr = vm_unmapped_area(mm, &info);
 
 		if (!(addr & ~PAGE_MASK))
 			return addr;
@@ -89,14 +89,14 @@ static unsigned long arch_get_unmapped_area_common(struct file *filp,
 
 	info.low_limit = mm->mmap_base;
 	info.high_limit = TASK_SIZE;
-	return vm_unmapped_area(&info);
+	return vm_unmapped_area(mm, &info);
 }
 
-unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr0,
-	unsigned long len, unsigned long pgoff, unsigned long flags,
-	vm_flags_t vm_flags)
+unsigned long arch_get_unmapped_area(struct mm_struct *mm, struct file *filp,
+	unsigned long addr0, unsigned long len, unsigned long pgoff,
+	unsigned long flags, vm_flags_t vm_flags)
 {
-	return arch_get_unmapped_area_common(filp,
+	return arch_get_unmapped_area_common(mm, filp,
 			addr0, len, pgoff, flags, UP);
 }
 
@@ -104,11 +104,11 @@ unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr0,
  * There is no need to export this but sched.h declares the function as
  * extern so making it static here results in an error.
  */
-unsigned long arch_get_unmapped_area_topdown(struct file *filp,
-	unsigned long addr0, unsigned long len, unsigned long pgoff,
-	unsigned long flags, vm_flags_t vm_flags)
+unsigned long arch_get_unmapped_area_topdown(struct mm_struct *mm,
+	struct file *filp, unsigned long addr0, unsigned long len,
+	unsigned long pgoff, unsigned long flags, vm_flags_t vm_flags)
 {
-	return arch_get_unmapped_area_common(filp,
+	return arch_get_unmapped_area_common(mm, filp,
 			addr0, len, pgoff, flags, DOWN);
 }
 
diff --git a/arch/mips/mm/mmap.c b/arch/mips/mm/mmap.c
index 5d2a1225785b..8905bba30aa0 100644
--- a/arch/mips/mm/mmap.c
+++ b/arch/mips/mm/mmap.c
@@ -26,11 +26,11 @@ EXPORT_SYMBOL(shm_align_mask);
 
 enum mmap_allocation_direction {UP, DOWN};
 
-static unsigned long arch_get_unmapped_area_common(struct file *filp,
-	unsigned long addr0, unsigned long len, unsigned long pgoff,
-	unsigned long flags, enum mmap_allocation_direction dir)
+static unsigned long arch_get_unmapped_area_common(struct mm_struct *mm,
+	struct file *filp, unsigned long addr0, unsigned long len,
+	unsigned long pgoff, unsigned long flags,
+	enum mmap_allocation_direction dir)
 {
-	struct mm_struct *mm = current->mm;
 	struct vm_area_struct *vma;
 	unsigned long addr = addr0;
 	int do_color_align;
@@ -79,7 +79,7 @@ static unsigned long arch_get_unmapped_area_common(struct file *filp,
 		info.flags = VM_UNMAPPED_AREA_TOPDOWN;
 		info.low_limit = PAGE_SIZE;
 		info.high_limit = mm->mmap_base;
-		addr = vm_unmapped_area(&info);
+		addr = vm_unmapped_area(mm, &info);
 
 		if (!(addr & ~PAGE_MASK))
 			return addr;
@@ -94,14 +94,14 @@ static unsigned long arch_get_unmapped_area_common(struct file *filp,
 
 	info.low_limit = mm->mmap_base;
 	info.high_limit = TASK_SIZE;
-	return vm_unmapped_area(&info);
+	return vm_unmapped_area(mm, &info);
 }
 
-unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr0,
-	unsigned long len, unsigned long pgoff, unsigned long flags,
-	vm_flags_t vm_flags)
+unsigned long arch_get_unmapped_area(struct mm_struct *mm, struct file *filp,
+	unsigned long addr0, unsigned long len, unsigned long pgoff,
+	unsigned long flags, vm_flags_t vm_flags)
 {
-	return arch_get_unmapped_area_common(filp,
+	return arch_get_unmapped_area_common(mm, filp,
 			addr0, len, pgoff, flags, UP);
 }
 
@@ -109,11 +109,11 @@ unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr0,
  * There is no need to export this but sched.h declares the function as
  * extern so making it static here results in an error.
  */
-unsigned long arch_get_unmapped_area_topdown(struct file *filp,
-	unsigned long addr0, unsigned long len, unsigned long pgoff,
-	unsigned long flags, vm_flags_t vm_flags)
+unsigned long arch_get_unmapped_area_topdown(struct mm_struct *mm,
+	struct file *filp, unsigned long addr0, unsigned long len,
+	unsigned long pgoff, unsigned long flags, vm_flags_t vm_flags)
 {
-	return arch_get_unmapped_area_common(filp,
+	return arch_get_unmapped_area_common(mm, filp,
 			addr0, len, pgoff, flags, DOWN);
 }
 
diff --git a/arch/parisc/kernel/sys_parisc.c b/arch/parisc/kernel/sys_parisc.c
index 1a676a9bf80c..b9bf734cf678 100644
--- a/arch/parisc/kernel/sys_parisc.c
+++ b/arch/parisc/kernel/sys_parisc.c
@@ -100,11 +100,11 @@ unsigned long mmap_upper_limit(const struct rlimit *rlim_stack)
 
 enum mmap_allocation_direction {UP, DOWN};
 
-static unsigned long arch_get_unmapped_area_common(struct file *filp,
-	unsigned long addr, unsigned long len, unsigned long pgoff,
-	unsigned long flags, enum mmap_allocation_direction dir)
+static unsigned long arch_get_unmapped_area_common(struct mm_struct *mm,
+	struct file *filp, unsigned long addr, unsigned long len,
+	unsigned long pgoff, unsigned long flags,
+	enum mmap_allocation_direction dir)
 {
-	struct mm_struct *mm = current->mm;
 	struct vm_area_struct *vma, *prev;
 	unsigned long filp_pgoff;
 	int do_color_align;
@@ -152,7 +152,7 @@ static unsigned long arch_get_unmapped_area_common(struct file *filp,
 		info.flags = VM_UNMAPPED_AREA_TOPDOWN;
 		info.low_limit = PAGE_SIZE;
 		info.high_limit = mm->mmap_base;
-		addr = vm_unmapped_area(&info);
+		addr = vm_unmapped_area(mm, &info);
 		if (!(addr & ~PAGE_MASK))
 			return addr;
 		VM_BUG_ON(addr != -ENOMEM);
@@ -167,22 +167,22 @@ static unsigned long arch_get_unmapped_area_common(struct file *filp,
 
 	info.low_limit = mm->mmap_base;
 	info.high_limit = mmap_upper_limit(NULL);
-	return vm_unmapped_area(&info);
+	return vm_unmapped_area(mm, &info);
 }
 
-unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr,
-	unsigned long len, unsigned long pgoff, unsigned long flags,
-	vm_flags_t vm_flags)
+unsigned long arch_get_unmapped_area(struct mm_struct *mm, struct file *filp,
+	unsigned long addr, unsigned long len, unsigned long pgoff,
+	unsigned long flags, vm_flags_t vm_flags)
 {
-	return arch_get_unmapped_area_common(filp,
+	return arch_get_unmapped_area_common(mm, filp,
 			addr, len, pgoff, flags, UP);
 }
 
-unsigned long arch_get_unmapped_area_topdown(struct file *filp,
-	unsigned long addr, unsigned long len, unsigned long pgoff,
-	unsigned long flags, vm_flags_t vm_flags)
+unsigned long arch_get_unmapped_area_topdown(struct mm_struct *mm,
+	struct file *filp, unsigned long addr, unsigned long len,
+	unsigned long pgoff, unsigned long flags, vm_flags_t vm_flags)
 {
-	return arch_get_unmapped_area_common(filp,
+	return arch_get_unmapped_area_common(mm, filp,
 			addr, len, pgoff, flags, DOWN);
 }
 
diff --git a/arch/powerpc/include/asm/book3s/64/slice.h b/arch/powerpc/include/asm/book3s/64/slice.h
index 6e2f7a74cd75..d95d9e292443 100644
--- a/arch/powerpc/include/asm/book3s/64/slice.h
+++ b/arch/powerpc/include/asm/book3s/64/slice.h
@@ -25,7 +25,8 @@
 
 struct mm_struct;
 
-unsigned long slice_get_unmapped_area(unsigned long addr, unsigned long len,
+unsigned long slice_get_unmapped_area(struct mm_struct *mm,
+				      unsigned long addr, unsigned long len,
 				      unsigned long flags, unsigned int psize,
 				      int topdown);
 
diff --git a/arch/powerpc/mm/book3s64/slice.c b/arch/powerpc/mm/book3s64/slice.c
index 28bec5bc7879..ab8b24f6287d 100644
--- a/arch/powerpc/mm/book3s64/slice.c
+++ b/arch/powerpc/mm/book3s64/slice.c
@@ -311,7 +311,7 @@ static unsigned long slice_find_area_bottomup(struct mm_struct *mm,
 		}
 		info.high_limit = addr;
 
-		found = vm_unmapped_area(&info);
+		found = vm_unmapped_area(mm, &info);
 		if (!(found & ~PAGE_MASK))
 			return found;
 	}
@@ -362,7 +362,7 @@ static unsigned long slice_find_area_topdown(struct mm_struct *mm,
 		}
 		info.low_limit = addr;
 
-		found = vm_unmapped_area(&info);
+		found = vm_unmapped_area(mm, &info);
 		if (!(found & ~PAGE_MASK))
 			return found;
 	}
@@ -422,7 +422,8 @@ static inline void slice_andnot_mask(struct slice_mask *dst,
 #define MMU_PAGE_BASE	MMU_PAGE_4K
 #endif
 
-unsigned long slice_get_unmapped_area(unsigned long addr, unsigned long len,
+unsigned long slice_get_unmapped_area(struct mm_struct *mm,
+				      unsigned long addr, unsigned long len,
 				      unsigned long flags, unsigned int psize,
 				      int topdown)
 {
@@ -433,7 +434,6 @@ unsigned long slice_get_unmapped_area(unsigned long addr, unsigned long len,
 	int fixed = (flags & MAP_FIXED);
 	int pshift = max_t(int, mmu_psize_defs[psize].shift, PAGE_SHIFT);
 	unsigned long page_size = 1UL << pshift;
-	struct mm_struct *mm = current->mm;
 	unsigned long newaddr;
 	unsigned long high_limit;
 
@@ -649,7 +649,8 @@ static int file_to_psize(struct file *file)
 }
 #endif
 
-unsigned long arch_get_unmapped_area(struct file *filp,
+unsigned long arch_get_unmapped_area(struct mm_struct *mm,
+				     struct file *filp,
 				     unsigned long addr,
 				     unsigned long len,
 				     unsigned long pgoff,
@@ -659,17 +660,19 @@ unsigned long arch_get_unmapped_area(struct file *filp,
 	unsigned int psize;
 
 	if (radix_enabled())
-		return generic_get_unmapped_area(filp, addr, len, pgoff, flags, vm_flags);
+		return generic_get_unmapped_area(mm, filp, addr, len, pgoff,
+						 flags, vm_flags);
 
 	if (filp && is_file_hugepages(filp))
 		psize = file_to_psize(filp);
 	else
-		psize = mm_ctx_user_psize(&current->mm->context);
+		psize = mm_ctx_user_psize(&mm->context);
 
-	return slice_get_unmapped_area(addr, len, flags, psize, 0);
+	return slice_get_unmapped_area(mm, addr, len, flags, psize, 0);
 }
 
-unsigned long arch_get_unmapped_area_topdown(struct file *filp,
+unsigned long arch_get_unmapped_area_topdown(struct mm_struct *mm,
+					     struct file *filp,
 					     const unsigned long addr0,
 					     const unsigned long len,
 					     const unsigned long pgoff,
@@ -679,14 +682,15 @@ unsigned long arch_get_unmapped_area_topdown(struct file *filp,
 	unsigned int psize;
 
 	if (radix_enabled())
-		return generic_get_unmapped_area_topdown(filp, addr0, len, pgoff, flags, vm_flags);
+		return generic_get_unmapped_area_topdown(mm, filp, addr0, len,
+							 pgoff, flags, vm_flags);
 
 	if (filp && is_file_hugepages(filp))
 		psize = file_to_psize(filp);
 	else
-		psize = mm_ctx_user_psize(&current->mm->context);
+		psize = mm_ctx_user_psize(&mm->context);
 
-	return slice_get_unmapped_area(addr0, len, flags, psize, 1);
+	return slice_get_unmapped_area(mm, addr0, len, flags, psize, 1);
 }
 
 unsigned int notrace get_slice_psize(struct mm_struct *mm, unsigned long addr)
diff --git a/arch/s390/mm/mmap.c b/arch/s390/mm/mmap.c
index ef7bfc87758c..92332e9f5229 100644
--- a/arch/s390/mm/mmap.c
+++ b/arch/s390/mm/mmap.c
@@ -75,11 +75,11 @@ static unsigned long get_align_mask(struct file *filp, unsigned long flags)
 	return 0;
 }
 
-unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr,
-				     unsigned long len, unsigned long pgoff,
-				     unsigned long flags, vm_flags_t vm_flags)
+unsigned long arch_get_unmapped_area(struct mm_struct *mm, struct file *filp,
+				     unsigned long addr, unsigned long len,
+				     unsigned long pgoff, unsigned long flags,
+				     vm_flags_t vm_flags)
 {
-	struct mm_struct *mm = current->mm;
 	struct vm_area_struct *vma;
 	struct vm_unmapped_area_info info = {};
 
@@ -103,7 +103,7 @@ unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr,
 	info.align_mask = get_align_mask(filp, flags);
 	if (!(filp && is_file_hugepages(filp)))
 		info.align_offset = pgoff << PAGE_SHIFT;
-	addr = vm_unmapped_area(&info);
+	addr = vm_unmapped_area(mm, &info);
 	if (offset_in_page(addr))
 		return addr;
 
@@ -111,12 +111,15 @@ unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr,
 	return check_asce_limit(mm, addr, len);
 }
 
-unsigned long arch_get_unmapped_area_topdown(struct file *filp, unsigned long addr,
-					     unsigned long len, unsigned long pgoff,
-					     unsigned long flags, vm_flags_t vm_flags)
+unsigned long arch_get_unmapped_area_topdown(struct mm_struct *mm,
+					     struct file *filp,
+					     unsigned long addr,
+					     unsigned long len,
+					     unsigned long pgoff,
+					     unsigned long flags,
+					     vm_flags_t vm_flags)
 {
 	struct vm_area_struct *vma;
-	struct mm_struct *mm = current->mm;
 	struct vm_unmapped_area_info info = {};
 
 	/* requested length too big for entire address space */
@@ -142,7 +145,7 @@ unsigned long arch_get_unmapped_area_topdown(struct file *filp, unsigned long ad
 	info.align_mask = get_align_mask(filp, flags);
 	if (!(filp && is_file_hugepages(filp)))
 		info.align_offset = pgoff << PAGE_SHIFT;
-	addr = vm_unmapped_area(&info);
+	addr = vm_unmapped_area(mm, &info);
 
 	/*
 	 * A failed mmap() very likely causes application failure,
@@ -155,7 +158,7 @@ unsigned long arch_get_unmapped_area_topdown(struct file *filp, unsigned long ad
 		info.flags = 0;
 		info.low_limit = TASK_UNMAPPED_BASE;
 		info.high_limit = TASK_SIZE;
-		addr = vm_unmapped_area(&info);
+		addr = vm_unmapped_area(mm, &info);
 		if (offset_in_page(addr))
 			return addr;
 	}
diff --git a/arch/sh/mm/mmap.c b/arch/sh/mm/mmap.c
index c442734d9b0c..d7a790521539 100644
--- a/arch/sh/mm/mmap.c
+++ b/arch/sh/mm/mmap.c
@@ -51,11 +51,10 @@ static inline unsigned long COLOUR_ALIGN(unsigned long addr,
 	return base + off;
 }
 
-unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr,
-	unsigned long len, unsigned long pgoff, unsigned long flags,
-	vm_flags_t vm_flags)
+unsigned long arch_get_unmapped_area(struct mm_struct *mm, struct file *filp,
+		unsigned long addr, unsigned long len, unsigned long pgoff,
+		unsigned long flags, vm_flags_t vm_flags)
 {
-	struct mm_struct *mm = current->mm;
 	struct vm_area_struct *vma;
 	int do_colour_align;
 	struct vm_unmapped_area_info info = {};
@@ -94,16 +93,16 @@ unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr,
 	info.high_limit = TASK_SIZE;
 	info.align_mask = do_colour_align ? (PAGE_MASK & shm_align_mask) : 0;
 	info.align_offset = pgoff << PAGE_SHIFT;
-	return vm_unmapped_area(&info);
+	return vm_unmapped_area(mm, &info);
 }
 
 unsigned long
-arch_get_unmapped_area_topdown(struct file *filp, const unsigned long addr0,
-			  const unsigned long len, const unsigned long pgoff,
-			  const unsigned long flags, vm_flags_t vm_flags)
+arch_get_unmapped_area_topdown(struct mm_struct *mm, struct file *filp,
+		const unsigned long addr0, const unsigned long len,
+		const unsigned long pgoff, const unsigned long flags,
+		vm_flags_t vm_flags)
 {
 	struct vm_area_struct *vma;
-	struct mm_struct *mm = current->mm;
 	unsigned long addr = addr0;
 	int do_colour_align;
 	struct vm_unmapped_area_info info = {};
@@ -144,7 +143,7 @@ arch_get_unmapped_area_topdown(struct file *filp, const unsigned long addr0,
 	info.high_limit = mm->mmap_base;
 	info.align_mask = do_colour_align ? (PAGE_MASK & shm_align_mask) : 0;
 	info.align_offset = pgoff << PAGE_SHIFT;
-	addr = vm_unmapped_area(&info);
+	addr = vm_unmapped_area(mm, &info);
 
 	/*
 	 * A failed mmap() very likely causes application failure,
@@ -157,7 +156,7 @@ arch_get_unmapped_area_topdown(struct file *filp, const unsigned long addr0,
 		info.flags = 0;
 		info.low_limit = TASK_UNMAPPED_BASE;
 		info.high_limit = TASK_SIZE;
-		addr = vm_unmapped_area(&info);
+		addr = vm_unmapped_area(mm, &info);
 	}
 
 	return addr;
diff --git a/arch/sparc/include/asm/pgtable_64.h b/arch/sparc/include/asm/pgtable_64.h
index 74ede706fb32..b8bed39cddc9 100644
--- a/arch/sparc/include/asm/pgtable_64.h
+++ b/arch/sparc/include/asm/pgtable_64.h
@@ -1141,9 +1141,9 @@ static inline bool pte_access_permitted(pte_t pte, bool write)
 /* We provide a special get_unmapped_area for framebuffer mmaps to try and use
  * the largest alignment possible such that larget PTEs can be used.
  */
-unsigned long get_fb_unmapped_area(struct file *filp, unsigned long,
-				   unsigned long, unsigned long,
-				   unsigned long);
+unsigned long get_fb_unmapped_area(struct mm_struct *mm, struct file *filp,
+				   unsigned long addr, unsigned long len,
+				   unsigned long pgoff, unsigned long flags);
 #define HAVE_ARCH_FB_UNMAPPED_AREA
 
 void sun4v_register_fault_status(void);
diff --git a/arch/sparc/kernel/sys_sparc_32.c b/arch/sparc/kernel/sys_sparc_32.c
index fb31bc0c5b48..930ca7f8a7dc 100644
--- a/arch/sparc/kernel/sys_sparc_32.c
+++ b/arch/sparc/kernel/sys_sparc_32.c
@@ -40,7 +40,10 @@ SYSCALL_DEFINE0(getpagesize)
 	return PAGE_SIZE; /* Possibly older binaries want 8192 on sun4's? */
 }
 
-unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags, vm_flags_t vm_flags)
+unsigned long arch_get_unmapped_area(struct mm_struct *mm, struct file *filp,
+				     unsigned long addr, unsigned long len,
+				     unsigned long pgoff, unsigned long flags,
+				     vm_flags_t vm_flags)
 {
 	struct vm_unmapped_area_info info = {};
 	bool file_hugepage = false;
@@ -74,7 +77,7 @@ unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr, unsi
 	} else {
 		info.align_mask = huge_page_mask_align(filp);
 	}
-	return vm_unmapped_area(&info);
+	return vm_unmapped_area(mm, &info);
 }
 
 /*
diff --git a/arch/sparc/kernel/sys_sparc_64.c b/arch/sparc/kernel/sys_sparc_64.c
index ecefcffcf7b1..e9f5e1b37ee9 100644
--- a/arch/sparc/kernel/sys_sparc_64.c
+++ b/arch/sparc/kernel/sys_sparc_64.c
@@ -98,9 +98,11 @@ static unsigned long get_align_mask(struct file *filp, unsigned long flags)
 	return 0;
 }
 
-unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags, vm_flags_t vm_flags)
+unsigned long arch_get_unmapped_area(struct mm_struct *mm, struct file *filp,
+				     unsigned long addr, unsigned long len,
+				     unsigned long pgoff, unsigned long flags,
+				     vm_flags_t vm_flags)
 {
-	struct mm_struct *mm = current->mm;
 	struct vm_area_struct * vma;
 	unsigned long task_size = TASK_SIZE;
 	int do_color_align;
@@ -147,25 +149,25 @@ unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr, unsi
 	info.align_mask = get_align_mask(filp, flags);
 	if (!file_hugepage)
 		info.align_offset = pgoff << PAGE_SHIFT;
-	addr = vm_unmapped_area(&info);
+	addr = vm_unmapped_area(mm, &info);
 
 	if ((addr & ~PAGE_MASK) && task_size > VA_EXCLUDE_END) {
 		VM_BUG_ON(addr != -ENOMEM);
 		info.low_limit = VA_EXCLUDE_END;
 		info.high_limit = task_size;
-		addr = vm_unmapped_area(&info);
+		addr = vm_unmapped_area(mm, &info);
 	}
 
 	return addr;
 }
 
 unsigned long
-arch_get_unmapped_area_topdown(struct file *filp, const unsigned long addr0,
-			  const unsigned long len, const unsigned long pgoff,
-			  const unsigned long flags, vm_flags_t vm_flags)
+arch_get_unmapped_area_topdown(struct mm_struct *mm, struct file *filp,
+			  const unsigned long addr0, const unsigned long len,
+			  const unsigned long pgoff, const unsigned long flags,
+			  vm_flags_t vm_flags)
 {
 	struct vm_area_struct *vma;
-	struct mm_struct *mm = current->mm;
 	unsigned long task_size = STACK_TOP32;
 	unsigned long addr = addr0;
 	int do_color_align;
@@ -215,7 +217,7 @@ arch_get_unmapped_area_topdown(struct file *filp, const unsigned long addr0,
 	info.align_mask = get_align_mask(filp, flags);
 	if (!file_hugepage)
 		info.align_offset = pgoff << PAGE_SHIFT;
-	addr = vm_unmapped_area(&info);
+	addr = vm_unmapped_area(mm, &info);
 
 	/*
 	 * A failed mmap() very likely causes application failure,
@@ -228,20 +230,22 @@ arch_get_unmapped_area_topdown(struct file *filp, const unsigned long addr0,
 		info.flags = 0;
 		info.low_limit = TASK_UNMAPPED_BASE;
 		info.high_limit = STACK_TOP32;
-		addr = vm_unmapped_area(&info);
+		addr = vm_unmapped_area(mm, &info);
 	}
 
 	return addr;
 }
 
 /* Try to align mapping such that we align it as much as possible. */
-unsigned long get_fb_unmapped_area(struct file *filp, unsigned long orig_addr, unsigned long len, unsigned long pgoff, unsigned long flags)
+unsigned long get_fb_unmapped_area(struct mm_struct *mm, struct file *filp,
+				   unsigned long orig_addr, unsigned long len,
+				   unsigned long pgoff, unsigned long flags)
 {
 	unsigned long align_goal, addr = -ENOMEM;
 
 	if (flags & MAP_FIXED) {
 		/* Ok, don't mess with it. */
-		return mm_get_unmapped_area(NULL, orig_addr, len, pgoff, flags);
+		return mm_get_unmapped_area(mm, NULL, orig_addr, len, pgoff, flags);
 	}
 	flags &= ~MAP_SHARED;
 
@@ -254,7 +258,7 @@ unsigned long get_fb_unmapped_area(struct file *filp, unsigned long orig_addr, u
 		align_goal = (64UL * 1024);
 
 	do {
-		addr = mm_get_unmapped_area(NULL, orig_addr,
+		addr = mm_get_unmapped_area(mm, NULL, orig_addr,
 					    len + (align_goal - PAGE_SIZE), pgoff, flags);
 		if (!(addr & ~PAGE_MASK)) {
 			addr = (addr + (align_goal - 1UL)) & ~(align_goal - 1UL);
@@ -273,7 +277,7 @@ unsigned long get_fb_unmapped_area(struct file *filp, unsigned long orig_addr, u
 	 * be obtained.
 	 */
 	if (addr & ~PAGE_MASK)
-		addr = mm_get_unmapped_area(NULL, orig_addr, len, pgoff, flags);
+		addr = mm_get_unmapped_area(mm, NULL, orig_addr, len, pgoff, flags);
 
 	return addr;
 }
diff --git a/arch/x86/include/asm/elf.h b/arch/x86/include/asm/elf.h
index 0de9df759c99..12c66609889e 100644
--- a/arch/x86/include/asm/elf.h
+++ b/arch/x86/include/asm/elf.h
@@ -307,7 +307,7 @@ static inline int mmap_is_ia32(void)
 
 extern unsigned long task_size_32bit(void);
 extern unsigned long task_size_64bit(int full_addr_space);
-extern unsigned long get_mmap_base(int is_legacy);
+extern unsigned long get_mmap_base(struct mm_struct *mm, int is_legacy);
 extern bool mmap_address_hint_valid(unsigned long addr, unsigned long len);
 extern unsigned long get_sigframe_size(void);
 
diff --git a/arch/x86/kernel/cpu/sgx/driver.c b/arch/x86/kernel/cpu/sgx/driver.c
index 9268289cd9f9..f81652086909 100644
--- a/arch/x86/kernel/cpu/sgx/driver.c
+++ b/arch/x86/kernel/cpu/sgx/driver.c
@@ -121,11 +121,12 @@ static int sgx_mmap(struct file *file, struct vm_area_struct *vma)
 	return 0;
 }
 
-static unsigned long sgx_get_unmapped_area(struct file *file,
-					   unsigned long addr,
-					   unsigned long len,
-					   unsigned long pgoff,
-					   unsigned long flags)
+static unsigned long sgx_get_unmapped_area(struct mm_struct *mm,
+					    struct file *file,
+					    unsigned long addr,
+					    unsigned long len,
+					    unsigned long pgoff,
+					    unsigned long flags)
 {
 	if ((flags & MAP_TYPE) == MAP_PRIVATE)
 		return -EINVAL;
@@ -133,7 +134,7 @@ static unsigned long sgx_get_unmapped_area(struct file *file,
 	if (flags & MAP_FIXED)
 		return addr;
 
-	return mm_get_unmapped_area(file, addr, len, pgoff, flags);
+	return mm_get_unmapped_area(mm, file, addr, len, pgoff, flags);
 }
 
 #ifdef CONFIG_COMPAT
diff --git a/arch/x86/kernel/sys_x86_64.c b/arch/x86/kernel/sys_x86_64.c
index 776ae6fa7f2d..d9c4353913a4 100644
--- a/arch/x86/kernel/sys_x86_64.c
+++ b/arch/x86/kernel/sys_x86_64.c
@@ -89,8 +89,9 @@ SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,
 	return ksys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
 }
 
-static void find_start_end(unsigned long addr, unsigned long flags,
-		unsigned long *begin, unsigned long *end)
+static void find_start_end(struct mm_struct *mm, unsigned long addr,
+			   unsigned long flags, unsigned long *begin,
+			   unsigned long *end)
 {
 	if (!in_32bit_syscall() && (flags & MAP_32BIT)) {
 		/* This is usually used needed to map code in small
@@ -108,7 +109,7 @@ static void find_start_end(unsigned long addr, unsigned long flags,
 		return;
 	}
 
-	*begin	= get_mmap_base(1);
+	*begin	= get_mmap_base(mm, 1);
 	if (in_32bit_syscall())
 		*end = task_size_32bit();
 	else
@@ -124,10 +125,11 @@ static inline unsigned long stack_guard_placement(vm_flags_t vm_flags)
 }
 
 unsigned long
-arch_get_unmapped_area(struct file *filp, unsigned long addr, unsigned long len,
-		       unsigned long pgoff, unsigned long flags, vm_flags_t vm_flags)
+arch_get_unmapped_area(struct mm_struct *mm, struct file *filp,
+		       unsigned long addr, unsigned long len,
+		       unsigned long pgoff, unsigned long flags,
+		       vm_flags_t vm_flags)
 {
-	struct mm_struct *mm = current->mm;
 	struct vm_area_struct *vma;
 	struct vm_unmapped_area_info info = {};
 	unsigned long begin, end;
@@ -135,7 +137,7 @@ arch_get_unmapped_area(struct file *filp, unsigned long addr, unsigned long len,
 	if (flags & MAP_FIXED)
 		return addr;
 
-	find_start_end(addr, flags, &begin, &end);
+	find_start_end(mm, addr, flags, &begin, &end);
 
 	if (len > end)
 		return -ENOMEM;
@@ -160,16 +162,16 @@ arch_get_unmapped_area(struct file *filp, unsigned long addr, unsigned long len,
 		info.align_offset += get_align_bits();
 	}
 
-	return vm_unmapped_area(&info);
+	return vm_unmapped_area(mm, &info);
 }
 
 unsigned long
-arch_get_unmapped_area_topdown(struct file *filp, unsigned long addr0,
-			  unsigned long len, unsigned long pgoff,
-			  unsigned long flags, vm_flags_t vm_flags)
+arch_get_unmapped_area_topdown(struct mm_struct *mm, struct file *filp,
+			  unsigned long addr0, unsigned long len,
+			  unsigned long pgoff, unsigned long flags,
+			  vm_flags_t vm_flags)
 {
 	struct vm_area_struct *vma;
-	struct mm_struct *mm = current->mm;
 	unsigned long addr = addr0;
 	struct vm_unmapped_area_info info = {};
 
@@ -204,7 +206,7 @@ arch_get_unmapped_area_topdown(struct file *filp, unsigned long addr0,
 	else
 		info.low_limit = PAGE_SIZE;
 
-	info.high_limit = get_mmap_base(0);
+	info.high_limit = get_mmap_base(mm, 0);
 	if (!(filp && is_file_hugepages(filp))) {
 		info.start_gap = stack_guard_placement(vm_flags);
 		info.align_offset = pgoff << PAGE_SHIFT;
@@ -224,7 +226,7 @@ arch_get_unmapped_area_topdown(struct file *filp, unsigned long addr0,
 		info.align_mask = get_align_mask(filp);
 		info.align_offset += get_align_bits();
 	}
-	addr = vm_unmapped_area(&info);
+	addr = vm_unmapped_area(mm, &info);
 	if (!(addr & ~PAGE_MASK))
 		return addr;
 	VM_BUG_ON(addr != -ENOMEM);
@@ -236,5 +238,5 @@ arch_get_unmapped_area_topdown(struct file *filp, unsigned long addr0,
 	 * can happen with large stack limits and large mmap()
 	 * allocations.
 	 */
-	return arch_get_unmapped_area(filp, addr0, len, pgoff, flags, 0);
+	return arch_get_unmapped_area(mm, filp, addr0, len, pgoff, flags, 0);
 }
diff --git a/arch/x86/kernel/uprobes.c b/arch/x86/kernel/uprobes.c
index 3af979fb41d3..417153d02c54 100644
--- a/arch/x86/kernel/uprobes.c
+++ b/arch/x86/kernel/uprobes.c
@@ -661,13 +661,13 @@ static unsigned long find_nearest_trampoline(unsigned long vaddr)
 	/* Search up from the caller address. */
 	info.low_limit = call_end;
 	info.high_limit = min(high_limit, TASK_SIZE);
-	high_tramp = vm_unmapped_area(&info);
+	high_tramp = vm_unmapped_area(current->mm, &info);
 
 	/* Search down from the caller address. */
 	info.low_limit = max(low_limit, PAGE_SIZE);
 	info.high_limit = call_end;
 	info.flags = VM_UNMAPPED_AREA_TOPDOWN;
-	low_tramp = vm_unmapped_area(&info);
+	low_tramp = vm_unmapped_area(current->mm, &info);
 
 	if (IS_ERR_VALUE(high_tramp) && IS_ERR_VALUE(low_tramp))
 		return -ENOMEM;
diff --git a/arch/x86/mm/mmap.c b/arch/x86/mm/mmap.c
index 82f3a987f7cf..75c864f3c0d8 100644
--- a/arch/x86/mm/mmap.c
+++ b/arch/x86/mm/mmap.c
@@ -143,10 +143,8 @@ void arch_pick_mmap_layout(struct mm_struct *mm, const struct rlimit *rlim_stack
 #endif
 }
 
-unsigned long get_mmap_base(int is_legacy)
+unsigned long get_mmap_base(struct mm_struct *mm, int is_legacy)
 {
-	struct mm_struct *mm = current->mm;
-
 #ifdef CONFIG_HAVE_ARCH_COMPAT_MMAP_BASES
 	if (in_32bit_syscall()) {
 		return is_legacy ? mm->mmap_compat_legacy_base
diff --git a/arch/xtensa/kernel/syscall.c b/arch/xtensa/kernel/syscall.c
index dc54f854c2f5..51bfe06746d7 100644
--- a/arch/xtensa/kernel/syscall.c
+++ b/arch/xtensa/kernel/syscall.c
@@ -54,9 +54,9 @@ asmlinkage long xtensa_fadvise64_64(int fd, int advice,
 }
 
 #ifdef CONFIG_MMU
-unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr,
-		unsigned long len, unsigned long pgoff, unsigned long flags,
-		vm_flags_t vm_flags)
+unsigned long arch_get_unmapped_area(struct mm_struct *mm, struct file *filp,
+		unsigned long addr, unsigned long len, unsigned long pgoff,
+		unsigned long flags, vm_flags_t vm_flags)
 {
 	struct vm_area_struct *vmm;
 	struct vma_iterator vmi;
@@ -81,7 +81,7 @@ unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr,
 	else
 		addr = PAGE_ALIGN(addr);
 
-	vma_iter_init(&vmi, current->mm, addr);
+	vma_iter_init(&vmi, mm, addr);
 	for_each_vma(vmi, vmm) {
 		/* At this point:  (addr < vmm->vm_end). */
 		if (addr + len <= vm_start_gap(vmm))
diff --git a/drivers/char/mem.c b/drivers/char/mem.c
index 63253d1de5d7..204bf406173d 100644
--- a/drivers/char/mem.c
+++ b/drivers/char/mem.c
@@ -280,7 +280,8 @@ static pgprot_t phys_mem_access_prot(struct file *file, unsigned long pfn,
 #endif
 
 #ifndef CONFIG_MMU
-static unsigned long get_unmapped_area_mem(struct file *file,
+static unsigned long get_unmapped_area_mem(struct mm_struct *mm,
+					   struct file *file,
 					   unsigned long addr,
 					   unsigned long len,
 					   unsigned long pgoff,
@@ -515,14 +516,16 @@ static int mmap_zero_prepare(struct vm_area_desc *desc)
 }
 
 #ifndef CONFIG_MMU
-static unsigned long get_unmapped_area_zero(struct file *file,
+static unsigned long get_unmapped_area_zero(struct mm_struct *mm,
+				struct file *file,
 				unsigned long addr, unsigned long len,
 				unsigned long pgoff, unsigned long flags)
 {
 	return -ENOSYS;
 }
 #else
-static unsigned long get_unmapped_area_zero(struct file *file,
+static unsigned long get_unmapped_area_zero(struct mm_struct *mm,
+				struct file *file,
 				unsigned long addr, unsigned long len,
 				unsigned long pgoff, unsigned long flags)
 {
@@ -534,7 +537,7 @@ static unsigned long get_unmapped_area_zero(struct file *file,
 		 * get_unmapped_area(), so as not to confuse shmem with our
 		 * handle on "/dev/zero".
 		 */
-		return shmem_get_unmapped_area(NULL, addr, len, pgoff, flags);
+		return shmem_get_unmapped_area(mm, NULL, addr, len, pgoff, flags);
 	}
 
 	/*
@@ -543,9 +546,9 @@ static unsigned long get_unmapped_area_zero(struct file *file,
 	 * fall back to system page size mappings.
 	 */
 #ifdef CONFIG_TRANSPARENT_HUGEPAGE
-	return thp_get_unmapped_area(file, addr, len, pgoff, flags);
+	return thp_get_unmapped_area(mm, file, addr, len, pgoff, flags);
 #else
-	return mm_get_unmapped_area(file, addr, len, pgoff, flags);
+	return mm_get_unmapped_area(mm, file, addr, len, pgoff, flags);
 #endif
 }
 #endif /* CONFIG_MMU */
diff --git a/drivers/dax/device.c b/drivers/dax/device.c
index d0c9b4e03b47..80df897b6535 100644
--- a/drivers/dax/device.c
+++ b/drivers/dax/device.c
@@ -295,9 +295,9 @@ static int dax_mmap_prepare(struct vm_area_desc *desc)
 }
 
 /* return an unmapped area aligned to the dax region specified alignment */
-static unsigned long dax_get_unmapped_area(struct file *filp,
-		unsigned long addr, unsigned long len, unsigned long pgoff,
-		unsigned long flags)
+static unsigned long dax_get_unmapped_area(struct mm_struct *mm,
+		struct file *filp, unsigned long addr, unsigned long len,
+		unsigned long pgoff, unsigned long flags)
 {
 	unsigned long off, off_end, off_align, len_align, addr_align, align;
 	struct dev_dax *dev_dax = filp ? filp->private_data : NULL;
@@ -317,13 +317,14 @@ static unsigned long dax_get_unmapped_area(struct file *filp,
 	if ((off + len_align) < off)
 		goto out;
 
-	addr_align = mm_get_unmapped_area(filp, addr, len_align, pgoff, flags);
+	addr_align = mm_get_unmapped_area(mm, filp, addr, len_align, pgoff,
+					  flags);
 	if (!IS_ERR_VALUE(addr_align)) {
 		addr_align += (off - addr_align) & (align - 1);
 		return addr_align;
 	}
  out:
-	return mm_get_unmapped_area(filp, addr, len, pgoff, flags);
+	return mm_get_unmapped_area(mm, filp, addr, len, pgoff, flags);
 }
 
 static const struct address_space_operations dev_dax_aops = {
diff --git a/drivers/gpu/drm/drm_gem.c b/drivers/gpu/drm/drm_gem.c
index e3ed684ddcf2..f3d4f71e61d4 100644
--- a/drivers/gpu/drm/drm_gem.c
+++ b/drivers/gpu/drm/drm_gem.c
@@ -1316,6 +1316,7 @@ drm_gem_object_lookup_at_offset(struct file *filp, unsigned long start,
 #ifdef CONFIG_MMU
 /**
  * drm_gem_get_unmapped_area - get memory mapping region routine for GEM objects
+ * @mm: mm_struct the mapping is placed in
  * @filp: DRM file pointer
  * @uaddr: User address hint
  * @len: Mapping length
@@ -1336,7 +1337,8 @@ drm_gem_object_lookup_at_offset(struct file *filp, unsigned long start,
  * If a GEM object is not available at the given offset or if the caller is not
  * granted access to it, fall back to mm_get_unmapped_area().
  */
-unsigned long drm_gem_get_unmapped_area(struct file *filp, unsigned long uaddr,
+unsigned long drm_gem_get_unmapped_area(struct mm_struct *mm,
+					struct file *filp, unsigned long uaddr,
 					unsigned long len, unsigned long pgoff,
 					unsigned long flags)
 {
@@ -1348,9 +1350,10 @@ unsigned long drm_gem_get_unmapped_area(struct file *filp, unsigned long uaddr,
 		obj = NULL;
 
 	if (!obj || !obj->filp || !obj->filp->f_op->get_unmapped_area)
-		ret = mm_get_unmapped_area(filp, uaddr, len, 0, flags);
+		ret = mm_get_unmapped_area(mm, filp, uaddr, len, 0, flags);
 	else
-		ret = obj->filp->f_op->get_unmapped_area(obj->filp, uaddr, len, 0, flags);
+		ret = obj->filp->f_op->get_unmapped_area(mm, obj->filp, uaddr,
+							 len, 0, flags);
 
 	drm_gem_object_put(obj);
 
diff --git a/drivers/gpu/drm/drm_gem_dma_helper.c b/drivers/gpu/drm/drm_gem_dma_helper.c
index 1c00a71ab3c9..59f517a4f283 100644
--- a/drivers/gpu/drm/drm_gem_dma_helper.c
+++ b/drivers/gpu/drm/drm_gem_dma_helper.c
@@ -330,6 +330,7 @@ EXPORT_SYMBOL_GPL(drm_gem_dma_vm_ops);
 #ifndef CONFIG_MMU
 /**
  * drm_gem_dma_get_unmapped_area - propose address for mapping in noMMU cases
+ * @mm: mm_struct the mapping is placed in
  * @filp: file object
  * @addr: memory address
  * @len: buffer size
@@ -344,7 +345,8 @@ EXPORT_SYMBOL_GPL(drm_gem_dma_vm_ops);
  * Returns:
  * mapping address on success or a negative error code on failure.
  */
-unsigned long drm_gem_dma_get_unmapped_area(struct file *filp,
+unsigned long drm_gem_dma_get_unmapped_area(struct mm_struct *mm,
+					    struct file *filp,
 					    unsigned long addr,
 					    unsigned long len,
 					    unsigned long pgoff,
diff --git a/drivers/media/v4l2-core/v4l2-dev.c b/drivers/media/v4l2-core/v4l2-dev.c
index 5516b2bbb08f..1cf7c36fc945 100644
--- a/drivers/media/v4l2-core/v4l2-dev.c
+++ b/drivers/media/v4l2-core/v4l2-dev.c
@@ -373,7 +373,8 @@ static long v4l2_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
 #ifdef CONFIG_MMU
 #define v4l2_get_unmapped_area NULL
 #else
-static unsigned long v4l2_get_unmapped_area(struct file *filp,
+static unsigned long v4l2_get_unmapped_area(struct mm_struct *mm,
+		struct file *filp,
 		unsigned long addr, unsigned long len, unsigned long pgoff,
 		unsigned long flags)
 {
diff --git a/drivers/mtd/mtdchar.c b/drivers/mtd/mtdchar.c
index bf01e6ac7293..e208a8de8716 100644
--- a/drivers/mtd/mtdchar.c
+++ b/drivers/mtd/mtdchar.c
@@ -1340,7 +1340,8 @@ static long mtdchar_compat_ioctl(struct file *file, unsigned int cmd,
  *   mappings)
  */
 #ifndef CONFIG_MMU
-static unsigned long mtdchar_get_unmapped_area(struct file *file,
+static unsigned long mtdchar_get_unmapped_area(struct mm_struct *mm,
+					   struct file *file,
 					   unsigned long addr,
 					   unsigned long len,
 					   unsigned long pgoff,
diff --git a/drivers/video/fbdev/core/fb_chrdev.c b/drivers/video/fbdev/core/fb_chrdev.c
index ba1d0bc214c5..cf8590b64b54 100644
--- a/drivers/video/fbdev/core/fb_chrdev.c
+++ b/drivers/video/fbdev/core/fb_chrdev.c
@@ -383,7 +383,8 @@ __releases(&info->lock)
 }
 
 #if defined(CONFIG_FB_PROVIDE_GET_FB_UNMAPPED_AREA) && !defined(CONFIG_MMU)
-static unsigned long get_fb_unmapped_area(struct file *filp,
+static unsigned long get_fb_unmapped_area(struct mm_struct *mm,
+				   struct file *filp,
 				   unsigned long addr, unsigned long len,
 				   unsigned long pgoff, unsigned long flags)
 {
diff --git a/fs/cramfs/inode.c b/fs/cramfs/inode.c
index 4edbfccd0bbe..d3aee6705b8d 100644
--- a/fs/cramfs/inode.c
+++ b/fs/cramfs/inode.c
@@ -449,7 +449,8 @@ static int cramfs_physmem_mmap(struct file *file, struct vm_area_struct *vma)
 	return is_nommu_shared_mapping(vma->vm_flags) ? 0 : -ENOSYS;
 }
 
-static unsigned long cramfs_physmem_get_unmapped_area(struct file *file,
+static unsigned long cramfs_physmem_get_unmapped_area(struct mm_struct *mm,
+			struct file *file,
 			unsigned long addr, unsigned long len,
 			unsigned long pgoff, unsigned long flags)
 {
diff --git a/fs/hugetlbfs/inode.c b/fs/hugetlbfs/inode.c
index 216e1a0dd0b2..f5d3c182cc7c 100644
--- a/fs/hugetlbfs/inode.c
+++ b/fs/hugetlbfs/inode.c
@@ -170,9 +170,9 @@ static int hugetlbfs_file_mmap(struct file *file, struct vm_area_struct *vma)
  */
 
 unsigned long
-hugetlb_get_unmapped_area(struct file *file, unsigned long addr,
-			    unsigned long len, unsigned long pgoff,
-			    unsigned long flags)
+hugetlb_get_unmapped_area(struct mm_struct *mm, struct file *file,
+			    unsigned long addr, unsigned long len,
+			    unsigned long pgoff, unsigned long flags)
 {
 	unsigned long addr0 = 0;
 	struct hstate *h = hstate_file(file);
@@ -184,7 +184,8 @@ hugetlb_get_unmapped_area(struct file *file, unsigned long addr,
 	if (addr)
 		addr0 = ALIGN(addr, huge_page_size(h));
 
-	return mm_get_unmapped_area_vmflags(file, addr0, len, pgoff, flags, 0);
+	return mm_get_unmapped_area_vmflags(mm, file, addr0, len, pgoff,
+					    flags, 0);
 }
 
 /*
diff --git a/fs/proc/inode.c b/fs/proc/inode.c
index b7634f975d98..3e542fe3118d 100644
--- a/fs/proc/inode.c
+++ b/fs/proc/inode.c
@@ -435,22 +435,25 @@ static int proc_reg_mmap(struct file *file, struct vm_area_struct *vma)
 }
 
 static unsigned long
-pde_get_unmapped_area(struct proc_dir_entry *pde, struct file *file, unsigned long orig_addr,
+pde_get_unmapped_area(struct proc_dir_entry *pde, struct mm_struct *mm,
+			   struct file *file, unsigned long orig_addr,
 			   unsigned long len, unsigned long pgoff,
 			   unsigned long flags)
 {
 	if (pde->proc_ops->proc_get_unmapped_area)
-		return pde->proc_ops->proc_get_unmapped_area(file, orig_addr, len, pgoff, flags);
+		return pde->proc_ops->proc_get_unmapped_area(mm, file,
+					orig_addr, len, pgoff, flags);
 
 #ifdef CONFIG_MMU
-	return mm_get_unmapped_area(file, orig_addr, len, pgoff, flags);
+	return mm_get_unmapped_area(mm, file, orig_addr, len, pgoff, flags);
 #endif
 
 	return orig_addr;
 }
 
 static unsigned long
-proc_reg_get_unmapped_area(struct file *file, unsigned long orig_addr,
+proc_reg_get_unmapped_area(struct mm_struct *mm, struct file *file,
+			   unsigned long orig_addr,
 			   unsigned long len, unsigned long pgoff,
 			   unsigned long flags)
 {
@@ -458,9 +461,9 @@ proc_reg_get_unmapped_area(struct file *file, unsigned long orig_addr,
 	unsigned long rv = -EIO;
 
 	if (pde_is_permanent(pde)) {
-		return pde_get_unmapped_area(pde, file, orig_addr, len, pgoff, flags);
+		return pde_get_unmapped_area(pde, mm, file, orig_addr, len, pgoff, flags);
 	} else if (use_pde(pde)) {
-		rv = pde_get_unmapped_area(pde, file, orig_addr, len, pgoff, flags);
+		rv = pde_get_unmapped_area(pde, mm, file, orig_addr, len, pgoff, flags);
 		unuse_pde(pde);
 	}
 	return rv;
diff --git a/fs/ramfs/file-mmu.c b/fs/ramfs/file-mmu.c
index c3ed1c5117b2..e070ca06499b 100644
--- a/fs/ramfs/file-mmu.c
+++ b/fs/ramfs/file-mmu.c
@@ -31,11 +31,12 @@
 
 #include "internal.h"
 
-static unsigned long ramfs_mmu_get_unmapped_area(struct file *file,
+static unsigned long ramfs_mmu_get_unmapped_area(struct mm_struct *mm,
+		struct file *file,
 		unsigned long addr, unsigned long len, unsigned long pgoff,
 		unsigned long flags)
 {
-	return mm_get_unmapped_area(file, addr, len, pgoff, flags);
+	return mm_get_unmapped_area(mm, file, addr, len, pgoff, flags);
 }
 
 const struct file_operations ramfs_file_operations = {
diff --git a/fs/ramfs/file-nommu.c b/fs/ramfs/file-nommu.c
index 2f79bcb89d2e..2e20fc9c0d37 100644
--- a/fs/ramfs/file-nommu.c
+++ b/fs/ramfs/file-nommu.c
@@ -23,7 +23,8 @@
 #include "internal.h"
 
 static int ramfs_nommu_setattr(struct mnt_idmap *, struct dentry *, struct iattr *);
-static unsigned long ramfs_nommu_get_unmapped_area(struct file *file,
+static unsigned long ramfs_nommu_get_unmapped_area(struct mm_struct *mm,
+						   struct file *file,
 						   unsigned long addr,
 						   unsigned long len,
 						   unsigned long pgoff,
@@ -199,7 +200,8 @@ static int ramfs_nommu_setattr(struct mnt_idmap *idmap,
  *   - the pages to be mapped must exist
  *   - the pages be physically contiguous in sequence
  */
-static unsigned long ramfs_nommu_get_unmapped_area(struct file *file,
+static unsigned long ramfs_nommu_get_unmapped_area(struct mm_struct *mm,
+					    struct file *file,
 					    unsigned long addr, unsigned long len,
 					    unsigned long pgoff, unsigned long flags)
 {
diff --git a/fs/romfs/mmap-nommu.c b/fs/romfs/mmap-nommu.c
index 7c3a1a7fecee..18c1e8545b0d 100644
--- a/fs/romfs/mmap-nommu.c
+++ b/fs/romfs/mmap-nommu.c
@@ -15,7 +15,8 @@
  *   mappings)
  * - attempts to map through to the underlying MTD device
  */
-static unsigned long romfs_get_unmapped_area(struct file *file,
+static unsigned long romfs_get_unmapped_area(struct mm_struct *mm,
+					     struct file *file,
 					     unsigned long addr,
 					     unsigned long len,
 					     unsigned long pgoff,
diff --git a/include/drm/drm_gem.h b/include/drm/drm_gem.h
index 8a704f6a65c1..a99d525f0369 100644
--- a/include/drm/drm_gem.h
+++ b/include/drm/drm_gem.h
@@ -536,7 +536,8 @@ int drm_gem_mmap_obj(struct drm_gem_object *obj, unsigned long obj_size,
 int drm_gem_mmap(struct file *filp, struct vm_area_struct *vma);
 
 #ifdef CONFIG_MMU
-unsigned long drm_gem_get_unmapped_area(struct file *filp, unsigned long uaddr,
+unsigned long drm_gem_get_unmapped_area(struct mm_struct *mm,
+					struct file *filp, unsigned long uaddr,
 					unsigned long len, unsigned long pgoff,
 					unsigned long flags);
 #else
diff --git a/include/drm/drm_gem_dma_helper.h b/include/drm/drm_gem_dma_helper.h
index f2678e7ecb98..127f52f192a9 100644
--- a/include/drm/drm_gem_dma_helper.h
+++ b/include/drm/drm_gem_dma_helper.h
@@ -232,7 +232,8 @@ drm_gem_dma_prime_import_sg_table_vmap(struct drm_device *drm,
  */
 
 #ifndef CONFIG_MMU
-unsigned long drm_gem_dma_get_unmapped_area(struct file *filp,
+unsigned long drm_gem_dma_get_unmapped_area(struct mm_struct *mm,
+					    struct file *filp,
 					    unsigned long addr,
 					    unsigned long len,
 					    unsigned long pgoff,
diff --git a/include/linux/bpf.h b/include/linux/bpf.h
index 7719f6528445..d72a5ba49654 100644
--- a/include/linux/bpf.h
+++ b/include/linux/bpf.h
@@ -147,7 +147,8 @@ struct bpf_map_ops {
 	int (*map_mmap)(struct bpf_map *map, struct vm_area_struct *vma);
 	__poll_t (*map_poll)(struct bpf_map *map, struct file *filp,
 			     struct poll_table_struct *pts);
-	unsigned long (*map_get_unmapped_area)(struct file *filep, unsigned long addr,
+	unsigned long (*map_get_unmapped_area)(struct mm_struct *mm,
+					       struct file *filep, unsigned long addr,
 					       unsigned long len, unsigned long pgoff,
 					       unsigned long flags);
 
diff --git a/include/linux/fs.h b/include/linux/fs.h
index 50ce731a2b78..58e00b320efd 100644
--- a/include/linux/fs.h
+++ b/include/linux/fs.h
@@ -1939,7 +1939,10 @@ struct file_operations {
 	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
 	int (*fasync) (int, struct file *, int);
 	int (*lock) (struct file *, int, struct file_lock *);
-	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
+	unsigned long (*get_unmapped_area)(struct mm_struct *mm,
+			struct file *file, unsigned long addr,
+			unsigned long len, unsigned long pgoff,
+			unsigned long flags);
 	int (*check_flags)(int);
 	int (*flock) (struct file *, int, struct file_lock *);
 	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
diff --git a/include/linux/huge_mm.h b/include/linux/huge_mm.h
index ad20f7f8c179..5760710c3c5d 100644
--- a/include/linux/huge_mm.h
+++ b/include/linux/huge_mm.h
@@ -388,11 +388,12 @@ static inline bool thp_disabled_by_hw(void)
 	return transparent_hugepage_flags & (1 << TRANSPARENT_HUGEPAGE_UNSUPPORTED);
 }
 
-unsigned long thp_get_unmapped_area(struct file *filp, unsigned long addr,
-		unsigned long len, unsigned long pgoff, unsigned long flags);
-unsigned long thp_get_unmapped_area_vmflags(struct file *filp, unsigned long addr,
-		unsigned long len, unsigned long pgoff, unsigned long flags,
-		vm_flags_t vm_flags);
+unsigned long thp_get_unmapped_area(struct mm_struct *mm, struct file *filp,
+		unsigned long addr, unsigned long len, unsigned long pgoff,
+		unsigned long flags);
+unsigned long thp_get_unmapped_area_vmflags(struct mm_struct *mm,
+		struct file *filp, unsigned long addr, unsigned long len,
+		unsigned long pgoff, unsigned long flags, vm_flags_t vm_flags);
 
 enum split_type {
 	SPLIT_TYPE_UNIFORM,
@@ -614,9 +615,10 @@ static inline unsigned long thp_vma_allowable_orders(struct vm_area_struct *vma,
 #define thp_get_unmapped_area	NULL
 
 static inline unsigned long
-thp_get_unmapped_area_vmflags(struct file *filp, unsigned long addr,
-			      unsigned long len, unsigned long pgoff,
-			      unsigned long flags, vm_flags_t vm_flags)
+thp_get_unmapped_area_vmflags(struct mm_struct *mm, struct file *filp,
+			      unsigned long addr, unsigned long len,
+			      unsigned long pgoff, unsigned long flags,
+			      vm_flags_t vm_flags)
 {
 	return 0;
 }
diff --git a/include/linux/hugetlb.h b/include/linux/hugetlb.h
index 2abaf99321e9..b2743931f927 100644
--- a/include/linux/hugetlb.h
+++ b/include/linux/hugetlb.h
@@ -543,9 +543,9 @@ static inline struct hstate *hstate_inode(struct inode *i)
 #endif /* !CONFIG_HUGETLBFS */
 
 unsigned long
-hugetlb_get_unmapped_area(struct file *file, unsigned long addr,
-				    unsigned long len, unsigned long pgoff,
-				    unsigned long flags);
+hugetlb_get_unmapped_area(struct mm_struct *mm, struct file *file,
+				    unsigned long addr, unsigned long len,
+				    unsigned long pgoff, unsigned long flags);
 
 /*
  * huegtlb page specific state flags.  These flags are located in page.private
diff --git a/include/linux/mm.h b/include/linux/mm.h
index 485df9c2dbdd..560bc369a54c 100644
--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -4138,14 +4138,17 @@ unsigned long randomize_stack_top(unsigned long stack_top);
 unsigned long randomize_page(unsigned long start, unsigned long range);
 
 unsigned long
-__get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,
-		    unsigned long pgoff, unsigned long flags, vm_flags_t vm_flags);
+__get_unmapped_area(struct mm_struct *mm, struct file *file,
+		    unsigned long addr, unsigned long len,
+		    unsigned long pgoff, unsigned long flags,
+		    vm_flags_t vm_flags);
 
 static inline unsigned long
 get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,
 		  unsigned long pgoff, unsigned long flags)
 {
-	return __get_unmapped_area(file, addr, len, pgoff, flags, 0);
+	return __get_unmapped_area(current->mm, file, addr, len, pgoff,
+				   flags, 0);
 }
 
 extern unsigned long do_mmap(struct file *file, unsigned long addr,
@@ -4194,7 +4197,8 @@ struct vm_unmapped_area_info {
 	unsigned long start_gap;
 };
 
-extern unsigned long vm_unmapped_area(struct vm_unmapped_area_info *info);
+extern unsigned long vm_unmapped_area(struct mm_struct *mm,
+				      struct vm_unmapped_area_info *info);
 
 /* truncate.c */
 void truncate_inode_pages(struct address_space *mapping, loff_t lstart);
diff --git a/include/linux/proc_fs.h b/include/linux/proc_fs.h
index 47d7deaeed8f..e7998a303519 100644
--- a/include/linux/proc_fs.h
+++ b/include/linux/proc_fs.h
@@ -47,7 +47,10 @@ struct proc_ops {
 	long	(*proc_compat_ioctl)(struct file *, unsigned int, unsigned long);
 #endif
 	int	(*proc_mmap)(struct file *, struct vm_area_struct *);
-	unsigned long (*proc_get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
+	unsigned long (*proc_get_unmapped_area)(struct mm_struct *mm,
+			struct file *file, unsigned long addr,
+			unsigned long len, unsigned long pgoff,
+			unsigned long flags);
 } __randomize_layout;
 
 /* definitions for hide_pid field */
diff --git a/include/linux/sched/mm.h b/include/linux/sched/mm.h
index 95d0040df584..adc11009fedf 100644
--- a/include/linux/sched/mm.h
+++ b/include/linux/sched/mm.h
@@ -181,19 +181,22 @@ extern void arch_pick_mmap_layout(struct mm_struct *mm,
 				  const struct rlimit *rlim_stack);
 
 unsigned long
-arch_get_unmapped_area(struct file *filp, unsigned long addr,
-		       unsigned long len, unsigned long pgoff,
-		       unsigned long flags, vm_flags_t vm_flags);
+arch_get_unmapped_area(struct mm_struct *mm, struct file *filp,
+		       unsigned long addr, unsigned long len,
+		       unsigned long pgoff, unsigned long flags,
+		       vm_flags_t vm_flags);
 unsigned long
-arch_get_unmapped_area_topdown(struct file *filp, unsigned long addr,
-			       unsigned long len, unsigned long pgoff,
-			       unsigned long flags, vm_flags_t);
+arch_get_unmapped_area_topdown(struct mm_struct *mm, struct file *filp,
+			       unsigned long addr, unsigned long len,
+			       unsigned long pgoff, unsigned long flags,
+			       vm_flags_t);
 
-unsigned long mm_get_unmapped_area(struct file *filp, unsigned long addr,
-				   unsigned long len, unsigned long pgoff,
-				   unsigned long flags);
+unsigned long mm_get_unmapped_area(struct mm_struct *mm, struct file *filp,
+				   unsigned long addr, unsigned long len,
+				   unsigned long pgoff, unsigned long flags);
 
-unsigned long mm_get_unmapped_area_vmflags(struct file *filp,
+unsigned long mm_get_unmapped_area_vmflags(struct mm_struct *mm,
+					   struct file *filp,
 					   unsigned long addr,
 					   unsigned long len,
 					   unsigned long pgoff,
@@ -201,13 +204,15 @@ unsigned long mm_get_unmapped_area_vmflags(struct file *filp,
 					   vm_flags_t vm_flags);
 
 unsigned long
-generic_get_unmapped_area(struct file *filp, unsigned long addr,
-			  unsigned long len, unsigned long pgoff,
-			  unsigned long flags, vm_flags_t vm_flags);
+generic_get_unmapped_area(struct mm_struct *mm, struct file *filp,
+			  unsigned long addr, unsigned long len,
+			  unsigned long pgoff, unsigned long flags,
+			  vm_flags_t vm_flags);
 unsigned long
-generic_get_unmapped_area_topdown(struct file *filp, unsigned long addr,
-				  unsigned long len, unsigned long pgoff,
-				  unsigned long flags, vm_flags_t vm_flags);
+generic_get_unmapped_area_topdown(struct mm_struct *mm, struct file *filp,
+				  unsigned long addr, unsigned long len,
+				  unsigned long pgoff, unsigned long flags,
+				  vm_flags_t vm_flags);
 #else
 static inline void arch_pick_mmap_layout(struct mm_struct *mm,
 					 const struct rlimit *rlim_stack) {}
diff --git a/include/linux/shmem_fs.h b/include/linux/shmem_fs.h
index e729b9b0e38d..3912836b375f 100644
--- a/include/linux/shmem_fs.h
+++ b/include/linux/shmem_fs.h
@@ -109,8 +109,9 @@ extern struct file *shmem_file_setup_with_mnt(struct vfsmount *mnt,
 		const char *name, loff_t size, vma_flags_t flags);
 int shmem_zero_setup(struct vm_area_struct *vma);
 int shmem_zero_setup_desc(struct vm_area_desc *desc);
-extern unsigned long shmem_get_unmapped_area(struct file *, unsigned long addr,
-		unsigned long len, unsigned long pgoff, unsigned long flags);
+extern unsigned long shmem_get_unmapped_area(struct mm_struct *mm,
+		struct file *file, unsigned long addr, unsigned long len,
+		unsigned long pgoff, unsigned long flags);
 extern int shmem_lock(struct file *file, int lock, struct ucounts *ucounts);
 #ifdef CONFIG_SHMEM
 bool shmem_mapping(const struct address_space *mapping);
diff --git a/io_uring/memmap.c b/io_uring/memmap.c
index 23e8a85111bc..255fd80fd2f2 100644
--- a/io_uring/memmap.c
+++ b/io_uring/memmap.c
@@ -318,7 +318,8 @@ __cold int io_uring_mmap(struct file *file, struct vm_area_struct *vma)
 	return io_region_mmap(ctx, region, vma, page_limit);
 }
 
-unsigned long io_uring_get_unmapped_area(struct file *filp, unsigned long addr,
+unsigned long io_uring_get_unmapped_area(struct mm_struct *mm,
+					 struct file *filp, unsigned long addr,
 					 unsigned long len, unsigned long pgoff,
 					 unsigned long flags)
 {
@@ -361,7 +362,7 @@ unsigned long io_uring_get_unmapped_area(struct file *filp, unsigned long addr,
 #else
 	addr = 0UL;
 #endif
-	return mm_get_unmapped_area(filp, addr, len, pgoff, flags);
+	return mm_get_unmapped_area(mm, filp, addr, len, pgoff, flags);
 }
 
 #else /* !CONFIG_MMU */
@@ -420,7 +421,8 @@ unsigned int io_uring_nommu_mmap_capabilities(struct file *file)
 	return NOMMU_MAP_DIRECT | NOMMU_MAP_READ | NOMMU_MAP_WRITE;
 }
 
-unsigned long io_uring_get_unmapped_area(struct file *file, unsigned long addr,
+unsigned long io_uring_get_unmapped_area(struct mm_struct *mm,
+					 struct file *file, unsigned long addr,
 					 unsigned long len, unsigned long pgoff,
 					 unsigned long flags)
 {
diff --git a/io_uring/memmap.h b/io_uring/memmap.h
index f4cfbb6b9a1f..40104ed26dbe 100644
--- a/io_uring/memmap.h
+++ b/io_uring/memmap.h
@@ -12,7 +12,8 @@ struct page **io_pin_pages(unsigned long uaddr, unsigned long len, int *npages);
 #ifndef CONFIG_MMU
 unsigned int io_uring_nommu_mmap_capabilities(struct file *file);
 #endif
-unsigned long io_uring_get_unmapped_area(struct file *file, unsigned long addr,
+unsigned long io_uring_get_unmapped_area(struct mm_struct *mm,
+					 struct file *file, unsigned long addr,
 					 unsigned long len, unsigned long pgoff,
 					 unsigned long flags);
 int io_uring_mmap(struct file *file, struct vm_area_struct *vma);
diff --git a/ipc/shm.c b/ipc/shm.c
index b3e8a58e177d..c49e1461ff9c 100644
--- a/ipc/shm.c
+++ b/ipc/shm.c
@@ -649,13 +649,13 @@ static long shm_fallocate(struct file *file, int mode, loff_t offset,
 	return sfd->file->f_op->fallocate(file, mode, offset, len);
 }
 
-static unsigned long shm_get_unmapped_area(struct file *file,
-	unsigned long addr, unsigned long len, unsigned long pgoff,
-	unsigned long flags)
+static unsigned long shm_get_unmapped_area(struct mm_struct *mm,
+	struct file *file, unsigned long addr, unsigned long len,
+	unsigned long pgoff, unsigned long flags)
 {
 	struct shm_file_data *sfd = shm_file_data(file);
 
-	return sfd->file->f_op->get_unmapped_area(sfd->file, addr, len,
+	return sfd->file->f_op->get_unmapped_area(mm, sfd->file, addr, len,
 						pgoff, flags);
 }
 
diff --git a/kernel/bpf/arena.c b/kernel/bpf/arena.c
index 80b7b8a69446..839ebaae301d 100644
--- a/kernel/bpf/arena.c
+++ b/kernel/bpf/arena.c
@@ -543,7 +543,8 @@ static const struct vm_operations_struct arena_vm_ops = {
 	.fault          = arena_vm_fault,
 };
 
-static unsigned long arena_get_unmapped_area(struct file *filp, unsigned long addr,
+static unsigned long arena_get_unmapped_area(struct mm_struct *mm,
+					     struct file *filp, unsigned long addr,
 					     unsigned long len, unsigned long pgoff,
 					     unsigned long flags)
 {
@@ -566,7 +567,7 @@ static unsigned long arena_get_unmapped_area(struct file *filp, unsigned long ad
 			return -EINVAL;
 	}
 
-	ret = mm_get_unmapped_area(filp, addr, len * 2, 0, flags);
+	ret = mm_get_unmapped_area(mm, filp, addr, len * 2, 0, flags);
 	if (IS_ERR_VALUE(ret))
 		return ret;
 	if ((ret >> 32) == ((ret + len - 1) >> 32))
diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
index 6db306d23b47..5092fc8707c5 100644
--- a/kernel/bpf/syscall.c
+++ b/kernel/bpf/syscall.c
@@ -1150,16 +1150,18 @@ static __poll_t bpf_map_poll(struct file *filp, struct poll_table_struct *pts)
 	return EPOLLERR;
 }
 
-static unsigned long bpf_get_unmapped_area(struct file *filp, unsigned long addr,
+static unsigned long bpf_get_unmapped_area(struct mm_struct *mm,
+					   struct file *filp, unsigned long addr,
 					   unsigned long len, unsigned long pgoff,
 					   unsigned long flags)
 {
 	struct bpf_map *map = filp->private_data;
 
 	if (map->ops->map_get_unmapped_area)
-		return map->ops->map_get_unmapped_area(filp, addr, len, pgoff, flags);
+		return map->ops->map_get_unmapped_area(mm, filp, addr, len,
+						       pgoff, flags);
 #ifdef CONFIG_MMU
-	return mm_get_unmapped_area(filp, addr, len, pgoff, flags);
+	return mm_get_unmapped_area(mm, filp, addr, len, pgoff, flags);
 #else
 	return addr;
 #endif
diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index 2bccb0a53a0a..708faf4a0e37 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -1193,8 +1193,8 @@ static inline bool is_transparent_hugepage(const struct folio *folio)
 		folio_test_large_rmappable(folio);
 }
 
-static unsigned long __thp_get_unmapped_area(struct file *filp,
-		unsigned long addr, unsigned long len,
+static unsigned long __thp_get_unmapped_area(struct mm_struct *mm,
+		struct file *filp, unsigned long addr, unsigned long len,
 		loff_t off, unsigned long flags, unsigned long size,
 		vm_flags_t vm_flags)
 {
@@ -1212,7 +1212,7 @@ static unsigned long __thp_get_unmapped_area(struct file *filp,
 	if (len_pad < len || (off + len_pad) < off)
 		return 0;
 
-	ret = mm_get_unmapped_area_vmflags(filp, addr, len_pad,
+	ret = mm_get_unmapped_area_vmflags(mm, filp, addr, len_pad,
 					   off >> PAGE_SHIFT, flags, vm_flags);
 
 	/*
@@ -1231,32 +1231,35 @@ static unsigned long __thp_get_unmapped_area(struct file *filp,
 
 	off_sub = (off - ret) & (size - 1);
 
-	if (mm_flags_test(MMF_TOPDOWN, current->mm) && !off_sub)
+	if (mm_flags_test(MMF_TOPDOWN, mm) && !off_sub)
 		return ret + size;
 
 	ret += off_sub;
 	return ret;
 }
 
-unsigned long thp_get_unmapped_area_vmflags(struct file *filp, unsigned long addr,
-		unsigned long len, unsigned long pgoff, unsigned long flags,
-		vm_flags_t vm_flags)
+unsigned long thp_get_unmapped_area_vmflags(struct mm_struct *mm,
+		struct file *filp, unsigned long addr, unsigned long len,
+		unsigned long pgoff, unsigned long flags, vm_flags_t vm_flags)
 {
 	unsigned long ret;
 	loff_t off = (loff_t)pgoff << PAGE_SHIFT;
 
-	ret = __thp_get_unmapped_area(filp, addr, len, off, flags, PMD_SIZE, vm_flags);
+	ret = __thp_get_unmapped_area(mm, filp, addr, len, off, flags,
+				      PMD_SIZE, vm_flags);
 	if (ret)
 		return ret;
 
-	return mm_get_unmapped_area_vmflags(filp, addr, len, pgoff, flags,
+	return mm_get_unmapped_area_vmflags(mm, filp, addr, len, pgoff, flags,
 					    vm_flags);
 }
 
-unsigned long thp_get_unmapped_area(struct file *filp, unsigned long addr,
-		unsigned long len, unsigned long pgoff, unsigned long flags)
+unsigned long thp_get_unmapped_area(struct mm_struct *mm, struct file *filp,
+		unsigned long addr, unsigned long len, unsigned long pgoff,
+		unsigned long flags)
 {
-	return thp_get_unmapped_area_vmflags(filp, addr, len, pgoff, flags, 0);
+	return thp_get_unmapped_area_vmflags(mm, filp, addr, len, pgoff,
+					     flags, 0);
 }
 EXPORT_SYMBOL_GPL(thp_get_unmapped_area);
 
diff --git a/mm/mmap.c b/mm/mmap.c
index 2311ae7c2ff4..54915ac478ab 100644
--- a/mm/mmap.c
+++ b/mm/mmap.c
@@ -405,7 +405,7 @@ unsigned long do_mmap(struct file *file, unsigned long addr,
 	/* Obtain the address to map to. we verify (or select) it and ensure
 	 * that it represents a valid section of the address space.
 	 */
-	addr = __get_unmapped_area(file, addr, len, pgoff, flags, vm_flags);
+	addr = __get_unmapped_area(mm, file, addr, len, pgoff, flags, vm_flags);
 	if (IS_ERR_VALUE(addr))
 		return addr;
 
@@ -662,14 +662,15 @@ static inline unsigned long stack_guard_placement(vm_flags_t vm_flags)
  * - is at least the desired size.
  * - satisfies (begin_addr & align_mask) == (align_offset & align_mask)
  */
-unsigned long vm_unmapped_area(struct vm_unmapped_area_info *info)
+unsigned long vm_unmapped_area(struct mm_struct *mm,
+			       struct vm_unmapped_area_info *info)
 {
 	unsigned long addr;
 
 	if (info->flags & VM_UNMAPPED_AREA_TOPDOWN)
-		addr = unmapped_area_topdown(info);
+		addr = unmapped_area_topdown(mm, info);
 	else
-		addr = unmapped_area(info);
+		addr = unmapped_area(mm, info);
 
 	trace_vm_unmapped_area(addr, info);
 	return addr;
@@ -687,11 +688,11 @@ unsigned long vm_unmapped_area(struct vm_unmapped_area_info *info)
  * This function "knows" that -ENOMEM has the bits set.
  */
 unsigned long
-generic_get_unmapped_area(struct file *filp, unsigned long addr,
-			  unsigned long len, unsigned long pgoff,
-			  unsigned long flags, vm_flags_t vm_flags)
+generic_get_unmapped_area(struct mm_struct *mm, struct file *filp,
+			  unsigned long addr, unsigned long len,
+			  unsigned long pgoff, unsigned long flags,
+			  vm_flags_t vm_flags)
 {
-	struct mm_struct *mm = current->mm;
 	struct vm_area_struct *vma, *prev;
 	struct vm_unmapped_area_info info = {};
 	const unsigned long mmap_end = arch_get_mmap_end(addr, len, flags);
@@ -717,16 +718,17 @@ generic_get_unmapped_area(struct file *filp, unsigned long addr,
 	info.start_gap = stack_guard_placement(vm_flags);
 	if (filp && is_file_hugepages(filp))
 		info.align_mask = huge_page_mask_align(filp);
-	return vm_unmapped_area(&info);
+	return vm_unmapped_area(mm, &info);
 }
 
 #ifndef HAVE_ARCH_UNMAPPED_AREA
 unsigned long
-arch_get_unmapped_area(struct file *filp, unsigned long addr,
-		       unsigned long len, unsigned long pgoff,
-		       unsigned long flags, vm_flags_t vm_flags)
+arch_get_unmapped_area(struct mm_struct *mm, struct file *filp,
+		       unsigned long addr, unsigned long len,
+		       unsigned long pgoff, unsigned long flags,
+		       vm_flags_t vm_flags)
 {
-	return generic_get_unmapped_area(filp, addr, len, pgoff, flags,
+	return generic_get_unmapped_area(mm, filp, addr, len, pgoff, flags,
 					 vm_flags);
 }
 #endif
@@ -736,12 +738,12 @@ arch_get_unmapped_area(struct file *filp, unsigned long addr,
  * stack's low limit (the base):
  */
 unsigned long
-generic_get_unmapped_area_topdown(struct file *filp, unsigned long addr,
-				  unsigned long len, unsigned long pgoff,
-				  unsigned long flags, vm_flags_t vm_flags)
+generic_get_unmapped_area_topdown(struct mm_struct *mm, struct file *filp,
+				  unsigned long addr, unsigned long len,
+				  unsigned long pgoff, unsigned long flags,
+				  vm_flags_t vm_flags)
 {
 	struct vm_area_struct *vma, *prev;
-	struct mm_struct *mm = current->mm;
 	struct vm_unmapped_area_info info = {};
 	const unsigned long mmap_end = arch_get_mmap_end(addr, len, flags);
 
@@ -769,7 +771,7 @@ generic_get_unmapped_area_topdown(struct file *filp, unsigned long addr,
 	info.start_gap = stack_guard_placement(vm_flags);
 	if (filp && is_file_hugepages(filp))
 		info.align_mask = huge_page_mask_align(filp);
-	addr = vm_unmapped_area(&info);
+	addr = vm_unmapped_area(mm, &info);
 
 	/*
 	 * A failed mmap() very likely causes application failure,
@@ -782,7 +784,7 @@ generic_get_unmapped_area_topdown(struct file *filp, unsigned long addr,
 		info.flags = 0;
 		info.low_limit = TASK_UNMAPPED_BASE;
 		info.high_limit = mmap_end;
-		addr = vm_unmapped_area(&info);
+		addr = vm_unmapped_area(mm, &info);
 	}
 
 	return addr;
@@ -790,31 +792,36 @@ generic_get_unmapped_area_topdown(struct file *filp, unsigned long addr,
 
 #ifndef HAVE_ARCH_UNMAPPED_AREA_TOPDOWN
 unsigned long
-arch_get_unmapped_area_topdown(struct file *filp, unsigned long addr,
-			       unsigned long len, unsigned long pgoff,
-			       unsigned long flags, vm_flags_t vm_flags)
+arch_get_unmapped_area_topdown(struct mm_struct *mm, struct file *filp,
+			       unsigned long addr, unsigned long len,
+			       unsigned long pgoff, unsigned long flags,
+			       vm_flags_t vm_flags)
 {
-	return generic_get_unmapped_area_topdown(filp, addr, len, pgoff, flags,
-						 vm_flags);
+	return generic_get_unmapped_area_topdown(mm, filp, addr, len, pgoff,
+						 flags, vm_flags);
 }
 #endif
 
-unsigned long mm_get_unmapped_area_vmflags(struct file *filp, unsigned long addr,
+unsigned long mm_get_unmapped_area_vmflags(struct mm_struct *mm,
+					   struct file *filp, unsigned long addr,
 					   unsigned long len, unsigned long pgoff,
 					   unsigned long flags, vm_flags_t vm_flags)
 {
-	if (mm_flags_test(MMF_TOPDOWN, current->mm))
-		return arch_get_unmapped_area_topdown(filp, addr, len, pgoff,
-						      flags, vm_flags);
-	return arch_get_unmapped_area(filp, addr, len, pgoff, flags, vm_flags);
+	if (mm_flags_test(MMF_TOPDOWN, mm))
+		return arch_get_unmapped_area_topdown(mm, filp, addr, len,
+						      pgoff, flags, vm_flags);
+	return arch_get_unmapped_area(mm, filp, addr, len, pgoff, flags,
+				      vm_flags);
 }
 
 unsigned long
-__get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,
+__get_unmapped_area(struct mm_struct *mm, struct file *file,
+		unsigned long addr, unsigned long len,
 		unsigned long pgoff, unsigned long flags, vm_flags_t vm_flags)
 {
-	unsigned long (*get_area)(struct file *, unsigned long,
-				  unsigned long, unsigned long, unsigned long)
+	unsigned long (*get_area)(struct mm_struct *, struct file *,
+				  unsigned long, unsigned long,
+				  unsigned long, unsigned long)
 				  = NULL;
 
 	unsigned long error = arch_mmap_check(addr, len, flags);
@@ -841,15 +848,15 @@ __get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,
 		pgoff = 0;
 
 	if (get_area) {
-		addr = get_area(file, addr, len, pgoff, flags);
+		addr = get_area(mm, file, addr, len, pgoff, flags);
 	} else if (IS_ENABLED(CONFIG_TRANSPARENT_HUGEPAGE) && !file
 		   && !addr /* no hint */
 		   && IS_ALIGNED(len, PMD_SIZE)) {
 		/* Ensures that larger anonymous mappings are THP aligned. */
-		addr = thp_get_unmapped_area_vmflags(file, addr, len,
+		addr = thp_get_unmapped_area_vmflags(mm, file, addr, len,
 						     pgoff, flags, vm_flags);
 	} else {
-		addr = mm_get_unmapped_area_vmflags(file, addr, len,
+		addr = mm_get_unmapped_area_vmflags(mm, file, addr, len,
 						    pgoff, flags, vm_flags);
 	}
 	if (IS_ERR_VALUE(addr))
@@ -865,10 +872,12 @@ __get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,
 }
 
 unsigned long
-mm_get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,
+mm_get_unmapped_area(struct mm_struct *mm, struct file *file,
+		     unsigned long addr, unsigned long len,
 		     unsigned long pgoff, unsigned long flags)
 {
-	return mm_get_unmapped_area_vmflags(file, addr, len, pgoff, flags, 0);
+	return mm_get_unmapped_area_vmflags(mm, file, addr, len, pgoff,
+					    flags, 0);
 }
 EXPORT_SYMBOL(mm_get_unmapped_area);
 
diff --git a/mm/nommu.c b/mm/nommu.c
index ed3934bc2de4..d6393bd2844e 100644
--- a/mm/nommu.c
+++ b/mm/nommu.c
@@ -1145,8 +1145,9 @@ unsigned long do_mmap(struct file *file,
 		 *   tell us the location of a shared mapping
 		 */
 		if (capabilities & NOMMU_MAP_DIRECT) {
-			addr = file->f_op->get_unmapped_area(file, addr, len,
-							     pgoff, flags);
+			addr = file->f_op->get_unmapped_area(current->mm, file,
+							     addr, len, pgoff,
+							     flags);
 			if (IS_ERR_VALUE(addr)) {
 				ret = addr;
 				if (ret != -ENOSYS)
diff --git a/mm/shmem.c b/mm/shmem.c
index b51f83c970bb..1349e00bc7fd 100644
--- a/mm/shmem.c
+++ b/mm/shmem.c
@@ -2711,7 +2711,8 @@ static vm_fault_t shmem_fault(struct vm_fault *vmf)
 	return ret;
 }
 
-unsigned long shmem_get_unmapped_area(struct file *file,
+unsigned long shmem_get_unmapped_area(struct mm_struct *mm,
+				      struct file *file,
 				      unsigned long uaddr, unsigned long len,
 				      unsigned long pgoff, unsigned long flags)
 {
@@ -2725,7 +2726,7 @@ unsigned long shmem_get_unmapped_area(struct file *file,
 	if (len > TASK_SIZE)
 		return -ENOMEM;
 
-	addr = mm_get_unmapped_area(file, uaddr, len, pgoff, flags);
+	addr = mm_get_unmapped_area(mm, file, uaddr, len, pgoff, flags);
 
 	if (!IS_ENABLED(CONFIG_TRANSPARENT_HUGEPAGE))
 		return addr;
@@ -2803,7 +2804,8 @@ unsigned long shmem_get_unmapped_area(struct file *file,
 	if (inflated_len < len)
 		return addr;
 
-	inflated_addr = mm_get_unmapped_area(NULL, uaddr, inflated_len, 0, flags);
+	inflated_addr = mm_get_unmapped_area(mm, NULL, uaddr, inflated_len, 0,
+					     flags);
 	if (IS_ERR_VALUE(inflated_addr))
 		return addr;
 	if (inflated_addr & ~PAGE_MASK)
@@ -5730,11 +5732,12 @@ void shmem_unlock_mapping(struct address_space *mapping)
 }
 
 #ifdef CONFIG_MMU
-unsigned long shmem_get_unmapped_area(struct file *file,
+unsigned long shmem_get_unmapped_area(struct mm_struct *mm,
+				      struct file *file,
 				      unsigned long addr, unsigned long len,
 				      unsigned long pgoff, unsigned long flags)
 {
-	return mm_get_unmapped_area(file, addr, len, pgoff, flags);
+	return mm_get_unmapped_area(mm, file, addr, len, pgoff, flags);
 }
 #endif
 
diff --git a/mm/vma.c b/mm/vma.c
index 9eea2850818a..9a6cb0338ed8 100644
--- a/mm/vma.c
+++ b/mm/vma.c
@@ -2957,20 +2957,22 @@ int do_brk_flags(struct vma_iterator *vmi, struct vm_area_struct *vma,
 
 /**
  * unmapped_area() - Find an area between the low_limit and the high_limit with
- * the correct alignment and offset, all from @info. Note: current->mm is used
+ * the correct alignment and offset, all from @info. Note: @mm is used
  * for the search.
  *
+ * @mm: The mm_struct to search.
  * @info: The unmapped area information including the range [low_limit -
  * high_limit), the alignment offset and mask.
  *
  * Return: A memory address or -ENOMEM.
  */
-unsigned long unmapped_area(struct vm_unmapped_area_info *info)
+unsigned long unmapped_area(struct mm_struct *mm,
+			    struct vm_unmapped_area_info *info)
 {
 	unsigned long length, gap;
 	unsigned long low_limit, high_limit;
 	struct vm_area_struct *tmp;
-	VMA_ITERATOR(vmi, current->mm, 0);
+	VMA_ITERATOR(vmi, mm, 0);
 
 	/* Adjust search length to account for worst case alignment overhead */
 	length = info->length + info->align_mask + info->start_gap;
@@ -3016,19 +3018,21 @@ unsigned long unmapped_area(struct vm_unmapped_area_info *info)
 /**
  * unmapped_area_topdown() - Find an area between the low_limit and the
  * high_limit with the correct alignment and offset at the highest available
- * address, all from @info. Note: current->mm is used for the search.
+ * address, all from @info. Note: @mm is used for the search.
  *
+ * @mm: The mm_struct to search.
  * @info: The unmapped area information including the range [low_limit -
  * high_limit), the alignment offset and mask.
  *
  * Return: A memory address or -ENOMEM.
  */
-unsigned long unmapped_area_topdown(struct vm_unmapped_area_info *info)
+unsigned long unmapped_area_topdown(struct mm_struct *mm,
+				    struct vm_unmapped_area_info *info)
 {
 	unsigned long length, gap, gap_end;
 	unsigned long low_limit, high_limit;
 	struct vm_area_struct *tmp;
-	VMA_ITERATOR(vmi, current->mm, 0);
+	VMA_ITERATOR(vmi, mm, 0);
 
 	/* Adjust search length to account for worst case alignment overhead */
 	length = info->length + info->align_mask + info->start_gap;
diff --git a/mm/vma.h b/mm/vma.h
index 8e4b61a7304c..d0a45718deb9 100644
--- a/mm/vma.h
+++ b/mm/vma.h
@@ -467,8 +467,10 @@ int do_brk_flags(struct vma_iterator *vmi, struct vm_area_struct *brkvma,
 		 unsigned long addr, unsigned long request,
 		 vma_flags_t vma_flags);
 
-unsigned long unmapped_area(struct vm_unmapped_area_info *info);
-unsigned long unmapped_area_topdown(struct vm_unmapped_area_info *info);
+unsigned long unmapped_area(struct mm_struct *mm,
+			    struct vm_unmapped_area_info *info);
+unsigned long unmapped_area_topdown(struct mm_struct *mm,
+				    struct vm_unmapped_area_info *info);
 
 static inline bool vma_wants_manual_pte_write_upgrade(struct vm_area_struct *vma)
 {
diff --git a/sound/core/pcm_native.c b/sound/core/pcm_native.c
index 7dc0060617f1..f01465a70ddd 100644
--- a/sound/core/pcm_native.c
+++ b/sound/core/pcm_native.c
@@ -4202,7 +4202,8 @@ static int snd_pcm_hw_params_old_user(struct snd_pcm_substream *substream,
 #endif /* CONFIG_SND_SUPPORT_OLD_API */
 
 #ifndef CONFIG_MMU
-static unsigned long snd_pcm_get_unmapped_area(struct file *file,
+static unsigned long snd_pcm_get_unmapped_area(struct mm_struct *mm,
+					       struct file *file,
 					       unsigned long addr,
 					       unsigned long len,
 					       unsigned long pgoff,
-- 
2.43.0



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

* [PATCH v6 2/8] mm: add __do_mmap() and vm_mmap_remote()/vm_munmap_remote()
  2026-07-15 21:19 [PATCH v6 0/8] seccomp: non-cooperative pinned-memfd argument redirect Cong Wang
  2026-07-15 21:20 ` [PATCH v6 1/8] mm: pass the target mm parameter through get_unmapped_area family Cong Wang
@ 2026-07-15 21:20 ` Cong Wang
  2026-07-15 21:20 ` [PATCH v6 3/8] seccomp: introduce SECCOMP_IOCTL_NOTIF_PIN_INSTALL Cong Wang
                   ` (5 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: Cong Wang @ 2026-07-15 21:20 UTC (permalink / raw)
  To: Andy Lutomirski
  Cc: Kees Cook, linux-kernel, Will Drewry, Christian Brauner,
	Andrew Morton, linux-mm, Cong Wang

From: Cong Wang <cwang@multikernel.io>

Add __do_mmap(), a variant of do_mmap() that installs the mapping into
a caller-supplied mm rather than current->mm; do_mmap() becomes a
wrapper passing current->mm and keeps the current-task READ_IMPLIES_EXEC
handling. mmap_region()/__mmap_region() gain an mm argument so the
target mm reaches VMA insertion.

On top of that, add vm_mmap_remote() and vm_munmap_remote() in mm/util.c:
entry points that install / remove a mapping in a caller-specified mm
without target-side cooperation. The intended user is the seccomp
unotify syscall redirect.

Kernel-chosen placement resolves through __get_unmapped_area() against
the target mm, so (via the previous patch) the file's own
->get_unmapped_area() runs and file-specific placement is honored. The
result is bounds-checked against the target's mm->task_size, and the
munmap machinery is likewise threaded with the target mm so counter
bookkeeping and range checks apply to it rather than current->mm.
vm_munmap_remote() delivers the target's UFFD_EVENT_UNMAP.

Both are MMU-only and guarded with CONFIG_MMU, with -EOPNOTSUPP stubs so
NOMMU keeps linking. LSM/fsnotify hooks run against current, the
supervisor, paralleling pidfd_getfd(); cross-task authorization is left
to the caller.

Assisted-by: Claude:claude-opus-4.8
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
 include/linux/mm.h |  19 ++++++++
 mm/internal.h      |   6 +++
 mm/mmap.c          |  49 +++++++++++++--------
 mm/mprotect.c      |   2 +-
 mm/nommu.c         |  22 +++++++---
 mm/util.c          | 106 +++++++++++++++++++++++++++++++++++++++++++++
 mm/vma.c           |  46 ++++++++++++--------
 mm/vma.h           |  12 ++---
 8 files changed, 212 insertions(+), 50 deletions(-)

diff --git a/include/linux/mm.h b/include/linux/mm.h
index 560bc369a54c..fa1d96bc379d 100644
--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -4155,6 +4155,25 @@ extern unsigned long do_mmap(struct file *file, unsigned long addr,
 	unsigned long len, unsigned long prot, unsigned long flags,
 	vm_flags_t vm_flags, unsigned long pgoff, unsigned long *populate,
 	struct list_head *uf);
+#ifdef CONFIG_MMU
+unsigned long vm_mmap_remote(struct mm_struct *mm, struct file *file,
+	unsigned long addr, unsigned long len, unsigned long prot,
+	unsigned long flags, unsigned long pgoff, vm_flags_t vm_flags);
+int vm_munmap_remote(struct mm_struct *mm, unsigned long start, size_t len);
+#else
+static inline unsigned long vm_mmap_remote(struct mm_struct *mm,
+	struct file *file, unsigned long addr, unsigned long len,
+	unsigned long prot, unsigned long flags, unsigned long pgoff,
+	vm_flags_t vm_flags)
+{
+	return -EOPNOTSUPP;
+}
+static inline int vm_munmap_remote(struct mm_struct *mm, unsigned long start,
+	size_t len)
+{
+	return -EOPNOTSUPP;
+}
+#endif
 extern int do_vmi_munmap(struct vma_iterator *vmi, struct mm_struct *mm,
 			 unsigned long start, size_t len, struct list_head *uf,
 			 bool unlock);
diff --git a/mm/internal.h b/mm/internal.h
index 181e79f1d6a2..fd3d39f6b4c4 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -1436,6 +1436,12 @@ extern unsigned long  __must_check vm_mmap_pgoff(struct file *, unsigned long,
         unsigned long, unsigned long,
         unsigned long, unsigned long);
 
+unsigned long __do_mmap(struct mm_struct *mm, struct file *file,
+	unsigned long addr, unsigned long len, unsigned long prot,
+	unsigned long flags, vm_flags_t vm_flags, unsigned long pgoff,
+	unsigned long *populate, struct list_head *uf);
+
+
 extern void set_pageblock_order(void);
 unsigned long reclaim_pages(struct list_head *folio_list);
 unsigned int reclaim_clean_pages_from_list(struct zone *zone,
diff --git a/mm/mmap.c b/mm/mmap.c
index 54915ac478ab..f379a065a8e6 100644
--- a/mm/mmap.c
+++ b/mm/mmap.c
@@ -277,7 +277,7 @@ static inline bool file_mmap_ok(struct file *file, struct inode *inode,
 }
 
 /**
- * do_mmap() - Perform a userland memory mapping into the current process
+ * __do_mmap() - Perform a userland memory mapping into @mm's
  * address space of length @len with protection bits @prot, mmap flags @flags
  * (from which VMA flags will be inferred), and any additional VMA flags to
  * apply @vm_flags. If this is a file-backed mapping then the file is specified
@@ -307,8 +307,11 @@ static inline bool file_mmap_ok(struct file *file, struct inode *inode,
  * start of a VMA, rather only the start of a valid mapped range of length
  * @len bytes, rounded down to the nearest page size.
  *
- * The caller must write-lock current->mm->mmap_lock.
+ * The caller must write-lock @mm->mmap_lock. do_mmap() is the common
+ * wrapper that targets current->mm.
  *
+ * @mm: The mm_struct to install the mapping into. The caller must hold a
+ * reference and write-lock its mmap_lock.
  * @file: An optional struct file pointer describing the file which is to be
  * mapped, if a file-backed mapping.
  * @addr: If non-zero, hints at (or if @flags has MAP_FIXED set, specifies) the
@@ -333,13 +336,12 @@ static inline bool file_mmap_ok(struct file *file, struct inode *inode,
  * Returns: Either an error, or the address at which the requested mapping has
  * been performed.
  */
-unsigned long do_mmap(struct file *file, unsigned long addr,
-			unsigned long len, unsigned long prot,
-			unsigned long flags, vm_flags_t vm_flags,
-			unsigned long pgoff, unsigned long *populate,
-			struct list_head *uf)
+unsigned long __do_mmap(struct mm_struct *mm, struct file *file,
+			unsigned long addr, unsigned long len,
+			unsigned long prot, unsigned long flags,
+			vm_flags_t vm_flags, unsigned long pgoff,
+			unsigned long *populate, struct list_head *uf)
 {
-	struct mm_struct *mm = current->mm;
 	int pkey = 0;
 
 	*populate = 0;
@@ -349,16 +351,6 @@ unsigned long do_mmap(struct file *file, unsigned long addr,
 	if (!len)
 		return -EINVAL;
 
-	/*
-	 * Does the application expect PROT_READ to imply PROT_EXEC?
-	 *
-	 * (the exception is when the underlying filesystem is noexec
-	 *  mounted, in which case we don't add PROT_EXEC.)
-	 */
-	if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
-		if (!(file && path_noexec(&file->f_path)))
-			prot |= PROT_EXEC;
-
 	/* force arch specific MAP_FIXED handling in get_unmapped_area */
 	if (flags & MAP_FIXED_NOREPLACE)
 		flags |= MAP_FIXED;
@@ -557,7 +549,7 @@ unsigned long do_mmap(struct file *file, unsigned long addr,
 			vm_flags |= VM_NORESERVE;
 	}
 
-	addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);
+	addr = mmap_region(mm, file, addr, len, vm_flags, pgoff, uf);
 	if (!IS_ERR_VALUE(addr) &&
 	    ((vm_flags & VM_LOCKED) ||
 	     (flags & (MAP_POPULATE | MAP_NONBLOCK)) == MAP_POPULATE))
@@ -565,6 +557,25 @@ unsigned long do_mmap(struct file *file, unsigned long addr,
 	return addr;
 }
 
+unsigned long do_mmap(struct file *file, unsigned long addr, unsigned long len,
+		      unsigned long prot, unsigned long flags,
+		      vm_flags_t vm_flags, unsigned long pgoff,
+		      unsigned long *populate, struct list_head *uf)
+{
+	/*
+	 * Does the application expect PROT_READ to imply PROT_EXEC?
+	 *
+	 * (the exception is when the underlying filesystem is noexec
+	 *  mounted, in which case we don't add PROT_EXEC.)
+	 */
+	if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
+		if (!(file && path_noexec(&file->f_path)))
+			prot |= PROT_EXEC;
+
+	return __do_mmap(current->mm, file, addr, len, prot, flags, vm_flags,
+			 pgoff, populate, uf);
+}
+
 unsigned long ksys_mmap_pgoff(unsigned long addr, unsigned long len,
 			      unsigned long prot, unsigned long flags,
 			      unsigned long fd, unsigned long pgoff)
diff --git a/mm/mprotect.c b/mm/mprotect.c
index 9cbf932b028c..02e90577db25 100644
--- a/mm/mprotect.c
+++ b/mm/mprotect.c
@@ -939,7 +939,7 @@ static int do_mprotect_pkey(unsigned long start, size_t len,
 			break;
 		}
 
-		if (map_deny_write_exec(&vma->flags, &new_vma_flags)) {
+		if (map_deny_write_exec(vma->vm_mm, &vma->flags, &new_vma_flags)) {
 			error = -EACCES;
 			break;
 		}
diff --git a/mm/nommu.c b/mm/nommu.c
index d6393bd2844e..15390ef5b784 100644
--- a/mm/nommu.c
+++ b/mm/nommu.c
@@ -1009,7 +1009,8 @@ static int do_mmap_private(struct vm_area_struct *vma,
 /*
  * handle mapping creation for uClinux
  */
-unsigned long do_mmap(struct file *file,
+unsigned long __do_mmap(struct mm_struct *mm,
+			struct file *file,
 			unsigned long addr,
 			unsigned long len,
 			unsigned long prot,
@@ -1024,7 +1025,7 @@ unsigned long do_mmap(struct file *file,
 	struct rb_node *rb;
 	unsigned long capabilities, result;
 	int ret;
-	VMA_ITERATOR(vmi, current->mm, 0);
+	VMA_ITERATOR(vmi, mm, 0);
 
 	*populate = 0;
 
@@ -1049,7 +1050,7 @@ unsigned long do_mmap(struct file *file,
 	if (!region)
 		goto error_getting_region;
 
-	vma = vm_area_alloc(current->mm);
+	vma = vm_area_alloc(mm);
 	if (!vma)
 		goto error_getting_vma;
 
@@ -1191,7 +1192,7 @@ unsigned long do_mmap(struct file *file,
 	/* okay... we have a mapping; now we have to register it */
 	result = vma->vm_start;
 
-	current->mm->total_vm += len >> PAGE_SHIFT;
+	mm->total_vm += len >> PAGE_SHIFT;
 
 share:
 	BUG_ON(!vma->vm_region);
@@ -1199,8 +1200,8 @@ unsigned long do_mmap(struct file *file,
 	if (vma_iter_prealloc(&vmi, vma))
 		goto error_just_free;
 
-	setup_vma_to_mm(vma, current->mm);
-	current->mm->map_count++;
+	setup_vma_to_mm(vma, mm);
+	mm->map_count++;
 	/* add the VMA to the tree */
 	vma_iter_store_new(&vmi, vma);
 
@@ -1247,6 +1248,15 @@ unsigned long do_mmap(struct file *file,
 	return -ENOMEM;
 }
 
+unsigned long do_mmap(struct file *file, unsigned long addr, unsigned long len,
+		      unsigned long prot, unsigned long flags,
+		      vm_flags_t vm_flags, unsigned long pgoff,
+		      unsigned long *populate, struct list_head *uf)
+{
+	return __do_mmap(current->mm, file, addr, len, prot, flags, vm_flags, pgoff,
+			 populate, uf);
+}
+
 unsigned long ksys_mmap_pgoff(unsigned long addr, unsigned long len,
 			      unsigned long prot, unsigned long flags,
 			      unsigned long fd, unsigned long pgoff)
diff --git a/mm/util.c b/mm/util.c
index af2c2103f0d9..2b5254106d15 100644
--- a/mm/util.c
+++ b/mm/util.c
@@ -588,6 +588,112 @@ unsigned long vm_mmap_pgoff(struct file *file, unsigned long addr,
 	return ret;
 }
 
+#ifdef CONFIG_MMU
+/**
+ * vm_mmap_remote - install a file (or anonymous) mapping into @mm, without
+ * target-side cooperation.
+ * @mm: Target mm; caller holds a reference (e.g. get_task_mm()).
+ * @file: Backing file, or NULL for an anonymous mapping.
+ * @addr: Page-aligned address. If non-zero it is used as given (honoring
+ *        MAP_FIXED/MAP_FIXED_NOREPLACE in @flags); if zero, the kernel
+ *        chooses a free area in @mm and returns it.
+ * @len: Length in bytes.
+ * @prot: PROT_* protection bits.
+ * @flags: MAP_* flags.
+ * @pgoff: Page offset into @file.
+ * @vm_flags: Extra VMA flags to OR in (e.g. VM_SEALED), or 0.
+ *
+ * The general form of the remote install primitive: a supervisor places a
+ * mapping into a remote task's address space. LSM/fsnotify hooks run against
+ * %current (the installer), paralleling pidfd_getfd()'s cross-task install;
+ * cross-task authorization is the caller's responsibility. The foreign mm is
+ * not mm_populate()d.
+ *
+ * Kernel-chosen placement (@addr == 0) resolves through
+ * __get_unmapped_area() against @mm, so the file's own
+ * ->get_unmapped_area() runs and file-specific placement (hugetlb page
+ * size, THP PMD alignment, SHMLBA cache coloring, device dax alignment)
+ * is honored exactly as for a local mmap().
+ *
+ * Returns the mapped address on success, or a negative errno.
+ */
+unsigned long vm_mmap_remote(struct mm_struct *mm, struct file *file,
+	unsigned long addr, unsigned long len, unsigned long prot,
+	unsigned long flags, unsigned long pgoff, vm_flags_t vm_flags)
+{
+	loff_t off = (loff_t)pgoff << PAGE_SHIFT;
+	unsigned long aligned_len = PAGE_ALIGN(len);
+	unsigned long ret;
+	unsigned long populate;
+	LIST_HEAD(uf);
+
+	if (WARN_ON_ONCE(!mm))
+		return -EINVAL;
+
+	ret = security_mmap_file(file, prot, flags);
+	if (!ret)
+		ret = fsnotify_mmap_perm(file, prot, off, len);
+	if (ret)
+		return ret;
+
+	if (mmap_write_lock_killable(mm))
+		return -EINTR;
+
+	/*
+	 * Resolve kernel-chosen placement up front and pin it with
+	 * MAP_FIXED, so the mm->task_size check below runs before the
+	 * mapping is installed. The file's own ->get_unmapped_area()
+	 * performs the search against @mm.
+	 */
+	if (!addr) {
+		addr = __get_unmapped_area(mm, file, 0, aligned_len, pgoff,
+					   flags, vm_flags);
+		if (IS_ERR_VALUE(addr)) {
+			ret = addr;
+			goto unlock;
+		}
+	}
+
+	if (aligned_len > mm->task_size || addr > mm->task_size - aligned_len) {
+		ret = -ENOMEM;
+		goto unlock;
+	}
+
+	if (!(flags & MAP_FIXED_NOREPLACE))
+		flags |= MAP_FIXED;
+
+	ret = __do_mmap(mm, file, addr, len, prot, flags, vm_flags,
+			pgoff, &populate, &uf);
+unlock:
+	mmap_write_unlock(mm);
+	userfaultfd_unmap_complete(mm, &uf);
+	return ret;
+}
+
+/**
+ * vm_munmap_remote - unmap a range from @mm, mirroring vm_munmap() but
+ * targeting an explicit mm.
+ * @mm: Target mm; caller holds a reference (e.g. get_task_mm()).
+ * @start: Start address of the range to unmap.
+ * @len: Length in bytes.
+ *
+ * Returns 0 on success, or a negative errno.
+ */
+int vm_munmap_remote(struct mm_struct *mm, unsigned long start, size_t len)
+{
+	VMA_ITERATOR(vmi, mm, start);
+	LIST_HEAD(uf);
+	int ret;
+
+	if (mmap_write_lock_killable(mm))
+		return -EINTR;
+	ret = do_vmi_munmap(&vmi, mm, start, len, &uf, false);
+	mmap_write_unlock(mm);
+	userfaultfd_unmap_complete(mm, &uf);
+	return ret;
+}
+#endif /* CONFIG_MMU */
+
 /*
  * Perform a userland memory mapping into the current process address space. See
  * the comment for do_mmap() for more details on this operation in general.
diff --git a/mm/vma.c b/mm/vma.c
index 9a6cb0338ed8..788c2a0f35ec 100644
--- a/mm/vma.c
+++ b/mm/vma.c
@@ -1333,7 +1333,7 @@ static void vms_complete_munmap_vmas(struct vma_munmap_struct *vms,
 	struct vm_area_struct *vma;
 	struct mm_struct *mm;
 
-	mm = current->mm;
+	mm = vms->mm;
 	mm->map_count -= vms->vma_count;
 	mm->locked_vm -= vms->locked_vm;
 	if (vms->unlock)
@@ -1544,10 +1544,11 @@ static int vms_gather_munmap_vmas(struct vma_munmap_struct *vms,
  * @unlock: Unlock after the operation.  Only unlocked on success
  */
 static void init_vma_munmap(struct vma_munmap_struct *vms,
-		struct vma_iterator *vmi, struct vm_area_struct *vma,
-		unsigned long start, unsigned long end, struct list_head *uf,
-		bool unlock)
+		struct mm_struct *mm, struct vma_iterator *vmi,
+		struct vm_area_struct *vma, unsigned long start,
+		unsigned long end, struct list_head *uf, bool unlock)
 {
+	vms->mm = mm;
 	vms->vmi = vmi;
 	vms->vma = vma;
 	if (vma) {
@@ -1591,7 +1592,7 @@ int do_vmi_align_munmap(struct vma_iterator *vmi, struct vm_area_struct *vma,
 	struct vma_munmap_struct vms;
 	int error;
 
-	init_vma_munmap(&vms, vmi, vma, start, end, uf, unlock);
+	init_vma_munmap(&vms, mm, vmi, vma, start, end, uf, unlock);
 	error = vms_gather_munmap_vmas(&vms, &mas_detach);
 	if (error)
 		goto gather_failed;
@@ -1634,7 +1635,8 @@ int do_vmi_munmap(struct vma_iterator *vmi, struct mm_struct *mm,
 	unsigned long end;
 	struct vm_area_struct *vma;
 
-	if ((offset_in_page(start)) || start > TASK_SIZE || len > TASK_SIZE-start)
+	if ((offset_in_page(start)) || start > mm->task_size ||
+	    len > mm->task_size - start)
 		return -EINVAL;
 
 	end = start + PAGE_ALIGN(len);
@@ -2426,7 +2428,7 @@ static int __mmap_setup(struct mmap_state *map, struct vm_area_desc *desc,
 
 	/* Find the first overlapping VMA and initialise unmap state. */
 	vms->vma = vma_find(vmi, map->end);
-	init_vma_munmap(vms, vmi, vms->vma, map->addr, map->end, uf,
+	init_vma_munmap(vms, map->mm, vmi, vms->vma, map->addr, map->end, uf,
 			/* unlock = */ false);
 
 	/* OK, we have overlapping VMAs - prepare to unmap them. */
@@ -2731,11 +2733,10 @@ static bool can_set_ksm_flags_early(struct mmap_state *map)
 	return false;
 }
 
-static unsigned long __mmap_region(struct file *file, unsigned long addr,
-		unsigned long len, vma_flags_t vma_flags,
+static unsigned long __mmap_region(struct mm_struct *mm, struct file *file,
+		unsigned long addr, unsigned long len, vma_flags_t vma_flags,
 		unsigned long pgoff, struct list_head *uf)
 {
-	struct mm_struct *mm = current->mm;
 	struct vm_area_struct *vma = NULL;
 	bool have_mmap_prepare = file && file->f_op->mmap_prepare;
 	VMA_ITERATOR(vmi, mm, addr);
@@ -2809,14 +2810,16 @@ static unsigned long __mmap_region(struct file *file, unsigned long addr,
 
 /**
  * mmap_region() - Actually perform the userland mapping of a VMA into
- * current->mm with known, aligned and overflow-checked @addr and @len, and
+ * @mm with known, aligned and overflow-checked @addr and @len, and
  * correctly determined VMA flags @vm_flags and page offset @pgoff.
  *
  * This is an internal memory management function, and should not be used
  * directly.
  *
- * The caller must write-lock current->mm->mmap_lock.
+ * The caller must write-lock @mm->mmap_lock.
  *
+ * @mm: The mm_struct to install the mapping into. The caller must hold a
+ * reference and write-lock its mmap_lock.
  * @file: If a file-backed mapping, a pointer to the struct file describing the
  * file to be mapped, otherwise NULL.
  * @addr: The page-aligned address at which to perform the mapping.
@@ -2830,18 +2833,19 @@ static unsigned long __mmap_region(struct file *file, unsigned long addr,
  * Returns: Either an error, or the address at which the requested mapping has
  * been performed.
  */
-unsigned long mmap_region(struct file *file, unsigned long addr,
-			  unsigned long len, vm_flags_t vm_flags,
-			  unsigned long pgoff, struct list_head *uf)
+unsigned long mmap_region(struct mm_struct *mm, struct file *file,
+			  unsigned long addr, unsigned long len,
+			  vm_flags_t vm_flags, unsigned long pgoff,
+			  struct list_head *uf)
 {
 	unsigned long ret;
 	bool writable_file_mapping = false;
 	const vma_flags_t vma_flags = legacy_to_vma_flags(vm_flags);
 
-	mmap_assert_write_locked(current->mm);
+	mmap_assert_write_locked(mm);
 
 	/* Check to see if MDWE is applicable. */
-	if (map_deny_write_exec(&vma_flags, &vma_flags))
+	if (map_deny_write_exec(mm, &vma_flags, &vma_flags))
 		return -EACCES;
 
 	/* Allow architectures to sanity-check the vm_flags. */
@@ -2857,13 +2861,13 @@ unsigned long mmap_region(struct file *file, unsigned long addr,
 		writable_file_mapping = true;
 	}
 
-	ret = __mmap_region(file, addr, len, vma_flags, pgoff, uf);
+	ret = __mmap_region(mm, file, addr, len, vma_flags, pgoff, uf);
 
 	/* Clear our write mapping regardless of error. */
 	if (writable_file_mapping)
 		mapping_unmap_writable(file->f_mapping);
 
-	validate_mm(current->mm);
+	validate_mm(mm);
 	return ret;
 }
 
@@ -2983,6 +2987,8 @@ unsigned long unmapped_area(struct mm_struct *mm,
 	if (low_limit < mmap_min_addr)
 		low_limit = mmap_min_addr;
 	high_limit = info->high_limit;
+	if (mm != current->mm)
+		high_limit = min(high_limit, mm->task_size);
 retry:
 	if (vma_iter_area_lowest(&vmi, low_limit, high_limit, length))
 		return -ENOMEM;
@@ -3043,6 +3049,8 @@ unsigned long unmapped_area_topdown(struct mm_struct *mm,
 	if (low_limit < mmap_min_addr)
 		low_limit = mmap_min_addr;
 	high_limit = info->high_limit;
+	if (mm != current->mm)
+		high_limit = min(high_limit, mm->task_size);
 retry:
 	if (vma_iter_area_highest(&vmi, low_limit, high_limit, length))
 		return -ENOMEM;
diff --git a/mm/vma.h b/mm/vma.h
index d0a45718deb9..0c0b9d9c5248 100644
--- a/mm/vma.h
+++ b/mm/vma.h
@@ -32,6 +32,7 @@ struct unlink_vma_file_batch {
  * vma munmap operation
  */
 struct vma_munmap_struct {
+	struct mm_struct *mm;           /* The mm being operated on */
 	struct vma_iterator *vmi;
 	struct vm_area_struct *vma;     /* The first vma to munmap */
 	struct vm_area_struct *prev;    /* vma before the munmap area */
@@ -459,9 +460,9 @@ bool vma_wants_writenotify(struct vm_area_struct *vma, pgprot_t vm_page_prot);
 int mm_take_all_locks(struct mm_struct *mm);
 void mm_drop_all_locks(struct mm_struct *mm);
 
-unsigned long mmap_region(struct file *file, unsigned long addr,
-		unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
-		struct list_head *uf);
+unsigned long mmap_region(struct mm_struct *mm, struct file *file,
+		unsigned long addr, unsigned long len, vm_flags_t vm_flags,
+		unsigned long pgoff, struct list_head *uf);
 
 int do_brk_flags(struct vma_iterator *vmi, struct vm_area_struct *brkvma,
 		 unsigned long addr, unsigned long request,
@@ -732,11 +733,12 @@ int relocate_vma_down(struct vm_area_struct *vma, unsigned long shift);
  *
  * Return: false if proposed change is OK, true if not ok and should be denied.
  */
-static inline bool map_deny_write_exec(const vma_flags_t *old,
+static inline bool map_deny_write_exec(struct mm_struct *mm,
+				       const vma_flags_t *old,
 				       const vma_flags_t *new)
 {
 	/* If MDWE is disabled, we have nothing to deny. */
-	if (!mm_flags_test(MMF_HAS_MDWE, current->mm))
+	if (!mm_flags_test(MMF_HAS_MDWE, mm))
 		return false;
 
 	/* If the new VMA is not executable, we have nothing to deny. */
-- 
2.43.0



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

* [PATCH v6 3/8] seccomp: introduce SECCOMP_IOCTL_NOTIF_PIN_INSTALL
  2026-07-15 21:19 [PATCH v6 0/8] seccomp: non-cooperative pinned-memfd argument redirect Cong Wang
  2026-07-15 21:20 ` [PATCH v6 1/8] mm: pass the target mm parameter through get_unmapped_area family Cong Wang
  2026-07-15 21:20 ` [PATCH v6 2/8] mm: add __do_mmap() and vm_mmap_remote()/vm_munmap_remote() Cong Wang
@ 2026-07-15 21:20 ` Cong Wang
  2026-07-15 21:20 ` [PATCH v6 4/8] seccomp: add __NR_seccomp_* aliases for rt_sigreturn and clone/fork Cong Wang
                   ` (4 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: Cong Wang @ 2026-07-15 21:20 UTC (permalink / raw)
  To: Andy Lutomirski
  Cc: Kees Cook, linux-kernel, Will Drewry, Christian Brauner,
	Andrew Morton, linux-mm, Cong Wang

From: Cong Wang <cwang@multikernel.io>

SECCOMP_IOCTL_NOTIF_PIN_INSTALL maps a supervisor-owned @memfd at
@target_addr in the trapped task's mm via vm_mmap_remote(),
PROT_READ, MAP_SHARED, MAP_FIXED_NOREPLACE and VM_SEALED. Because the
mapping is sealed, neither the target nor a CLONE_VM peer can munmap,
mremap, mprotect or MAP_FIXED-stomp it; its contents are immutable from
the target's side while the supervisor retains write access through its
own mapping of the same memfd. The install needs no target-side
cooperation, which is what makes the feature usable for fork+execve
sandbox wrappers (Sandlock, Firejail, Bubblewrap-style) that have no
trusted post-exec window to install their own mappings.

The pin is just a sealed VMA owned by the target's mm: it persists until
the task execve()s or exits (a sealed VMA cannot be unmapped piecemeal),
and the kernel keeps no per-pin bookkeeping. A supervisor reuses one
region across many redirects.

Assisted-by: Claude:claude-opus-4.8
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
 include/linux/seccomp.h      |   5 ++
 include/uapi/linux/seccomp.h |  34 ++++++++++
 kernel/seccomp.c             | 117 +++++++++++++++++++++++++++++++++++
 3 files changed, 156 insertions(+)

diff --git a/include/linux/seccomp.h b/include/linux/seccomp.h
index 9b959972bf4a..a91d1fc8a2b8 100644
--- a/include/linux/seccomp.h
+++ b/include/linux/seccomp.h
@@ -16,6 +16,11 @@
 #define SECCOMP_NOTIFY_ADDFD_SIZE_VER0 24
 #define SECCOMP_NOTIFY_ADDFD_SIZE_LATEST SECCOMP_NOTIFY_ADDFD_SIZE_VER0
 
+/* sizeof() the first published struct seccomp_notif_pin_install */
+#define SECCOMP_NOTIFY_PIN_INSTALL_SIZE_VER0 32		/* up to @size */
+#define SECCOMP_NOTIFY_PIN_INSTALL_SIZE_VER1 40		/* adds @offset */
+#define SECCOMP_NOTIFY_PIN_INSTALL_SIZE_LATEST SECCOMP_NOTIFY_PIN_INSTALL_SIZE_VER1
+
 #ifdef CONFIG_SECCOMP
 
 #include <linux/thread_info.h>
diff --git a/include/uapi/linux/seccomp.h b/include/uapi/linux/seccomp.h
index dbfc9b37fcae..d3249294788b 100644
--- a/include/uapi/linux/seccomp.h
+++ b/include/uapi/linux/seccomp.h
@@ -137,6 +137,37 @@ struct seccomp_notif_addfd {
 	__u32 newfd_flags;
 };
 
+/**
+ * struct seccomp_notif_pin_install - have the kernel install a sealed
+ * MAP_SHARED mapping of @memfd into the trapped task's mm at @target_addr.
+ *
+ * The supervisor owns @memfd and the kernel installs the mapping without
+ * target-side cooperation. It is read-only and VM_SEALED, so the target and
+ * any CLONE_VM peer cannot munmap, mremap, mprotect or MAP_FIXED-stomp it.
+ * @memfd must be write-sealed (F_SEAL_WRITE or F_SEAL_FUTURE_WRITE, -EINVAL
+ * otherwise) so its bytes cannot be rewritten through any other reference to
+ * the same memfd.
+ *
+ * @id: The ID of an active seccomp notification on this listener,
+ *      identifying the trapped task whose mm receives the pin.
+ * @flags: Reserved, must be 0.
+ * @memfd: Supervisor-side fd for the backing memfd. Must be write-sealed.
+ * @target_addr: Page-aligned address in the trapped task's mm to install at.
+ *               If non-zero it is MAP_FIXED (no existing mapping may overlap
+ *               [@target_addr, @target_addr + @size)); if zero the kernel
+ *               picks a free area. The actual address is written back here.
+ * @size: Size of the pin in bytes. Must be page-aligned.
+ * @offset: Page-aligned byte offset into @memfd to map from.
+ */
+struct seccomp_notif_pin_install {
+	__u64 id;
+	__u32 flags;
+	__u32 memfd;
+	__u64 target_addr;
+	__u64 size;
+	__u64 offset;
+};
+
 #define SECCOMP_IOC_MAGIC		'!'
 #define SECCOMP_IO(nr)			_IO(SECCOMP_IOC_MAGIC, nr)
 #define SECCOMP_IOR(nr, type)		_IOR(SECCOMP_IOC_MAGIC, nr, type)
@@ -154,4 +185,7 @@ struct seccomp_notif_addfd {
 
 #define SECCOMP_IOCTL_NOTIF_SET_FLAGS	SECCOMP_IOW(4, __u64)
 
+#define SECCOMP_IOCTL_NOTIF_PIN_INSTALL	SECCOMP_IOWR(5, \
+						struct seccomp_notif_pin_install)
+
 #endif /* _UAPI_LINUX_SECCOMP_H */
diff --git a/kernel/seccomp.c b/kernel/seccomp.c
index 066909393c38..12135c5df9b1 100644
--- a/kernel/seccomp.c
+++ b/kernel/seccomp.c
@@ -37,12 +37,18 @@
 #ifdef CONFIG_SECCOMP_FILTER
 #include <linux/file.h>
 #include <linux/filter.h>
+#include <linux/memfd.h>
 #include <linux/pid.h>
 #include <linux/ptrace.h>
 #include <linux/capability.h>
 #include <linux/uaccess.h>
 #include <linux/anon_inodes.h>
 #include <linux/lockdep.h>
+#include <linux/mm.h>
+#include <linux/mman.h>
+#include <linux/mmap_lock.h>
+#include <linux/sched/mm.h>
+#include <linux/task_work.h>
 
 /*
  * When SECCOMP_IOCTL_NOTIF_ID_VALID was first introduced, it had the
@@ -1823,6 +1829,114 @@ static long seccomp_notify_addfd(struct seccomp_filter *filter,
 	return ret;
 }
 
+static unsigned long seccomp_install_pin(struct mm_struct *mm,
+					 struct file *memfd_file,
+					 unsigned long target_addr, size_t size,
+					 unsigned long offset)
+{
+	unsigned long ret;
+
+	if (!VM_SEALED)
+		return -EOPNOTSUPP;
+
+	/*
+	 * Install a sealed, read-only mapping. A fixed request (@target_addr
+	 * != 0) is MAP_FIXED_NOREPLACE: an existing mapping yields -EEXIST
+	 * rather than being silently clobbered. A request of 0 lets the kernel
+	 * pick a free area in the target mm.
+	 */
+	ret = vm_mmap_remote(mm, memfd_file, target_addr, size, PROT_READ,
+			     MAP_SHARED | MAP_FIXED_NOREPLACE,
+			     offset >> PAGE_SHIFT, VM_SEALED);
+	if (IS_ERR_VALUE(ret))
+		return ret;
+	if (target_addr && ret != target_addr)
+		return -ENOMEM;
+	return ret;
+}
+
+static long seccomp_notify_pin_install(struct seccomp_filter *filter,
+				       struct seccomp_notif_pin_install __user *upin,
+				       unsigned int size)
+{
+	struct seccomp_notif_pin_install pin;
+	struct seccomp_knotif *knotif;
+	struct task_struct *target;
+	struct file *memfd_file;
+	struct mm_struct *mm;
+	unsigned long addr;
+	int seals;
+	long ret;
+
+	BUILD_BUG_ON(sizeof(pin) < SECCOMP_NOTIFY_PIN_INSTALL_SIZE_VER0);
+	BUILD_BUG_ON(sizeof(pin) != SECCOMP_NOTIFY_PIN_INSTALL_SIZE_LATEST);
+
+	if (size < SECCOMP_NOTIFY_PIN_INSTALL_SIZE_VER0 || size >= PAGE_SIZE)
+		return -EINVAL;
+
+	ret = copy_struct_from_user(&pin, sizeof(pin), upin, size);
+	if (ret)
+		return ret;
+
+	if (pin.flags)
+		return -EINVAL;
+	if (!pin.size || !IS_ALIGNED(pin.target_addr, PAGE_SIZE) ||
+	    !IS_ALIGNED(pin.size, PAGE_SIZE) || !IS_ALIGNED(pin.offset, PAGE_SIZE))
+		return -EINVAL;
+	if (pin.target_addr + pin.size < pin.target_addr)
+		return -EINVAL;
+	if (pin.offset + pin.size < pin.offset)
+		return -EINVAL;
+
+	memfd_file = fget(pin.memfd);
+	if (!memfd_file)
+		return -EBADF;
+
+	seals = memfd_get_seals(memfd_file);
+	if (seals < 0 || !(seals & (F_SEAL_WRITE | F_SEAL_FUTURE_WRITE))) {
+		ret = -EINVAL;
+		goto out_fput;
+	}
+
+	ret = mutex_lock_interruptible(&filter->notify_lock);
+	if (ret < 0)
+		goto out_fput;
+
+	knotif = find_notification(filter, pin.id);
+	if (!knotif) {
+		ret = -ENOENT;
+		goto out_unlock;
+	}
+	if (knotif->state != SECCOMP_NOTIFY_SENT) {
+		ret = -EINPROGRESS;
+		goto out_unlock;
+	}
+
+	target = knotif->task;
+	mm = get_task_mm(target);
+	mutex_unlock(&filter->notify_lock);
+	if (!mm) {
+		ret = -ESRCH;
+		goto out_fput;
+	}
+	addr = seccomp_install_pin(mm, memfd_file, pin.target_addr,
+				   pin.size, pin.offset);
+	mmput(mm);
+	if (IS_ERR_VALUE(addr))
+		ret = addr;
+	else if (put_user(addr, &upin->target_addr))
+		ret = -EFAULT;
+	else
+		ret = 0;
+	goto out_fput;
+
+out_unlock:
+	mutex_unlock(&filter->notify_lock);
+out_fput:
+	fput(memfd_file);
+	return ret;
+}
+
 static long seccomp_notify_ioctl(struct file *file, unsigned int cmd,
 				 unsigned long arg)
 {
@@ -1847,6 +1961,9 @@ static long seccomp_notify_ioctl(struct file *file, unsigned int cmd,
 	switch (EA_IOCTL(cmd)) {
 	case EA_IOCTL(SECCOMP_IOCTL_NOTIF_ADDFD):
 		return seccomp_notify_addfd(filter, buf, _IOC_SIZE(cmd));
+	case EA_IOCTL(SECCOMP_IOCTL_NOTIF_PIN_INSTALL):
+		return seccomp_notify_pin_install(filter, buf,
+						  _IOC_SIZE(cmd));
 	default:
 		return -EINVAL;
 	}
-- 
2.43.0



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

* [PATCH v6 4/8] seccomp: add __NR_seccomp_* aliases for rt_sigreturn and clone/fork
  2026-07-15 21:19 [PATCH v6 0/8] seccomp: non-cooperative pinned-memfd argument redirect Cong Wang
                   ` (2 preceding siblings ...)
  2026-07-15 21:20 ` [PATCH v6 3/8] seccomp: introduce SECCOMP_IOCTL_NOTIF_PIN_INSTALL Cong Wang
@ 2026-07-15 21:20 ` Cong Wang
  2026-07-15 21:20 ` [PATCH v6 5/8] seccomp: add kernel-installed pinned-memfd redirect Cong Wang
                   ` (3 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: Cong Wang @ 2026-07-15 21:20 UTC (permalink / raw)
  To: Andy Lutomirski
  Cc: Kees Cook, linux-kernel, Will Drewry, Christian Brauner,
	Andrew Morton, linux-mm, Cong Wang

From: Cong Wang <cwang@multikernel.io>

The existing __NR_seccomp_* aliases name only the strict-mode syscalls
(read/write/exit/sigreturn). SEND_REDIRECT must also recognise
rt_sigreturn and the clone/fork task-creation family, native and compat,
so it can refuse to redirect them.

Gate these behind a new SECCOMP_ARCH_REDIRECT opt-in, mirroring how
SECCOMP_ARCH_NATIVE gates the bitmap cache: an arch declares it only once
it supplies a complete, verified set of these numbers, and where it is
undefined the feature is compiled out. This avoids a generic fallback
silently handing an arch a wrong number that would drop a syscall from
the deny list. x86_64 opts in and supplies the ia32 compat numbers.

Assisted-by: Claude:claude-opus-4.8
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
 arch/x86/include/asm/seccomp.h | 15 ++++++++++++++-
 include/asm-generic/seccomp.h  | 30 ++++++++++++++++++++++++++++++
 2 files changed, 44 insertions(+), 1 deletion(-)

diff --git a/arch/x86/include/asm/seccomp.h b/arch/x86/include/asm/seccomp.h
index 42bcd42d70d1..a911fce504ac 100644
--- a/arch/x86/include/asm/seccomp.h
+++ b/arch/x86/include/asm/seccomp.h
@@ -6,6 +6,7 @@
 
 #ifdef CONFIG_X86_32
 #define __NR_seccomp_sigreturn		__NR_sigreturn
+#define __NR_seccomp_rt_sigreturn	__NR_rt_sigreturn
 #endif
 
 #ifdef CONFIG_COMPAT
@@ -14,12 +15,18 @@
 #define __NR_seccomp_write_32		__NR_ia32_write
 #define __NR_seccomp_exit_32		__NR_ia32_exit
 #define __NR_seccomp_sigreturn_32	__NR_ia32_sigreturn
+#define __NR_seccomp_rt_sigreturn_32	__NR_ia32_rt_sigreturn
+#define __NR_seccomp_clone_32		__NR_ia32_clone
+#define __NR_seccomp_clone3_32		__NR_ia32_clone3
+#define __NR_seccomp_fork_32		__NR_ia32_fork
+#define __NR_seccomp_vfork_32		__NR_ia32_vfork
 #endif
 
 #ifdef CONFIG_X86_64
 # define SECCOMP_ARCH_NATIVE		AUDIT_ARCH_X86_64
 # define SECCOMP_ARCH_NATIVE_NR		NR_syscalls
 # define SECCOMP_ARCH_NATIVE_NAME	"x86_64"
+# define SECCOMP_ARCH_REDIRECT		1
 # ifdef CONFIG_COMPAT
 #  define SECCOMP_ARCH_COMPAT		AUDIT_ARCH_I386
 #  define SECCOMP_ARCH_COMPAT_NR	IA32_NR_syscalls
@@ -28,8 +35,14 @@
 /*
  * x32 will have __X32_SYSCALL_BIT set in syscall number. We don't support
  * caching them and they are treated as out of range syscalls, which will
- * always pass through the BPF filter.
+ * always pass through the BPF filter. It shares AUDIT_ARCH_X86_64 with the
+ * native ABI, so refuse to redirect it: the generic denylist keys off plain
+ * syscall numbers and cannot name x32's sigreturn/clone.
  */
+static inline bool arch_seccomp_redirect_deny(const struct seccomp_data *sd)
+{
+	return sd->nr & __X32_SYSCALL_BIT;
+}
 #else /* !CONFIG_X86_64 */
 # define SECCOMP_ARCH_NATIVE		AUDIT_ARCH_I386
 # define SECCOMP_ARCH_NATIVE_NR	        NR_syscalls
diff --git a/include/asm-generic/seccomp.h b/include/asm-generic/seccomp.h
index 6b6f42bc58f9..42b1b9b79ddf 100644
--- a/include/asm-generic/seccomp.h
+++ b/include/asm-generic/seccomp.h
@@ -26,6 +26,36 @@
 #define __NR_seccomp_sigreturn		__NR_rt_sigreturn
 #endif
 
+#ifdef SECCOMP_ARCH_REDIRECT
+#ifndef __NR_seccomp_rt_sigreturn
+#define __NR_seccomp_rt_sigreturn	__NR_seccomp_sigreturn
+#endif
+#ifndef __NR_seccomp_clone
+#define __NR_seccomp_clone		__NR_clone
+#endif
+#ifndef __NR_seccomp_clone3
+#ifdef __NR_clone3
+#define __NR_seccomp_clone3		__NR_clone3
+#else
+#define __NR_seccomp_clone3		(-1)
+#endif
+#endif
+#ifndef __NR_seccomp_fork
+#ifdef __NR_fork
+#define __NR_seccomp_fork		__NR_fork
+#else
+#define __NR_seccomp_fork		(-1)
+#endif
+#endif
+#ifndef __NR_seccomp_vfork
+#ifdef __NR_vfork
+#define __NR_seccomp_vfork		__NR_vfork
+#else
+#define __NR_seccomp_vfork		(-1)
+#endif
+#endif
+#endif /* SECCOMP_ARCH_REDIRECT */
+
 #ifdef CONFIG_COMPAT
 #ifndef get_compat_mode1_syscalls
 static inline const int *get_compat_mode1_syscalls(void)
-- 
2.43.0



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

* [PATCH v6 5/8] seccomp: add kernel-installed pinned-memfd redirect
  2026-07-15 21:19 [PATCH v6 0/8] seccomp: non-cooperative pinned-memfd argument redirect Cong Wang
                   ` (3 preceding siblings ...)
  2026-07-15 21:20 ` [PATCH v6 4/8] seccomp: add __NR_seccomp_* aliases for rt_sigreturn and clone/fork Cong Wang
@ 2026-07-15 21:20 ` Cong Wang
  2026-07-15 21:20 ` [PATCH v6 6/8] seccomp: re-validate a redirected syscall against outer filters Cong Wang
                   ` (2 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: Cong Wang @ 2026-07-15 21:20 UTC (permalink / raw)
  To: Andy Lutomirski
  Cc: Kees Cook, linux-kernel, Will Drewry, Christian Brauner,
	Andrew Morton, linux-mm, Cong Wang

From: Cong Wang <cwang@multikernel.io>

Add SECCOMP_IOCTL_NOTIF_SEND_REDIRECT, which resumes a trapped syscall
(like SECCOMP_USER_NOTIF_FLAG_CONTINUE) with selected argument registers
rewritten to point into a pin installed by SECCOMP_IOCTL_NOTIF_PIN_INSTALL.
This closes the user-notification TOCTOU for fork+execve sandboxes: the
kernel acts on an immutable, supervisor-controlled sealed mapping instead
of memory a CLONE_VM peer can rewrite after the check.

The feature is gated behind SECCOMP_FILTER_FLAG_REDIRECT, declared at
listener creation (it requires SECCOMP_FILTER_FLAG_NEW_LISTENER) and
required by both ioctls. At most one redirect-capable filter may exist in
a chain (-EBUSY otherwise), so a redirect has a single, unambiguous
register fixup.

The supervisor supplies an args_mask, a ptr_mask and replacement values.
Each pointer substitution is validated by seccomp_pin_check(): the access
[args[i], args[i] + ptr_len[i]) must lie in a single VM_SEALED, read-only,
MAP_SHARED VMA still backed by the named memfd. The kernel keeps no
bookkeeping; after execve or exit the VMA is gone and validation returns
-EFAULT.

Original arg registers are saved and restored at user-mode return (via a
TWA_RESUME task_work, so a restartable syscall is not turned into a
livelock), preserving the caller-saved arg-register ABI. The restore is
skipped after a successful execve. rt_sigreturn is refused (-EOPNOTSUPP):
it restores the whole register frame and takes no arguments to substitute.

The whole redirect path is now gated by SECCOMP_ARCH_REDIRECT, the opt-in
arch declares once its deny list syscall numbers are complete and verified.
Only x86_64 opts in so far.

Assisted-by: Claude:claude-opus-4.8
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
 include/linux/seccomp.h      |   7 +-
 include/uapi/linux/seccomp.h |  55 +++++-
 kernel/seccomp.c             | 313 +++++++++++++++++++++++++++++++++++
 3 files changed, 373 insertions(+), 2 deletions(-)

diff --git a/include/linux/seccomp.h b/include/linux/seccomp.h
index a91d1fc8a2b8..5d53f8fce508 100644
--- a/include/linux/seccomp.h
+++ b/include/linux/seccomp.h
@@ -10,7 +10,8 @@
 					 SECCOMP_FILTER_FLAG_SPEC_ALLOW | \
 					 SECCOMP_FILTER_FLAG_NEW_LISTENER | \
 					 SECCOMP_FILTER_FLAG_TSYNC_ESRCH | \
-					 SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV)
+					 SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV | \
+					 SECCOMP_FILTER_FLAG_REDIRECT)
 
 /* sizeof() the first published struct seccomp_notif_addfd */
 #define SECCOMP_NOTIFY_ADDFD_SIZE_VER0 24
@@ -21,6 +22,10 @@
 #define SECCOMP_NOTIFY_PIN_INSTALL_SIZE_VER1 40		/* adds @offset */
 #define SECCOMP_NOTIFY_PIN_INSTALL_SIZE_LATEST SECCOMP_NOTIFY_PIN_INSTALL_SIZE_VER1
 
+/* sizeof() the first published struct seccomp_notif_resp_redirect */
+#define SECCOMP_NOTIFY_RESP_REDIRECT_SIZE_VER0 120
+#define SECCOMP_NOTIFY_RESP_REDIRECT_SIZE_LATEST SECCOMP_NOTIFY_RESP_REDIRECT_SIZE_VER0
+
 #ifdef CONFIG_SECCOMP
 
 #include <linux/thread_info.h>
diff --git a/include/uapi/linux/seccomp.h b/include/uapi/linux/seccomp.h
index d3249294788b..bb5875f72556 100644
--- a/include/uapi/linux/seccomp.h
+++ b/include/uapi/linux/seccomp.h
@@ -25,6 +25,12 @@
 #define SECCOMP_FILTER_FLAG_TSYNC_ESRCH		(1UL << 4)
 /* Received notifications wait in killable state (only respond to fatal signals) */
 #define SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV	(1UL << 5)
+/*
+ * Declares that this listener's notifier may issue
+ * SECCOMP_IOCTL_NOTIF_PIN_INSTALL / SECCOMP_IOCTL_NOTIF_SEND_REDIRECT. At most
+ * one such filter may exist in a task's filter chain. Requires NEW_LISTENER.
+ */
+#define SECCOMP_FILTER_FLAG_REDIRECT		(1UL << 6)
 
 /*
  * All BPF programs must return a 32-bit value.
@@ -139,7 +145,9 @@ struct seccomp_notif_addfd {
 
 /**
  * struct seccomp_notif_pin_install - have the kernel install a sealed
- * MAP_SHARED mapping of @memfd into the trapped task's mm at @target_addr.
+ * MAP_SHARED mapping of @memfd into the trapped task's mm at @target_addr,
+ * which SECCOMP_IOCTL_NOTIF_SEND_REDIRECT can then use as a target for
+ * substituted pointer arguments.
  *
  * The supervisor owns @memfd and the kernel installs the mapping without
  * target-side cooperation. It is read-only and VM_SEALED, so the target and
@@ -168,6 +176,45 @@ struct seccomp_notif_pin_install {
 	__u64 offset;
 };
 
+#define SECCOMP_REDIRECT_ARGS 6
+
+/**
+ * struct seccomp_notif_resp_redirect - resume the trapped syscall with
+ * substituted arg-register values, optionally pointing into an installed
+ * pinned-memfd region.
+ *
+ * Like SECCOMP_USER_NOTIF_FLAG_CONTINUE the syscall runs, but the kernel
+ * first rewrites the arg registers in @args_mask. Pointer substitutions
+ * (@ptr_mask) are validated against the trapped task's live mapping of
+ * @memfd, so a target that has exited or execve()d simply fails validation.
+ * Original registers are restored at syscall exit, skipped after a successful
+ * execve whose fresh register file must not be clobbered.
+ *
+ * @id: The ID of the seccomp notification this response consumes.
+ * @flags: SECCOMP_REDIRECT_FLAG_*. CONTINUE must be set.
+ * @args_mask: Bit i set means args[i] replaces arg register i before the
+ *             syscall runs.
+ * @ptr_mask: Subset of @args_mask. Bit i set means args[i] is a pointer whose
+ *            access [args[i], args[i] + ptr_len[i]) must lie inside a single
+ *            VM_SEALED, read-only mapping of @memfd. Scalars (in @args_mask
+ *            but not @ptr_mask) are written verbatim.
+ * @memfd: Supervisor-side fd for the backing memfd. Consulted only when
+ *         @ptr_mask is non-zero.
+ * @args: Replacement values for the arg registers.
+ * @ptr_len: For each bit set in @ptr_mask, the byte length of the access at
+ *           args[i]; must be non-zero and args[i] + ptr_len[i] must not
+ *           overflow. Must be 0 where @ptr_mask bit i is clear.
+ */
+struct seccomp_notif_resp_redirect {
+	__u64 id;
+	__u32 flags;
+	__u32 args_mask;
+	__u32 ptr_mask;
+	__u32 memfd;
+	__u64 args[SECCOMP_REDIRECT_ARGS];
+	__u64 ptr_len[SECCOMP_REDIRECT_ARGS];
+};
+
 #define SECCOMP_IOC_MAGIC		'!'
 #define SECCOMP_IO(nr)			_IO(SECCOMP_IOC_MAGIC, nr)
 #define SECCOMP_IOR(nr, type)		_IOR(SECCOMP_IOC_MAGIC, nr, type)
@@ -188,4 +235,10 @@ struct seccomp_notif_pin_install {
 #define SECCOMP_IOCTL_NOTIF_PIN_INSTALL	SECCOMP_IOWR(5, \
 						struct seccomp_notif_pin_install)
 
+#define SECCOMP_IOCTL_NOTIF_SEND_REDIRECT	SECCOMP_IOW(6, \
+						struct seccomp_notif_resp_redirect)
+
+/* Valid flags for struct seccomp_notif_resp_redirect. */
+#define SECCOMP_REDIRECT_FLAG_CONTINUE (1UL << 0)
+
 #endif /* _UAPI_LINUX_SECCOMP_H */
diff --git a/kernel/seccomp.c b/kernel/seccomp.c
index 12135c5df9b1..17fe4c360839 100644
--- a/kernel/seccomp.c
+++ b/kernel/seccomp.c
@@ -211,6 +211,9 @@ static inline void seccomp_cache_prepare(struct seccomp_filter *sfilter)
  * @log: true if all actions except for SECCOMP_RET_ALLOW should be logged
  * @wait_killable_recv: Put notifying process in killable state once the
  *			notification is received by the userspace listener.
+ * @redirect_capable: true if installed with SECCOMP_FILTER_FLAG_REDIRECT, so
+ *		      this filter's notifier may issue SEND_REDIRECT. At most
+ *		      one filter in a stack may be redirect-capable.
  * @prev: points to a previously installed, or inherited, filter
  * @prog: the BPF program to evaluate
  * @notif: the struct that holds all notification related information
@@ -232,6 +235,7 @@ struct seccomp_filter {
 	refcount_t users;
 	bool log;
 	bool wait_killable_recv;
+	bool redirect_capable;
 	struct action_cache cache;
 	struct seccomp_filter *prev;
 	struct bpf_prog *prog;
@@ -952,6 +956,13 @@ static long seccomp_attach_filter(unsigned int flags,
 		}
 	}
 
+	if (flags & SECCOMP_FILTER_FLAG_REDIRECT) {
+		for (walker = current->seccomp.filter; walker;
+		     walker = walker->prev)
+			if (walker->redirect_capable)
+				return -EBUSY;
+	}
+
 	/* Set log flag, if present. */
 	if (flags & SECCOMP_FILTER_FLAG_LOG)
 		filter->log = true;
@@ -960,6 +971,10 @@ static long seccomp_attach_filter(unsigned int flags,
 	if (flags & SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV)
 		filter->wait_killable_recv = true;
 
+	/* Set redirect-capable flag, if present. */
+	if (flags & SECCOMP_FILTER_FLAG_REDIRECT)
+		filter->redirect_capable = true;
+
 	/*
 	 * If there is an existing filter, make it the prev and don't drop its
 	 * task reference.
@@ -1937,6 +1952,292 @@ static long seccomp_notify_pin_install(struct seccomp_filter *filter,
 	return ret;
 }
 
+#ifdef SECCOMP_ARCH_REDIRECT
+static bool seccomp_pin_check(struct task_struct *target,
+			      struct file *memfd_file, u64 ptr, u64 len)
+{
+	struct vm_area_struct *vma;
+	struct mm_struct *mm;
+	bool ok = false;
+	u64 end;
+
+	if (!len)
+		return false;
+	end = ptr + len;
+	if (end < ptr)
+		return false;
+
+	mm = get_task_mm(target);
+	if (!mm)
+		return false;
+
+	mmap_read_lock(mm);
+	vma = vma_lookup(mm, ptr);
+	/*
+	 * The access must lie in a single sealed, read-only, MAP_SHARED,
+	 * memfd-backed VMA. VM_SHARED is required so the bytes the kernel reads
+	 * are the memfd's own pages: a MAP_PRIVATE mapping would resolve to
+	 * anonymous COW copies the target could have written before sealing it
+	 * read-only, defeating the guarantee.
+	 */
+	if (vma && end <= vma->vm_end && (vma->vm_flags & VM_SEALED) &&
+	    (vma->vm_flags & VM_SHARED) && !(vma->vm_flags & VM_WRITE) &&
+	    vma->vm_file && file_inode(vma->vm_file) == file_inode(memfd_file))
+		ok = true;
+	mmap_read_unlock(mm);
+
+	mmput(mm);
+	return ok;
+}
+
+struct seccomp_redirect_restore {
+	struct callback_head twork;
+	unsigned long orig_args[SECCOMP_REDIRECT_ARGS];
+	u32 args_mask;		/* bit i: arg i was substituted, restore it */
+	u64 self_exec_id;	/* snapshot to detect an intervening execve */
+};
+
+/*
+ * If a syscall is redirected by more than one filter, one restore is
+ * queued per redirect, each recording the args as they stood at its own
+ * redirect. Correct restoration relies on task_work running LIFO:
+ * the first redirect captured the true original and queued first, runs
+ * last, so it wins.
+ */
+static void seccomp_redirect_restore_cb(struct callback_head *cb)
+{
+	struct seccomp_redirect_restore *r =
+		container_of(cb, struct seccomp_redirect_restore, twork);
+	unsigned long args[SECCOMP_REDIRECT_ARGS];
+	long ret, err;
+	int i;
+
+	if (READ_ONCE(current->self_exec_id) != r->self_exec_id) {
+		kfree(r);
+		return;
+	}
+
+	err = syscall_get_error(current, current_pt_regs());
+	ret = syscall_get_return_value(current, current_pt_regs());
+
+	syscall_get_arguments(current, current_pt_regs(), args);
+	for (i = 0; i < SECCOMP_REDIRECT_ARGS; i++)
+		if (r->args_mask & (1U << i))
+			args[i] = r->orig_args[i];
+	syscall_set_arguments(current, current_pt_regs(), args);
+
+	syscall_set_return_value(current, current_pt_regs(), err, ret);
+
+	kfree(r);
+}
+
+/*
+ * sigreturn/rt_sigreturn restore the entire register frame from the user
+ * signal stack; the SEND_REDIRECT register-restore (run from task_work at
+ * user-mode return) would corrupt that frame, and the syscall takes no
+ * arguments to substitute anyway. Refuse to redirect any of them, including
+ * the compat variants (legacy sigreturn and rt_sigreturn are distinct
+ * numbers). x32 rt_sigreturn carries __X32_SYSCALL_BIT and is not matched
+ * here; the deprecated x32 ABI is out of scope for redirect.
+ */
+static bool seccomp_redirect_is_sigreturn(const struct seccomp_data *sd)
+{
+#ifdef SECCOMP_ARCH_COMPAT
+	if (sd->arch == SECCOMP_ARCH_COMPAT)
+		return sd->nr == __NR_seccomp_sigreturn_32 ||
+		       sd->nr == __NR_seccomp_rt_sigreturn_32;
+#endif
+	return sd->nr == __NR_seccomp_sigreturn ||
+	       sd->nr == __NR_seccomp_rt_sigreturn;
+}
+
+/*
+ * clone/fork-family syscalls copy the trapped task's register frame into the
+ * new child, which gets no restore task_work of its own. A redirected task
+ * creation would leave the child in user space with the substituted (pinned)
+ * values still in its caller-saved arg registers, breaking the ABI the restore
+ * preserves for the parent. There is no use case for redirecting task creation,
+ * so refuse it. Syscalls absent on an arch resolve to -1 and never match.
+ */
+static bool seccomp_redirect_is_task_create(const struct seccomp_data *sd)
+{
+#ifdef SECCOMP_ARCH_COMPAT
+	if (sd->arch == SECCOMP_ARCH_COMPAT)
+		return sd->nr == __NR_seccomp_clone_32 ||
+		       sd->nr == __NR_seccomp_clone3_32 ||
+		       sd->nr == __NR_seccomp_fork_32 ||
+		       sd->nr == __NR_seccomp_vfork_32;
+#endif
+	return sd->nr == __NR_seccomp_clone ||
+	       sd->nr == __NR_seccomp_clone3 ||
+	       sd->nr == __NR_seccomp_fork ||
+	       sd->nr == __NR_seccomp_vfork;
+}
+
+static bool seccomp_redirect_deny(const struct seccomp_data *sd)
+{
+	return arch_seccomp_redirect_deny(sd) ||
+	       seccomp_redirect_is_sigreturn(sd) ||
+	       seccomp_redirect_is_task_create(sd);
+}
+
+static long seccomp_notify_send_redirect(struct seccomp_filter *filter,
+					 struct seccomp_notif_resp_redirect __user *uresp,
+					 unsigned int size)
+{
+	unsigned long args[SECCOMP_REDIRECT_ARGS];
+	struct seccomp_redirect_restore *restore;
+	struct seccomp_notif_resp_redirect resp;
+	struct file *memfd_file = NULL;
+	struct seccomp_knotif *knotif;
+	struct pt_regs *target_regs;
+	long ret;
+	int i;
+
+	BUILD_BUG_ON(sizeof(resp) < SECCOMP_NOTIFY_RESP_REDIRECT_SIZE_VER0);
+	BUILD_BUG_ON(sizeof(resp) != SECCOMP_NOTIFY_RESP_REDIRECT_SIZE_LATEST);
+
+	if (!filter->redirect_capable)
+		return -EPERM;
+
+	if (size < SECCOMP_NOTIFY_RESP_REDIRECT_SIZE_VER0 || size >= PAGE_SIZE)
+		return -EINVAL;
+
+	ret = copy_struct_from_user(&resp, sizeof(resp), uresp, size);
+	if (ret)
+		return ret;
+
+	if (!(resp.flags & SECCOMP_REDIRECT_FLAG_CONTINUE))
+		return -EINVAL;
+	if (resp.flags & ~SECCOMP_REDIRECT_FLAG_CONTINUE)
+		return -EINVAL;
+	if (resp.args_mask & ~((1U << SECCOMP_REDIRECT_ARGS) - 1))
+		return -EINVAL;
+	if (resp.ptr_mask & ~resp.args_mask)
+		return -EINVAL;
+	if (!resp.args_mask)
+		return -EINVAL;
+	for (i = 0; i < SECCOMP_REDIRECT_ARGS; i++) {
+		if (resp.ptr_mask & (1U << i)) {
+			if (!resp.ptr_len[i])
+				return -EINVAL;
+		} else if (resp.ptr_len[i]) {
+			return -EINVAL;
+		}
+	}
+	if (resp.ptr_mask) {
+		memfd_file = fget(resp.memfd);
+		if (!memfd_file)
+			return -EBADF;
+	}
+
+	restore = kzalloc_obj(*restore, GFP_KERNEL_ACCOUNT);
+	if (!restore) {
+		ret = -ENOMEM;
+		goto out_free;
+	}
+	init_task_work(&restore->twork, seccomp_redirect_restore_cb);
+
+	ret = mutex_lock_interruptible(&filter->notify_lock);
+	if (ret < 0)
+		goto out_free;
+
+	knotif = find_notification(filter, resp.id);
+	if (!knotif) {
+		ret = -ENOENT;
+		goto out_unlock_free;
+	}
+	if (knotif->state != SECCOMP_NOTIFY_SENT) {
+		ret = -EINPROGRESS;
+		goto out_unlock_free;
+	}
+
+	if (seccomp_redirect_deny(knotif->data)) {
+		ret = -EOPNOTSUPP;
+		goto out_unlock_free;
+	}
+
+#ifdef SECCOMP_ARCH_COMPAT
+	if (knotif->data->arch == SECCOMP_ARCH_COMPAT) {
+		for (i = 0; i < SECCOMP_REDIRECT_ARGS; i++)
+			if (resp.ptr_mask & (1U << i))
+				resp.args[i] = (u32)resp.args[i];
+	}
+#endif
+
+	for (i = 0; i < SECCOMP_REDIRECT_ARGS; i++) {
+		if (!(resp.ptr_mask & (1U << i)))
+			continue;
+		if (!seccomp_pin_check(knotif->task, memfd_file,
+				       resp.args[i], resp.ptr_len[i])) {
+			ret = -EFAULT;
+			goto out_unlock_free;
+		}
+	}
+
+	target_regs = task_pt_regs(knotif->task);
+	syscall_get_arguments(knotif->task, target_regs, args);
+
+	for (i = 0; i < SECCOMP_REDIRECT_ARGS; i++)
+		restore->orig_args[i] = args[i];
+	restore->args_mask = resp.args_mask;
+	restore->self_exec_id = READ_ONCE(knotif->task->self_exec_id);
+
+	for (i = 0; i < SECCOMP_REDIRECT_ARGS; i++)
+		if (resp.args_mask & (1U << i))
+			args[i] = resp.args[i];
+	syscall_set_arguments(knotif->task, target_regs, args);
+
+	/*
+	 * Use TWA_RESUME, not TWA_SIGNAL. TWA_SIGNAL sets TIF_NOTIFY_SIGNAL,
+	 * which makes signal_pending() true for the entire redirected syscall
+	 * An interruptible syscall would then bail out with -ERESTARTSYS before
+	 * doing any work, restart, re-trap and get redirected again, it is a
+	 * livelock. TWA_RESUME does not feed signal_pending(), and the restore
+	 * still runs before signal delivery: get_signal() runs task_work_run()
+	 * before it dequeues a signal, so the original args are back in pt_regs
+	 * before handle_signal() builds the sigframe or the -ERESTART* path
+	 * rewinds for restart.
+	 */
+	ret = task_work_add(knotif->task, &restore->twork, TWA_RESUME);
+	if (ret) {
+		for (i = 0; i < SECCOMP_REDIRECT_ARGS; i++)
+			args[i] = restore->orig_args[i];
+		syscall_set_arguments(knotif->task, target_regs, args);
+		goto out_unlock_free;
+	}
+
+	knotif->state = SECCOMP_NOTIFY_REPLIED;
+	knotif->error = 0;
+	knotif->val = 0;
+	knotif->flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE;
+	if (filter->notif->flags & SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP)
+		complete_on_current_cpu(&knotif->ready);
+	else
+		complete(&knotif->ready);
+
+	mutex_unlock(&filter->notify_lock);
+	if (memfd_file)
+		fput(memfd_file);
+	return 0;
+
+out_unlock_free:
+	mutex_unlock(&filter->notify_lock);
+out_free:
+	if (memfd_file)
+		fput(memfd_file);
+	kfree(restore);
+	return ret;
+}
+#else /* !SECCOMP_ARCH_REDIRECT */
+static long seccomp_notify_send_redirect(struct seccomp_filter *filter,
+					 struct seccomp_notif_resp_redirect __user *uresp,
+					 unsigned int size)
+{
+	return -EOPNOTSUPP;
+}
+#endif /* SECCOMP_ARCH_REDIRECT */
+
 static long seccomp_notify_ioctl(struct file *file, unsigned int cmd,
 				 unsigned long arg)
 {
@@ -1964,6 +2265,9 @@ static long seccomp_notify_ioctl(struct file *file, unsigned int cmd,
 	case EA_IOCTL(SECCOMP_IOCTL_NOTIF_PIN_INSTALL):
 		return seccomp_notify_pin_install(filter, buf,
 						  _IOC_SIZE(cmd));
+	case EA_IOCTL(SECCOMP_IOCTL_NOTIF_SEND_REDIRECT):
+		return seccomp_notify_send_redirect(filter, buf,
+						    _IOC_SIZE(cmd));
 	default:
 		return -EINVAL;
 	}
@@ -2103,6 +2407,15 @@ static long seccomp_set_mode_filter(unsigned int flags,
 	    ((flags & SECCOMP_FILTER_FLAG_NEW_LISTENER) == 0))
 		return -EINVAL;
 
+#ifndef SECCOMP_ARCH_REDIRECT
+	/* Redirect denylist inputs are not verified on this arch. */
+	if (flags & SECCOMP_FILTER_FLAG_REDIRECT)
+		return -EINVAL;
+#endif
+	if ((flags & SECCOMP_FILTER_FLAG_REDIRECT) &&
+	    ((flags & SECCOMP_FILTER_FLAG_NEW_LISTENER) == 0))
+		return -EINVAL;
+
 	/* Prepare the new filter before holding any locks. */
 	prepared = seccomp_prepare_user_filter(filter);
 	if (IS_ERR(prepared))
-- 
2.43.0



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

* [PATCH v6 6/8] seccomp: re-validate a redirected syscall against outer filters
  2026-07-15 21:19 [PATCH v6 0/8] seccomp: non-cooperative pinned-memfd argument redirect Cong Wang
                   ` (4 preceding siblings ...)
  2026-07-15 21:20 ` [PATCH v6 5/8] seccomp: add kernel-installed pinned-memfd redirect Cong Wang
@ 2026-07-15 21:20 ` Cong Wang
  2026-07-15 21:20 ` [PATCH v6 7/8] docs/seccomp: document pinned-memfd redirect ioctls Cong Wang
  2026-07-15 21:20 ` [PATCH v6] selftests/seccomp: cover non-cooperative pinned-memfd install Cong Wang
  7 siblings, 0 replies; 9+ messages in thread
From: Cong Wang @ 2026-07-15 21:20 UTC (permalink / raw)
  To: Andy Lutomirski
  Cc: Kees Cook, linux-kernel, Will Drewry, Christian Brauner,
	Andrew Morton, linux-mm, Cong Wang

From: Cong Wang <cwang@multikernel.io>

Stacked filters compose by taking the most restrictive verdict over one
evaluation of a single seccomp_data, assuming the syscall they voted on
is the syscall that runs. SECCOMP_IOCTL_NOTIF_SEND_REDIRECT breaks that:
the supervisor rewrites the argument registers and the syscall resumes
without the stack being re-consulted, so an inner, container-installed
filter can redirect a syscall into a form an outer filter would have
blocked.

Close the hole with seccomp_redirect_revalidate(): after a redirect it
walks from the notifier outward, judging the substituted syscall one
filter at a time; the innermost filter that does not allow it decides.
ALLOW and LOG fall through; ERRNO, TRAP and KILL are terminal;
USER_NOTIF consults the outer supervisor, whose plain FLAG_CONTINUE
keeps the walk going; TRACE fails closed with -ENOSYS, since a tracer
rewrite cannot be soundly re-composed mid-walk.

The walk is strictly outward, so the notifier is never reconsulted and
no re-notify loop exists, so a deep chain cannot exhaust the kernel
stack. A redirect never changes the syscall number, only the argument
registers differ.

Assisted-by: Claude:claude-opus-4.8
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
 kernel/seccomp.c | 141 ++++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 126 insertions(+), 15 deletions(-)

diff --git a/kernel/seccomp.c b/kernel/seccomp.c
index 17fe4c360839..b1fd2ec44324 100644
--- a/kernel/seccomp.c
+++ b/kernel/seccomp.c
@@ -93,6 +93,13 @@ struct seccomp_knotif {
 	long val;
 	u32 flags;
 
+	/*
+	 * Set by SEND_REDIRECT: the reply rewrote the syscall's registers,
+	 * so on resume the syscall must be re-evaluated against the filters
+	 * outer to the one that notified (see __seccomp_filter()).
+	 */
+	bool redirect;
+
 	/*
 	 * Signals when this has changed states, such as the listener
 	 * dying, a new seccomp addfd message, or changing to REPLIED
@@ -1183,10 +1190,12 @@ static bool should_sleep_killable(struct seccomp_filter *match,
 
 static int seccomp_do_user_notification(int this_syscall,
 					struct seccomp_filter *match,
-					const struct seccomp_data *sd)
+					const struct seccomp_data *sd,
+					bool *redirected)
 {
 	int err;
 	u32 flags = 0;
+	bool redirect = false;
 	long ret = 0;
 	struct seccomp_knotif n = {};
 	struct seccomp_kaddfd *addfd, *tmp;
@@ -1243,6 +1252,7 @@ static int seccomp_do_user_notification(int this_syscall,
 	ret = n.val;
 	err = n.error;
 	flags = n.flags;
+	redirect = n.redirect;
 
 interrupted:
 	/* If there were any pending addfd calls, clear them out */
@@ -1269,19 +1279,120 @@ static int seccomp_do_user_notification(int this_syscall,
 	mutex_unlock(&match->notify_lock);
 
 	/* Userspace requests to continue the syscall. */
-	if (flags & SECCOMP_USER_NOTIF_FLAG_CONTINUE)
+	if (flags & SECCOMP_USER_NOTIF_FLAG_CONTINUE) {
+		*redirected = redirect;
 		return 0;
+	}
 
 	syscall_set_return_value(current, current_pt_regs(),
 				 err, ret);
 	return -1;
 }
 
+static void seccomp_kill_task(int this_syscall, u32 action, int data)
+{
+	current->seccomp.mode = SECCOMP_MODE_DEAD;
+	seccomp_log(this_syscall, SIGSYS, action, true);
+	/* Dump core only if this is the last remaining thread. */
+	if (action != SECCOMP_RET_KILL_THREAD ||
+	    (atomic_read(&current->signal->live) == 1)) {
+		/* Show the original registers in the dump. */
+		syscall_rollback(current, current_pt_regs());
+		/* Trigger a coredump with SIGSYS */
+		force_sig_seccomp(this_syscall, data, true);
+	} else {
+		do_exit(SIGSYS);
+	}
+}
+
+static int seccomp_redirect_revalidate(struct seccomp_filter *notifier)
+{
+	struct seccomp_filter *f;
+	struct seccomp_data sd;
+	bool redirected = false;
+	int this_syscall;
+	u32 action;
+	int data;
+
+	populate_seccomp_data(&sd);
+	this_syscall = sd.nr;
+
+	for (f = notifier->prev; f; f = f->prev) {
+		u32 cur_ret = bpf_prog_run_pin_on_cpu(f->prog, &sd);
+
+		data = cur_ret & SECCOMP_RET_DATA;
+		action = cur_ret & SECCOMP_RET_ACTION_FULL;
+
+		switch (action) {
+		case SECCOMP_RET_ALLOW:
+			continue;
+
+		case SECCOMP_RET_LOG:
+			seccomp_log(this_syscall, 0, action, true);
+			continue;
+
+		case SECCOMP_RET_ERRNO:
+			/* Set low-order bits as an errno, capped at MAX_ERRNO. */
+			if (data > MAX_ERRNO)
+				data = MAX_ERRNO;
+			syscall_set_return_value(current, current_pt_regs(),
+						 -data, 0);
+			goto skip;
+
+		case SECCOMP_RET_TRAP:
+			/* Show the handler the original registers. */
+			syscall_rollback(current, current_pt_regs());
+			/* Let the filter pass back 16 bits of data. */
+			force_sig_seccomp(this_syscall, data, false);
+			goto skip;
+
+		case SECCOMP_RET_USER_NOTIF:
+			/*
+			 * The outer supervisor judges the substituted call:
+			 * an error reply skips it, a plain FLAG_CONTINUE
+			 * keeps the walk going, or this reply would slip the
+			 * call past a stricter filter further out. It cannot
+			 * redirect again: at most one redirect-capable
+			 * listener exists in a chain, and the walk starts
+			 * outside it.
+			 */
+			if (seccomp_do_user_notification(this_syscall, f, &sd,
+							 &redirected))
+				goto skip;
+			continue;
+
+		case SECCOMP_RET_TRACE:
+			/*
+			 * A tracer may rewrite the syscall, and there is no
+			 * defensible way to restart composition mid-walk.
+			 * Fail closed exactly like TRACE with no tracer
+			 * attached: skip with -ENOSYS.
+			 */
+			syscall_set_return_value(current, current_pt_regs(),
+						 -ENOSYS, 0);
+			goto skip;
+
+		case SECCOMP_RET_KILL_THREAD:
+		case SECCOMP_RET_KILL_PROCESS:
+		default:
+			seccomp_kill_task(this_syscall, action, data);
+			return -1;
+		}
+	}
+
+	return 0;
+
+skip:
+	seccomp_log(this_syscall, 0, action, f->log);
+	return -1;
+}
+
 static int __seccomp_filter(int this_syscall, const bool recheck_after_trace)
 {
 	u32 filter_ret, action;
 	struct seccomp_data sd;
 	struct seccomp_filter *match = NULL;
+	bool redirected = false;
 	int data;
 
 	/*
@@ -1356,9 +1467,19 @@ static int __seccomp_filter(int this_syscall, const bool recheck_after_trace)
 		return 0;
 
 	case SECCOMP_RET_USER_NOTIF:
-		if (seccomp_do_user_notification(this_syscall, match, &sd))
+		if (seccomp_do_user_notification(this_syscall, match, &sd,
+						 &redirected))
 			goto skip;
 
+		/*
+		 * A redirect rewrote the argument registers; every filter
+		 * outer to the notifier must judge the substituted syscall
+		 * before it runs. A redirect from the outermost filter has
+		 * no outer filter left to judge it.
+		 */
+		if (redirected && match->prev)
+			return seccomp_redirect_revalidate(match);
+
 		return 0;
 
 	case SECCOMP_RET_LOG:
@@ -1376,18 +1497,7 @@ static int __seccomp_filter(int this_syscall, const bool recheck_after_trace)
 	case SECCOMP_RET_KILL_THREAD:
 	case SECCOMP_RET_KILL_PROCESS:
 	default:
-		current->seccomp.mode = SECCOMP_MODE_DEAD;
-		seccomp_log(this_syscall, SIGSYS, action, true);
-		/* Dump core only if this is the last remaining thread. */
-		if (action != SECCOMP_RET_KILL_THREAD ||
-		    (atomic_read(&current->signal->live) == 1)) {
-			/* Show the original registers in the dump. */
-			syscall_rollback(current, current_pt_regs());
-			/* Trigger a coredump with SIGSYS */
-			force_sig_seccomp(this_syscall, data, true);
-		} else {
-			do_exit(SIGSYS);
-		}
+		seccomp_kill_task(this_syscall, action, data);
 		return -1; /* skip the syscall go directly to signal handling */
 	}
 
@@ -2207,6 +2317,7 @@ static long seccomp_notify_send_redirect(struct seccomp_filter *filter,
 		goto out_unlock_free;
 	}
 
+	knotif->redirect = true;
 	knotif->state = SECCOMP_NOTIFY_REPLIED;
 	knotif->error = 0;
 	knotif->val = 0;
-- 
2.43.0



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

* [PATCH v6 7/8] docs/seccomp: document pinned-memfd redirect ioctls
  2026-07-15 21:19 [PATCH v6 0/8] seccomp: non-cooperative pinned-memfd argument redirect Cong Wang
                   ` (5 preceding siblings ...)
  2026-07-15 21:20 ` [PATCH v6 6/8] seccomp: re-validate a redirected syscall against outer filters Cong Wang
@ 2026-07-15 21:20 ` Cong Wang
  2026-07-15 21:20 ` [PATCH v6] selftests/seccomp: cover non-cooperative pinned-memfd install Cong Wang
  7 siblings, 0 replies; 9+ messages in thread
From: Cong Wang @ 2026-07-15 21:20 UTC (permalink / raw)
  To: Andy Lutomirski
  Cc: Kees Cook, linux-kernel, Will Drewry, Christian Brauner,
	Andrew Morton, linux-mm, Cong Wang

From: Cong Wang <cwang@multikernel.io>

Document SECCOMP_IOCTL_NOTIF_PIN_INSTALL and
SECCOMP_IOCTL_NOTIF_SEND_REDIRECT in the userspace API guide: the
SECCOMP_FILTER_FLAG_REDIRECT opt-in and the single-redirector
restriction, the two response structures, and how the pair closes the
user-notification TOCTOU for non-cooperative fork+execve sandboxes.

Also spell out the scope the implementation deliberately enforces or
relies on: read-only input pointers only, same-syscall-number only
(rt_sigreturn and the clone/fork family are refused), the per-interruption
re-notification of restartable syscalls and the restart-block behaviour,
and the ptrace syscall-stop semantics.

Assisted-by: Claude:claude-opus-4.8
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
 .../userspace-api/seccomp_filter.rst          | 109 ++++++++++++++++++
 1 file changed, 109 insertions(+)

diff --git a/Documentation/userspace-api/seccomp_filter.rst b/Documentation/userspace-api/seccomp_filter.rst
index cff0fa7f3175..be2c338224ac 100644
--- a/Documentation/userspace-api/seccomp_filter.rst
+++ b/Documentation/userspace-api/seccomp_filter.rst
@@ -289,6 +289,115 @@ above in this document: all arguments being read from the tracee's memory
 should be read into the tracer's memory before any policy decisions are made.
 This allows for an atomic decision on syscall arguments.
 
+Non-cooperative pinned-memfd redirect
+=====================================
+
+The TOCTOU described above means ``SECCOMP_USER_NOTIF_FLAG_CONTINUE`` cannot
+enforce a policy on pointer arguments: after the supervisor inspects the
+target's memory and lets the syscall continue, the target (or a thread sharing
+its address space) can rewrite that memory before the kernel reads it. The
+cooperative workaround, the target ``mmap()`` + ``mseal()``-ing a shared
+buffer, is unavailable in the fork+execve sandbox model, where the supervisor
+confines a binary it did not write.
+
+Two ioctls let the supervisor close this race without target cooperation. The
+redirect step (below) requires a listener created with
+``SECCOMP_FILTER_FLAG_REDIRECT`` (in addition to
+``SECCOMP_FILTER_FLAG_NEW_LISTENER``). Because it rewrites another task's
+registers, at most one such listener may exist in a task's filter chain; a
+second fails with ``-EBUSY``:
+
+.. code-block:: c
+
+    fd = seccomp(SECCOMP_SET_MODE_FILTER,
+                 SECCOMP_FILTER_FLAG_NEW_LISTENER | SECCOMP_FILTER_FLAG_REDIRECT,
+                 &prog);
+
+``ioctl(SECCOMP_IOCTL_NOTIF_PIN_INSTALL)`` installs a sealed mapping of a
+supervisor-owned ``memfd`` directly into the trapped task's address space:
+
+.. code-block:: c
+
+    struct seccomp_notif_pin_install {
+        __u64 id;
+        __u32 flags;       /* reserved, must be 0 */
+        __u32 memfd;
+        __u64 target_addr;
+        __u64 size;
+        __u64 offset;      /* page-aligned offset into memfd */
+    };
+
+``id`` names an active notification (the trapped task to install into).
+``target_addr``, ``size`` and ``offset`` are page-aligned; ``offset`` selects
+where in ``memfd`` the mapping starts, so one memfd can back several pins. If
+``target_addr`` is ``0`` the kernel picks a free address and writes it back;
+otherwise an existing mapping there yields ``-EEXIST``. The pin is read-only
+and sealed, the target and its threads cannot unmap, move, reprotect or
+overwrite it, and lasts until the target calls ``execve()`` or exits.
+
+``memfd`` must be write-sealed (``F_SEAL_WRITE`` or ``F_SEAL_FUTURE_WRITE``)
+or the ioctl returns ``-EINVAL``; otherwise the target could rewrite the pin's
+bytes through a separate writable handle to the same memfd.
+``F_SEAL_FUTURE_WRITE`` still lets the supervisor update the contents through
+its own mapping made before the seal.
+
+``ioctl(SECCOMP_IOCTL_NOTIF_SEND_REDIRECT)`` then resumes the trapped syscall
+like ``SECCOMP_USER_NOTIF_FLAG_CONTINUE``, but with selected argument
+registers replaced:
+
+.. code-block:: c
+
+    struct seccomp_notif_resp_redirect {
+        __u64 id;
+        __u32 flags;      /* SECCOMP_REDIRECT_FLAG_CONTINUE must be set */
+        __u32 args_mask;  /* which arg registers to replace */
+        __u32 ptr_mask;   /* which of those are pointers into a pin */
+        __u32 memfd;      /* the pin's backing memfd */
+        __u64 args[6];    /* replacement values */
+        __u64 ptr_len[6]; /* validated access length for each pointer arg */
+    };
+
+Each bit in ``ptr_mask`` (a subset of ``args_mask``) marks ``args[i]`` as a
+pointer; the access ``[args[i], args[i] + ptr_len[i])`` must lie within a
+single read-only pin of ``memfd`` in the target, or the ioctl returns
+``-EFAULT``. ``ptr_len[i]`` must be non-zero for those bits and ``0``
+otherwise. Bits in ``args_mask`` but not ``ptr_mask`` are scalar replacements
+written verbatim, e.g. to set the length register that goes with a redirected
+pointer. The original registers are restored at syscall exit, so the
+substitution is invisible to the target and the TOCTOU is closed.
+
+Scope and limitations
+---------------------
+
+The redirect mechanism is deliberately narrow and is *not* a general syscall
+rewriting facility:
+
+- **Read-only input pointers only.** A pin is read-only, so only an argument
+  the syscall *reads* (a pathname, a ``sockaddr``) may be redirected into it.
+  Aiming an output or in/out argument at a pin makes the syscall fail with
+  ``-EFAULT`` when it writes back.
+
+- **Same syscall only.** A redirect replaces arguments, never the syscall
+  number. ``rt_sigreturn()`` (and its compat variant) cannot be redirected and
+  return ``-EOPNOTSUPP``.
+
+- **Signals and restarts.** The redirected syscall really runs, so it can be
+  interrupted and restarted. On a restart the original arguments are restored
+  and the syscall re-traps, so the supervisor is notified again and must answer
+  consistently. Syscalls the kernel restarts without re-trapping (e.g.
+  ``nanosleep()``, ``futex(FUTEX_WAIT)``) keep the substituted arguments --
+  safe for read-only inputs, but a reason not to redirect arguments of syscalls
+  that block or wait.
+
+- **clone()/fork().** The task-creation family (``clone``, ``clone3``,
+  ``fork``, ``vfork``) cannot be redirected and returns ``-EOPNOTSUPP``. A new
+  child inherits the substituted argument registers but not the restore, so it
+  would return to user space with the pinned values still in its registers.
+
+- **ptrace.** A tracer sees the substituted arguments at the syscall-exit stop;
+  they are restored before the task resumes, so a ``PTRACE_SETREGS`` of a
+  substituted register at that stop is overwritten.
+
 Sysctls
 =======
 
-- 
2.43.0



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

* [PATCH v6] selftests/seccomp: cover non-cooperative pinned-memfd install
  2026-07-15 21:19 [PATCH v6 0/8] seccomp: non-cooperative pinned-memfd argument redirect Cong Wang
                   ` (6 preceding siblings ...)
  2026-07-15 21:20 ` [PATCH v6 7/8] docs/seccomp: document pinned-memfd redirect ioctls Cong Wang
@ 2026-07-15 21:20 ` Cong Wang
  7 siblings, 0 replies; 9+ messages in thread
From: Cong Wang @ 2026-07-15 21:20 UTC (permalink / raw)
  To: Andy Lutomirski
  Cc: Kees Cook, linux-kernel, Will Drewry, Christian Brauner,
	Andrew Morton, linux-mm, Cong Wang

From: Cong Wang <cwang@multikernel.io>

Add 11 tests for SECCOMP_IOCTL_NOTIF_PIN_INSTALL and
SECCOMP_IOCTL_NOTIF_SEND_REDIRECT:

  - pinned_memfd_remote: basic install + redirect, plus the unsealed
    memfd and out-of-pin rejection paths
  - pinned_memfd_target_cannot_unmap: the sealed pin survives the
    target's munmap/mprotect/mremap/MAP_FIXED attacks (all EPERM)
  - pinned_memfd_execve_scm: SCM_RIGHTS supervisor handoff and re-pin
    in the fresh post-execve mm
  - pinned_memfd_churn: one listener serves many short-lived targets,
    no per-target state
  - redirect_outer_refilter, redirect_revalidate_chain: a redirect is
    re-validated by every outer filter, not just the nearest
  - redirect_outer_trace, redirect_outer_notify: outer TRACE and
    USER_NOTIF verdicts over a redirected call fail closed with ENOSYS;
    the tracer must not get a PTRACE_EVENT_SECCOMP for the substituted
    call, and a listener-less outer USER_NOTIF must still block it
  - redirect_denied_syscalls: EOPNOTSUPP for rt_sigreturn and the
    clone/fork family
  - pinned_memfd_abi, redirect_signal_abi: the original arg register is
    restored before returning to user mode, and before any signal frame
    is built

Assisted-by: Claude:claude-opus-4.8
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
 tools/testing/selftests/seccomp/seccomp_bpf.c | 1615 +++++++++++++++++
 1 file changed, 1615 insertions(+)

diff --git a/tools/testing/selftests/seccomp/seccomp_bpf.c b/tools/testing/selftests/seccomp/seccomp_bpf.c
index 358b6c65e120..abe8d6c969ab 100644
--- a/tools/testing/selftests/seccomp/seccomp_bpf.c
+++ b/tools/testing/selftests/seccomp/seccomp_bpf.c
@@ -217,6 +217,10 @@ struct seccomp_metadata {
 #define SECCOMP_FILTER_FLAG_NEW_LISTENER	(1UL << 3)
 #endif
 
+#ifndef SECCOMP_FILTER_FLAG_REDIRECT
+#define SECCOMP_FILTER_FLAG_REDIRECT		(1UL << 6)
+#endif
+
 #ifndef SECCOMP_RET_USER_NOTIF
 #define SECCOMP_RET_USER_NOTIF 0x7fc00000U
 
@@ -295,6 +299,35 @@ struct seccomp_notif_addfd_big {
 #define PTRACE_EVENTMSG_SYSCALL_EXIT	2
 #endif
 
+#ifndef SECCOMP_IOCTL_NOTIF_PIN_INSTALL
+struct seccomp_notif_pin_install {
+	__u64 id;
+	__u32 flags;
+	__u32 memfd;
+	__u64 target_addr;
+	__u64 size;
+	__u64 offset;
+};
+#define SECCOMP_IOCTL_NOTIF_PIN_INSTALL	SECCOMP_IOWR(5, \
+						struct seccomp_notif_pin_install)
+#endif
+
+#ifndef SECCOMP_IOCTL_NOTIF_SEND_REDIRECT
+#define SECCOMP_REDIRECT_FLAG_CONTINUE (1UL << 0)
+#define SECCOMP_REDIRECT_ARGS 6
+struct seccomp_notif_resp_redirect {
+	__u64 id;
+	__u32 flags;
+	__u32 args_mask;
+	__u32 ptr_mask;
+	__u32 memfd;
+	__u64 args[SECCOMP_REDIRECT_ARGS];
+	__u64 ptr_len[SECCOMP_REDIRECT_ARGS];
+};
+#define SECCOMP_IOCTL_NOTIF_SEND_REDIRECT	SECCOMP_IOW(6, \
+						struct seccomp_notif_resp_redirect)
+#endif
+
 #ifndef SECCOMP_USER_NOTIF_FLAG_CONTINUE
 #define SECCOMP_USER_NOTIF_FLAG_CONTINUE 0x00000001
 #endif
@@ -4368,6 +4401,1588 @@ TEST(user_notification_addfd_rlimit)
 	close(memfd);
 }
 
+/*
+ * Create a write-sealed memfd of @size for PIN_INSTALL and map a supervisor
+ * writable view, primed with @content. F_SEAL_FUTURE_WRITE keeps this
+ * pre-seal mapping writable (so the test can still stage content) while
+ * barring any other writable reference, as PIN_INSTALL requires. Returns
+ * the memfd.
+ */
+static int make_pin_memfd(struct __test_metadata *_metadata, const char *name,
+			  size_t size, char **sup_view, const char *content)
+{
+	int memfd = memfd_create(name, MFD_ALLOW_SEALING);
+
+	ASSERT_GE(memfd, 0);
+	ASSERT_EQ(0, ftruncate(memfd, size));
+	ASSERT_EQ(0, fcntl(memfd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW));
+
+	*sup_view = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED,
+			 memfd, 0);
+	ASSERT_NE(MAP_FAILED, *sup_view);
+	ASSERT_EQ(0, fcntl(memfd, F_ADD_SEALS, F_SEAL_FUTURE_WRITE));
+	memcpy(*sup_view, content, strlen(content) + 1);
+	return memfd;
+}
+
+/*
+ * Non-cooperative pinned-memfd: kernel installs a sealed PROT_READ
+ * MAP_SHARED mapping of the supervisor's memfd directly into the
+ * trapped task's mm. The target runs no mmap or mseal code itself —
+ * this exercises the same kernel path that a fork+execve sandbox
+ * supervisor would use to install a pin in the new image's fresh
+ * post-exec mm.
+ *
+ * Target child does nothing but call openat() on a bait path. The
+ * supervisor catches the trap, calls PIN_INSTALL (kernel does the
+ * mmap + seal in target's mm via vm_mmap_remote()), writes a
+ * safe path into its own memfd view, and SEND_REDIRECTs args[1]
+ * into the freshly installed pin. The child's openat resumes,
+ * reads from the sealed pin, and returns an fd to the safe path.
+ */
+TEST(user_notification_pinned_memfd_remote)
+{
+	pid_t pid;
+	long ret;
+	int status, listener, memfd, unsealed;
+	struct seccomp_notif req = {};
+	struct seccomp_notif_pin_install pin = {};
+	struct seccomp_notif_pin_install unsealed_pin = {};
+	struct seccomp_notif_resp_redirect redir = {};
+	char *sup_view;
+	const size_t PIN_SIZE = 4096;
+	const char *safe_path = "/dev/null";
+
+	ret = prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+	ASSERT_EQ(0, ret) {
+		TH_LOG("Kernel does not support PR_SET_NO_NEW_PRIVS!");
+	}
+
+	memfd = make_pin_memfd(_metadata, "pinned-remote", PIN_SIZE,
+			       &sup_view, safe_path);
+
+	listener = user_notif_syscall(__NR_openat,
+				      SECCOMP_FILTER_FLAG_NEW_LISTENER |
+				      SECCOMP_FILTER_FLAG_REDIRECT);
+	ASSERT_GE(listener, 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+
+	if (pid == 0) {
+		int fd;
+
+		/*
+		 * Target performs no setup. Just trap on openat. Kernel
+		 * (driven by the supervisor) will install the pin in this
+		 * process's mm at a kernel-chosen address behind our back,
+		 * and our openat will be redirected to read from there.
+		 */
+		fd = syscall(__NR_openat, AT_FDCWD,
+			     "/this/should/never/be/touched", O_RDONLY, 0);
+		if (fd < 0)
+			_exit(11);
+		_exit(0);
+	}
+
+	ASSERT_EQ(0, ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req));
+	EXPECT_EQ(req.data.nr, __NR_openat);
+
+	pin.id = req.id;
+	pin.memfd = memfd;
+	pin.target_addr = 0;
+	pin.size = PIN_SIZE;
+	EXPECT_EQ(0, ioctl(listener, SECCOMP_IOCTL_NOTIF_PIN_INSTALL, &pin)) {
+		if (errno == EINVAL) {
+			kill(pid, SIGKILL);
+			waitpid(pid, &status, 0);
+			SKIP(goto cleanup,
+			     "Kernel does not support pinned-memfd remote install");
+		}
+		TH_LOG("PIN_INSTALL failed: errno=%d", errno);
+	}
+
+	/* The kernel wrote a non-zero, page-aligned address back to us. */
+	EXPECT_NE(0, pin.target_addr);
+	EXPECT_EQ(0, pin.target_addr & (PIN_SIZE - 1));
+
+	/* Reject: the backing memfd must be write-sealed. */
+	unsealed = memfd_create("unsealed", MFD_ALLOW_SEALING);
+	ASSERT_GE(unsealed, 0);
+	ASSERT_EQ(0, ftruncate(unsealed, PIN_SIZE));
+	unsealed_pin.id = req.id;
+	unsealed_pin.memfd = unsealed;
+	unsealed_pin.size = PIN_SIZE;
+	EXPECT_EQ(-1, ioctl(listener, SECCOMP_IOCTL_NOTIF_PIN_INSTALL,
+			    &unsealed_pin));
+	EXPECT_EQ(EINVAL, errno);
+	close(unsealed);
+
+	/* Reject: redirect outside any installed pin. */
+	redir.id = req.id;
+	redir.flags = SECCOMP_REDIRECT_FLAG_CONTINUE;
+	redir.args_mask = 1U << 1;
+	redir.ptr_mask = 1U << 1;
+	redir.memfd = memfd;
+	redir.ptr_len[1] = strlen(safe_path) + 1;
+	redir.args[1] = pin.target_addr + PIN_SIZE;	/* one byte past */
+	EXPECT_EQ(-1, ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND_REDIRECT,
+			    &redir));
+	EXPECT_EQ(EFAULT, errno);
+
+	/* Reject: base is inside the pin but the extent runs past its end. */
+	redir.args[1] = pin.target_addr;
+	redir.ptr_len[1] = PIN_SIZE + 1;
+	EXPECT_EQ(-1, ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND_REDIRECT,
+			    &redir));
+	EXPECT_EQ(EFAULT, errno);
+
+	/* Happy path: redirect into the kernel-installed pin. */
+	redir.args[1] = pin.target_addr;
+	redir.ptr_len[1] = strlen(safe_path) + 1;
+	EXPECT_EQ(0, ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND_REDIRECT,
+			   &redir)) {
+		/* Unblock the trapped child so waitpid() cannot hang. */
+		kill(pid, SIGKILL);
+	}
+
+	EXPECT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(true, WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status)) {
+		TH_LOG("child exit %d (11=openat fail)", WEXITSTATUS(status));
+	}
+
+cleanup:
+	munmap(sup_view, PIN_SIZE);
+	close(memfd);
+	close(listener);
+}
+
+/*
+ * The installed pin is VM_SEALED. A hostile target that learns the pin
+ * address must still be unable to unmap, move, reprotect or MAP_FIXED-stomp
+ * it, though it may read it. This is the property that keeps a redirected
+ * pointer aimed at supervisor-controlled bytes for the whole syscall.
+ */
+TEST(user_notification_pinned_memfd_target_cannot_unmap)
+{
+	pid_t pid;
+	long ret;
+	int status, listener, memfd;
+	struct seccomp_notif req = {};
+	struct seccomp_notif_pin_install pin = {};
+	struct seccomp_notif_resp_redirect redir = {};
+	char *sup_view;
+	int addrpipe[2];
+	const size_t PIN_SIZE = 4096;
+	const char *safe_path = "/dev/null";
+
+	ret = prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+	ASSERT_EQ(0, ret) {
+		TH_LOG("Kernel does not support PR_SET_NO_NEW_PRIVS!");
+	}
+
+	ASSERT_EQ(0, pipe(addrpipe));
+
+	memfd = make_pin_memfd(_metadata, "pin-nounmap", PIN_SIZE,
+			       &sup_view, safe_path);
+
+	listener = user_notif_syscall(__NR_openat,
+				      SECCOMP_FILTER_FLAG_NEW_LISTENER |
+				      SECCOMP_FILTER_FLAG_REDIRECT);
+	ASSERT_GE(listener, 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+
+	if (pid == 0) {
+		unsigned long pin_addr;
+		char *p;
+		int fd;
+
+		close(addrpipe[1]);
+
+		/* Trap; the supervisor installs+seals the pin and redirects. */
+		fd = syscall(__NR_openat, AT_FDCWD,
+			     "/this/should/never/be/touched", O_RDONLY, 0);
+		if (fd < 0)
+			_exit(11);
+
+		/* Learn where the kernel installed the pin. */
+		if (read(addrpipe[0], &pin_addr, sizeof(pin_addr)) !=
+		    sizeof(pin_addr))
+			_exit(12);
+		p = (char *)pin_addr;
+
+		/* Mapped and readable: it holds the redirected path. */
+		if (*p != '/')
+			_exit(13);
+
+		/* Sealed: unmap must fail and tear nothing down. */
+		if (munmap((void *)pin_addr, PIN_SIZE) == 0)
+			_exit(20);
+		if (errno != EPERM)
+			_exit(21);
+
+		/* Sealed: cannot reprotect it (even to PROT_NONE). */
+		if (mprotect((void *)pin_addr, PIN_SIZE, PROT_NONE) == 0)
+			_exit(22);
+		if (errno != EPERM)
+			_exit(23);
+
+		/* Sealed: cannot move or resize it. */
+		if (mremap((void *)pin_addr, PIN_SIZE, PIN_SIZE * 2,
+			   MREMAP_MAYMOVE) != MAP_FAILED)
+			_exit(24);
+		if (errno != EPERM)
+			_exit(25);
+
+		/* Sealed: cannot be replaced by a MAP_FIXED mapping. */
+		if (mmap((void *)pin_addr, PIN_SIZE, PROT_READ,
+			 MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) !=
+		    MAP_FAILED)
+			_exit(26);
+		if (errno != EPERM)
+			_exit(27);
+
+		/* Survived every attack, still mapped and readable. */
+		if (*p != '/')
+			_exit(28);
+		_exit(0);
+	}
+
+	close(addrpipe[0]);
+
+	ASSERT_EQ(0, ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req));
+	EXPECT_EQ(req.data.nr, __NR_openat);
+
+	pin.id = req.id;
+	pin.memfd = memfd;
+	pin.target_addr = 0;
+	pin.size = PIN_SIZE;
+	EXPECT_EQ(0, ioctl(listener, SECCOMP_IOCTL_NOTIF_PIN_INSTALL, &pin)) {
+		if (errno == EINVAL) {
+			kill(pid, SIGKILL);
+			waitpid(pid, &status, 0);
+			SKIP(goto cleanup,
+			     "Kernel does not support pinned-memfd remote install");
+		}
+		TH_LOG("PIN_INSTALL failed: errno=%d", errno);
+	}
+
+	redir.id = req.id;
+	redir.flags = SECCOMP_REDIRECT_FLAG_CONTINUE;
+	redir.args_mask = 1U << 1;
+	redir.ptr_mask = 1U << 1;
+	redir.memfd = memfd;
+	redir.ptr_len[1] = strlen(safe_path) + 1;
+	redir.args[1] = pin.target_addr;
+	EXPECT_EQ(0, ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND_REDIRECT, &redir)) {
+		kill(pid, SIGKILL);
+	}
+
+	/* Hand the target the pin address so it can try to attack it. */
+	EXPECT_EQ(sizeof(pin.target_addr),
+		  write(addrpipe[1], &pin.target_addr, sizeof(pin.target_addr)));
+
+	EXPECT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(true, WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status)) {
+		TH_LOG("child exit %d (>=20 = seal breach)",
+		       WEXITSTATUS(status));
+	}
+
+cleanup:
+	close(addrpipe[1]);
+	munmap(sup_view, PIN_SIZE);
+	close(memfd);
+	close(listener);
+}
+
+/*
+ * Helper for the execve test: read up to @max bytes of a NUL-terminated
+ * string from @pid's mm at @addr into @out. Returns the length read
+ * (excluding the NUL), or -1 on failure or no NUL.
+ */
+static ssize_t read_remote_string(pid_t pid, unsigned long addr,
+				  char *out, size_t max)
+{
+	struct iovec local = { .iov_base = out, .iov_len = max };
+	struct iovec remote = { .iov_base = (void *)addr, .iov_len = max };
+	ssize_t n;
+	size_t i;
+
+	n = process_vm_readv(pid, &local, 1, &remote, 1, 0);
+	if (n <= 0)
+		return -1;
+	for (i = 0; i < (size_t)n; i++)
+		if (out[i] == '\0')
+			return (ssize_t)i;
+	return -1;
+}
+
+/*
+ * Send a file descriptor over a connected UNIX socket via SCM_RIGHTS.
+ * Used by the execve_scm test so the target child can hand its
+ * SECCOMP_FILTER_FLAG_NEW_LISTENER fd to the supervising parent
+ * without the parent having to inherit the seccomp filter itself.
+ */
+static int send_fd(int sock, int fd)
+{
+	char cbuf[CMSG_SPACE(sizeof(int))] = {};
+	char data = 'x';
+	struct iovec iov = { .iov_base = &data, .iov_len = 1 };
+	struct msghdr msg = {
+		.msg_iov = &iov, .msg_iovlen = 1,
+		.msg_control = cbuf, .msg_controllen = sizeof(cbuf),
+	};
+	struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+
+	cmsg->cmsg_level = SOL_SOCKET;
+	cmsg->cmsg_type = SCM_RIGHTS;
+	cmsg->cmsg_len = CMSG_LEN(sizeof(int));
+	memcpy(CMSG_DATA(cmsg), &fd, sizeof(int));
+	return sendmsg(sock, &msg, 0) < 0 ? -1 : 0;
+}
+
+static int recv_fd(int sock)
+{
+	char cbuf[CMSG_SPACE(sizeof(int))] = {};
+	char data;
+	struct iovec iov = { .iov_base = &data, .iov_len = 1 };
+	struct msghdr msg = {
+		.msg_iov = &iov, .msg_iovlen = 1,
+		.msg_control = cbuf, .msg_controllen = sizeof(cbuf),
+	};
+	struct cmsghdr *cmsg;
+	int fd;
+
+	if (recvmsg(sock, &msg, 0) < 0)
+		return -1;
+	cmsg = CMSG_FIRSTHDR(&msg);
+	if (!cmsg || cmsg->cmsg_level != SOL_SOCKET ||
+	    cmsg->cmsg_type != SCM_RIGHTS ||
+	    cmsg->cmsg_len != CMSG_LEN(sizeof(int)))
+		return -1;
+	memcpy(&fd, CMSG_DATA(cmsg), sizeof(int));
+	return fd;
+}
+
+struct addr_range {
+	unsigned long start, end;
+};
+
+/*
+ * Parse /proc/<pid>/maps looking for the dynamic linker's executable
+ * mapping (glibc ld-linux-*.so, musl ld-musl-*.so, etc.). The trapped
+ * task's instruction_pointer falling in this range identifies a
+ * loader-bootstrap syscall (race-free, kernel-truth) so the supervisor
+ * can auto-allow it without inspecting argument content via the racy
+ * process_vm_readv path.
+ *
+ * Requires the supervisor not to be subject to the seccomp filter
+ * itself -- fopen() internally calls openat(). The execve_scm test
+ * structure (child installs filter, sends listener fd to parent via
+ * SCM_RIGHTS) satisfies that.
+ *
+ * Returns 0 on success with @out populated, -1 if not found.
+ */
+static int find_loader_text_range(pid_t pid, struct addr_range *out)
+{
+	char maps_path[64];
+	char line[512];
+	FILE *f;
+	int found = 0;
+
+	snprintf(maps_path, sizeof(maps_path), "/proc/%d/maps", pid);
+	f = fopen(maps_path, "r");
+	if (!f)
+		return -1;
+
+	while (fgets(line, sizeof(line), f)) {
+		unsigned long start, end;
+		char perms[8];
+		char *path;
+
+		if (sscanf(line, "%lx-%lx %7s", &start, &end, perms) != 3)
+			continue;
+		if (!strchr(perms, 'x'))
+			continue;
+		path = strchr(line, '/');
+		if (!path)
+			continue;
+		/*
+		 * Match common dynamic-linker basenames: ld-linux-*.so
+		 * (glibc), ld-musl-*.so (musl), ld-*.so (older glibc).
+		 */
+		if (strstr(path, "/ld-") || strstr(path, "/ld.so")) {
+			out->start = start;
+			out->end = end;
+			found = 1;
+			break;
+		}
+	}
+	fclose(f);
+	return found ? 0 : -1;
+}
+
+/*
+ * Non-cooperative pinned-memfd across a real execve, using the proper
+ * supervisor-isolation pattern: the child (target) installs the seccomp
+ * filter on itself and sends its listener fd to the parent (supervisor)
+ * via SCM_RIGHTS over a socketpair. The parent therefore does not carry
+ * the seccomp filter and can freely call openat() -- which is what makes
+ * the race-free, kernel-truth loader detection (req.data.instruction_pointer
+ * + /proc/<pid>/maps) actually usable.
+ *
+ * Phase 1: child does a pre-execve openat; the supervisor PIN_INSTALLs and
+ * SEND_REDIRECTs. Phase 2: child execve's, so the pre-execve pin VMA dies
+ * with the old mm. Phase 3: in the fresh post-execve mm the supervisor
+ * PIN_INSTALLs again (idempotent replace of the stale bookkeeping) and
+ * SEND_REDIRECTs, proving the full redirect mechanism survives an mm
+ * replacement, not just the install side.
+ */
+TEST(user_notification_pinned_memfd_execve_scm)
+{
+	pid_t pid;
+	int status, listener, memfd, sv[2];
+	struct seccomp_notif req = {};
+	struct seccomp_notif_pin_install pin = {};
+	struct seccomp_notif_resp_redirect redir = {};
+	struct seccomp_notif_resp cont_resp = {};
+	char *sup_view;
+	const size_t PIN_SIZE = 4096;
+	const char *safe_path = "/dev/null";
+	const char *bait = "/seccomp_pinned_memfd_test_bait_scm";
+	bool post_exec_install_ok = false;
+	bool post_exec_redirect_done = false;
+	bool loader_known = false;
+	bool loader_check_attempted = false;
+	struct addr_range loader_range = {};
+	int phase = 0;
+	int trap_count = 0;
+	const int trap_limit = 200;
+
+	if (access("/bin/cat", X_OK) != 0)
+		SKIP(return, "/bin/cat not present");
+
+	memfd = make_pin_memfd(_metadata, "pin-execve-scm", PIN_SIZE,
+			       &sup_view, safe_path);
+
+	ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sv));
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+
+	if (pid == 0) {
+		struct sock_filter filter[] = {
+			BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
+				 offsetof(struct seccomp_data, nr)),
+			BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_openat,
+				 0, 1),
+			BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_USER_NOTIF),
+			BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
+		};
+		struct sock_fprog prog = {
+			.len = (unsigned short)ARRAY_SIZE(filter),
+			.filter = filter,
+		};
+		int my_listener;
+		int fd;
+
+		close(sv[0]);
+		if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
+			_exit(20);
+		my_listener = seccomp(SECCOMP_SET_MODE_FILTER,
+				      SECCOMP_FILTER_FLAG_NEW_LISTENER |
+				      SECCOMP_FILTER_FLAG_REDIRECT,
+				      &prog);
+		if (my_listener < 0)
+			_exit(21);
+		if (send_fd(sv[1], my_listener) < 0)
+			_exit(22);
+		close(my_listener);
+		close(sv[1]);
+
+		/* Pre-execve trap. */
+		fd = syscall(__NR_openat, AT_FDCWD,
+			     "/this/should/never/be/touched", O_RDONLY, 0);
+		if (fd < 0)
+			_exit(11);
+
+		execl("/bin/cat", "cat", bait, (char *)NULL);
+		_exit(12);
+	}
+
+	close(sv[1]);
+	listener = recv_fd(sv[0]);
+	close(sv[0]);
+	ASSERT_GE(listener, 0);
+
+	/*
+	 * Parent has the listener fd and does NOT have the seccomp
+	 * filter. fopen(/proc/<pid>/maps) below works without
+	 * deadlocking on the parent's own openat.
+	 */
+	for (;;) {
+		struct pollfd pfd = { .fd = listener, .events = POLLIN };
+		int pret = poll(&pfd, 1, 500);
+		pid_t reaped;
+		bool ip_in_loader;
+
+		if (pret < 0)
+			break;
+		if (pret == 0 || !(pfd.revents & POLLIN)) {
+			reaped = waitpid(pid, &status, WNOHANG);
+			if (reaped == pid)
+				break;
+			if (pfd.revents & (POLLHUP | POLLERR))
+				break;
+			continue;
+		}
+
+		memset(&req, 0, sizeof(req));
+		if (ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req) < 0) {
+			TH_LOG("NOTIF_RECV failed: errno=%d", errno);
+			break;
+		}
+		if (++trap_count > trap_limit) {
+			TH_LOG("trap_limit (%d) exceeded", trap_limit);
+			break;
+		}
+
+		if (phase == 0) {
+			pin.id = req.id;
+			pin.memfd = memfd;
+			pin.target_addr = 0;
+			pin.size = PIN_SIZE;
+			if (ioctl(listener,
+				  SECCOMP_IOCTL_NOTIF_PIN_INSTALL,
+				  &pin) != 0) {
+				TH_LOG("pre-exec PIN_INSTALL failed: errno=%d",
+				       errno);
+				if (errno == EINVAL)
+					SKIP(goto cleanup_scm,
+					     "Kernel lacks pinned-memfd remote");
+				goto cleanup_scm;
+			}
+
+			memset(&redir, 0, sizeof(redir));
+			redir.id = req.id;
+			redir.flags = SECCOMP_REDIRECT_FLAG_CONTINUE;
+			redir.args_mask = 1U << 1;
+			redir.ptr_mask = 1U << 1;
+			redir.memfd = memfd;
+			redir.ptr_len[1] = strlen(safe_path) + 1;
+			redir.args[1] = pin.target_addr;
+			if (ioctl(listener,
+				  SECCOMP_IOCTL_NOTIF_SEND_REDIRECT,
+				  &redir) != 0) {
+				TH_LOG("pre-exec SEND_REDIRECT failed: errno=%d",
+				       errno);
+				goto cleanup_scm;
+			}
+			phase = 1;
+			continue;
+		}
+
+		/*
+		 * Post-execve. Lazily resolve the loader range. The
+		 * supervisor's own openat (fopen on /proc/<pid>/maps)
+		 * doesn't trap because the filter lives on the child,
+		 * not on us.
+		 */
+		if (!loader_known && !loader_check_attempted) {
+			if (find_loader_text_range(req.pid,
+						   &loader_range) == 0)
+				loader_known = true;
+			loader_check_attempted = true;
+		}
+
+		ip_in_loader = loader_known &&
+			req.data.instruction_pointer >= loader_range.start &&
+			req.data.instruction_pointer <  loader_range.end;
+
+		if (ip_in_loader) {
+			memset(&cont_resp, 0, sizeof(cont_resp));
+			cont_resp.id = req.id;
+			cont_resp.flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE;
+			ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &cont_resp);
+			continue;
+		}
+
+		/* Program code: inspect the path to identify the bait. */
+		{
+			char path[PATH_MAX];
+			ssize_t n;
+
+			n = read_remote_string(req.pid, req.data.args[1],
+					       path, sizeof(path));
+			if (n < 0 || strcmp(path, bait) != 0) {
+				memset(&cont_resp, 0, sizeof(cont_resp));
+				cont_resp.id = req.id;
+				cont_resp.flags =
+					SECCOMP_USER_NOTIF_FLAG_CONTINUE;
+				ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND,
+				      &cont_resp);
+				continue;
+			}
+
+			pin.id = req.id;
+			pin.memfd = memfd;
+			pin.target_addr = 0;
+			pin.size = PIN_SIZE;
+			if (ioctl(listener,
+				  SECCOMP_IOCTL_NOTIF_PIN_INSTALL,
+				  &pin) == 0) {
+				post_exec_install_ok = true;
+			} else {
+				TH_LOG("post-exec PIN_INSTALL failed: errno=%d",
+				       errno);
+				memset(&cont_resp, 0, sizeof(cont_resp));
+				cont_resp.id = req.id;
+				cont_resp.flags =
+					SECCOMP_USER_NOTIF_FLAG_CONTINUE;
+				ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND,
+				      &cont_resp);
+				continue;
+			}
+
+			memset(&redir, 0, sizeof(redir));
+			redir.id = req.id;
+			redir.flags = SECCOMP_REDIRECT_FLAG_CONTINUE;
+			redir.args_mask = 1U << 1;
+			redir.ptr_mask = 1U << 1;
+			redir.memfd = memfd;
+			redir.ptr_len[1] = strlen(safe_path) + 1;
+			redir.args[1] = pin.target_addr;
+			if (ioctl(listener,
+				  SECCOMP_IOCTL_NOTIF_SEND_REDIRECT,
+				  &redir) == 0) {
+				post_exec_redirect_done = true;
+			} else {
+				TH_LOG("post-exec SEND_REDIRECT failed: errno=%d",
+				       errno);
+				memset(&cont_resp, 0, sizeof(cont_resp));
+				cont_resp.id = req.id;
+				cont_resp.flags =
+					SECCOMP_USER_NOTIF_FLAG_CONTINUE;
+				ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND,
+				      &cont_resp);
+			}
+		}
+	}
+
+	if (waitpid(pid, &status, WNOHANG) == 0) {
+		kill(pid, SIGKILL);
+		waitpid(pid, &status, 0);
+	}
+	EXPECT_EQ(true, loader_known) {
+		TH_LOG("find_loader_text_range never resolved");
+	}
+	EXPECT_EQ(true, post_exec_install_ok);
+	EXPECT_EQ(true, post_exec_redirect_done);
+	EXPECT_EQ(true, WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status));
+
+cleanup_scm:
+	if (waitpid(pid, &status, WNOHANG) == 0) {
+		kill(pid, SIGKILL);
+		waitpid(pid, &status, 0);
+	}
+	munmap(sup_view, PIN_SIZE);
+	close(memfd);
+	close(listener);
+}
+
+/*
+ * Stateless redirect validation must hold up across many short-lived
+ * targets over one listener, and must not accumulate per-target state.
+ *
+ * PIN_INSTALL records nothing: the installed VM_SEALED VMA is the only
+ * record, and SEND_REDIRECT re-validates the pointer against the live
+ * mapping (sealed, read-only, backed by the supervisor's memfd inode).
+ * So a supervisor servicing a long churn of targets keeps working with
+ * no bookkeeping to leak. Each iteration lets the kernel choose the pin
+ * address in the fresh target mm; every install/redirect must succeed, and
+ * kmemleak/KASAN over the loop confirms nothing accumulates.
+ */
+TEST(user_notification_pinned_memfd_churn)
+{
+	const size_t PIN_SIZE = 4096;
+	const char *safe_path = "/dev/null";
+	const int iters = 16;
+	int listener, memfd, i;
+	char *sup_view;
+	long ret;
+
+	ret = prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+	ASSERT_EQ(0, ret) {
+		TH_LOG("Kernel does not support PR_SET_NO_NEW_PRIVS!");
+	}
+
+	memfd = make_pin_memfd(_metadata, "pinned-reap", PIN_SIZE,
+			       &sup_view, safe_path);
+
+	listener = user_notif_syscall(__NR_openat,
+				      SECCOMP_FILTER_FLAG_NEW_LISTENER |
+				      SECCOMP_FILTER_FLAG_REDIRECT);
+	ASSERT_GE(listener, 0);
+
+	for (i = 0; i < iters; i++) {
+		struct seccomp_notif req = {};
+		struct seccomp_notif_pin_install pin = {};
+		struct seccomp_notif_resp_redirect redir = {};
+		int status;
+		pid_t pid;
+
+		pid = fork();
+		ASSERT_GE(pid, 0);
+		if (pid == 0) {
+			int fd = syscall(__NR_openat, AT_FDCWD,
+					 "/never/touched", O_RDONLY, 0);
+			_exit(fd < 0 ? 11 : 0);
+		}
+
+		ASSERT_EQ(0, ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req));
+		EXPECT_EQ(req.data.nr, __NR_openat);
+
+		pin.id = req.id;
+		pin.memfd = memfd;
+		pin.target_addr = 0;
+		pin.size = PIN_SIZE;
+		EXPECT_EQ(0, ioctl(listener, SECCOMP_IOCTL_NOTIF_PIN_INSTALL,
+				   &pin)) {
+			if (errno == EINVAL) {
+				kill(pid, SIGKILL);
+				waitpid(pid, &status, 0);
+				SKIP(goto cleanup,
+				     "Kernel lacks pinned-memfd remote install");
+			}
+			TH_LOG("iter %d PIN_INSTALL failed: errno=%d", i, errno);
+		}
+
+		redir.id = req.id;
+		redir.flags = SECCOMP_REDIRECT_FLAG_CONTINUE;
+		redir.args_mask = 1U << 1;
+		redir.ptr_mask = 1U << 1;
+		redir.memfd = memfd;
+		redir.ptr_len[1] = strlen(safe_path) + 1;
+		redir.args[1] = pin.target_addr;
+		EXPECT_EQ(0, ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND_REDIRECT,
+				   &redir)) {
+			kill(pid, SIGKILL);
+		}
+
+		EXPECT_EQ(waitpid(pid, &status, 0), pid);
+		EXPECT_EQ(true, WIFEXITED(status));
+		EXPECT_EQ(0, WEXITSTATUS(status)) {
+			TH_LOG("iter %d child exit %d (11=openat fail)",
+			       i, WEXITSTATUS(status));
+		}
+		/*
+		 * Target is dead now; its pin (this iter's mm, at the
+		 * kernel-chosen address) is stale. The next iteration's
+		 * PIN_INSTALL walk must reap it rather than leak the range +
+		 * mm + memfd reference.
+		 */
+	}
+
+cleanup:
+	munmap(sup_view, PIN_SIZE);
+	close(memfd);
+	close(listener);
+}
+
+#ifdef __NR_socket
+/*
+ * A redirect must not let an inner (more recently installed) filter's
+ * notifier smuggle a syscall past an outer filter. Two filters are
+ * stacked on the target:
+ *
+ *   outer (installed first):  socket(AF_INET, ...) -> RET_ERRNO(EACCES),
+ *                             everything else ALLOW.
+ *   inner (installed second): socket -> RET_USER_NOTIF.
+ *
+ * The child calls socket(AF_UNIX, ...), which the outer filter allows, so
+ * the inner notifier wins and fires. The supervisor SEND_REDIRECTs arg0
+ * to AF_INET. The kernel must then re-run the outer filter against the
+ * rewritten registers and block it with EACCES; without the outer-suffix
+ * re-validation the inner filter would have bypassed the outer policy.
+ */
+TEST(user_notification_redirect_outer_refilter)
+{
+	struct sock_filter outer_filter[] = {
+		BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
+			 offsetof(struct seccomp_data, nr)),
+		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_socket, 0, 3),
+		BPF_STMT(BPF_LD | BPF_W | BPF_ABS, syscall_arg(0)),
+		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET, 0, 1),
+		BPF_STMT(BPF_RET | BPF_K,
+			 SECCOMP_RET_ERRNO | (EACCES & SECCOMP_RET_DATA)),
+		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
+	};
+	struct sock_fprog outer_prog = {
+		.len = (unsigned short)ARRAY_SIZE(outer_filter),
+		.filter = outer_filter,
+	};
+	struct seccomp_notif req = {};
+	struct seccomp_notif_resp_redirect redir = {};
+	int status, listener;
+	pid_t pid;
+	long ret;
+
+	ret = prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+	ASSERT_EQ(0, ret) {
+		TH_LOG("Kernel does not support PR_SET_NO_NEW_PRIVS!");
+	}
+
+	/* Outer filter first => it becomes the outer/root of the stack. */
+	ASSERT_EQ(0, seccomp(SECCOMP_SET_MODE_FILTER, 0, &outer_prog));
+
+	/* Inner USER_NOTIF filter second (innermost); returns the listener. */
+	listener = user_notif_syscall(__NR_socket,
+				      SECCOMP_FILTER_FLAG_NEW_LISTENER |
+				      SECCOMP_FILTER_FLAG_REDIRECT);
+	ASSERT_GE(listener, 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+
+	if (pid == 0) {
+		int fd = syscall(__NR_socket, AF_UNIX, SOCK_STREAM, 0);
+
+		if (fd >= 0)
+			_exit(12);
+		if (errno != EACCES)
+			_exit(13);
+		_exit(0);
+	}
+
+	ASSERT_EQ(0, ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req));
+	EXPECT_EQ(req.data.nr, __NR_socket);
+	EXPECT_EQ(req.data.args[0], AF_UNIX);
+
+	/* Scalar redirect of arg0 (no pin needed): AF_UNIX -> AF_INET. */
+	redir.id = req.id;
+	redir.flags = SECCOMP_REDIRECT_FLAG_CONTINUE;
+	redir.args_mask = 1U << 0;
+	redir.args[0] = AF_INET;
+	ret = ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND_REDIRECT, &redir);
+	if (ret < 0 && errno == EINVAL) {
+		kill(pid, SIGKILL);
+		waitpid(pid, &status, 0);
+		close(listener);
+		SKIP(return, "Kernel lacks SECCOMP_IOCTL_NOTIF_SEND_REDIRECT");
+	}
+	EXPECT_EQ(0, ret);
+	if (ret)
+		kill(pid, SIGKILL);
+
+	EXPECT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(true, WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status)) {
+		switch (WEXITSTATUS(status)) {
+		case 12:
+			TH_LOG("child exit 12: redirect bypassed the outer filter");
+			break;
+		case 13:
+			TH_LOG("child exit 13: socket failed with unexpected errno");
+			break;
+		default:
+			TH_LOG("child exit %d (unexpected)", WEXITSTATUS(status));
+		}
+	}
+
+	close(listener);
+}
+#endif /* __NR_socket */
+
+/*
+ * SEND_REDIRECT refuses syscalls whose register substitution would be unsafe
+ * and returns -EOPNOTSUPP: sigreturn (its frame restore fights the redirect
+ * restore) and the clone/fork family (a new child inherits the substituted
+ * registers with no restore). The trapped call is then answered with a plain
+ * error, so it never actually runs.
+ *
+ * The target installs the filter and hands the listener to the supervisor over
+ * a socketpair: a filter that traps fork/clone cannot be installed before the
+ * supervisor itself forks the target.
+ */
+TEST(user_notification_redirect_denied_syscalls)
+{
+	static const int denied[] = {
+		__NR_rt_sigreturn,
+		__NR_clone,
+		__NR_clone3,
+#ifdef __NR_fork
+		__NR_fork,
+#endif
+#ifdef __NR_vfork
+		__NR_vfork,
+#endif
+	};
+	unsigned int i;
+	long ret;
+
+	for (i = 0; i < ARRAY_SIZE(denied); i++) {
+		struct seccomp_notif req = {};
+		struct seccomp_notif_resp resp = {};
+		struct seccomp_notif_resp_redirect redir = {};
+		int sk[2], listener, status;
+		pid_t pid;
+
+		if (denied[i] < 0)
+			continue;
+
+		ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, sk));
+
+		pid = fork();
+		ASSERT_GE(pid, 0);
+		if (pid == 0) {
+			close(sk[0]);
+			if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
+				_exit(1);
+			listener = user_notif_syscall(
+					denied[i],
+					SECCOMP_FILTER_FLAG_NEW_LISTENER |
+					SECCOMP_FILTER_FLAG_REDIRECT);
+			if (listener < 0)
+				_exit(2);
+			if (send_fd(sk[1], listener))
+				_exit(3);
+			syscall(denied[i], 0, 0, 0, 0, 0, 0);
+			_exit(0);
+		}
+
+		close(sk[1]);
+		listener = recv_fd(sk[0]);
+		if (listener < 0) {
+			waitpid(pid, &status, 0);
+			close(sk[0]);
+			SKIP(return, "SECCOMP_FILTER_FLAG_REDIRECT unsupported");
+		}
+
+		ASSERT_EQ(0, ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req));
+		EXPECT_EQ(req.data.nr, denied[i]);
+
+		redir.id = req.id;
+		redir.flags = SECCOMP_REDIRECT_FLAG_CONTINUE;
+		redir.args_mask = 1U << 0;
+		redir.args[0] = 0;
+		errno = 0;
+		ret = ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND_REDIRECT, &redir);
+		EXPECT_EQ(-1, ret);
+		EXPECT_EQ(EOPNOTSUPP, errno) {
+			TH_LOG("nr %d: SEND_REDIRECT errno %d, want EOPNOTSUPP",
+			       denied[i], errno);
+		}
+
+		resp.id = req.id;
+		resp.error = -EPERM;
+		EXPECT_EQ(0, ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp));
+
+		EXPECT_EQ(waitpid(pid, &status, 0), pid);
+		EXPECT_EQ(true, WIFEXITED(status));
+		EXPECT_EQ(0, WEXITSTATUS(status)) {
+			TH_LOG("nr %d: child exit %d", denied[i],
+			       WEXITSTATUS(status));
+		}
+		close(listener);
+		close(sk[0]);
+	}
+}
+
+#ifdef __NR_socket
+/*
+ * Re-validation walks *every* filter outer to the notifier, not just the
+ * nearest. Stack (outer -> inner):
+ *
+ *   outer  (1st): socket(AF_INET) -> RET_ERRNO(EACCES), else ALLOW
+ *   middle (2nd): ALLOW everything
+ *   inner  (3rd): socket -> RET_USER_NOTIF (the redirector)
+ *
+ * The target calls socket(AF_UNIX); the inner notifier fires and the
+ * supervisor SEND_REDIRECTs arg0 to AF_INET. The outward walk must pass
+ * through the permissive middle filter and still reach the outer filter,
+ * which blocks the substituted call with EACCES. If the walk stopped at the
+ * first outer filter (middle, ALLOW), the redirect would slip past the outer
+ * policy. This exercises the iterative multi-filter walk that the single-outer
+ * outer_refilter test does not.
+ */
+TEST(user_notification_redirect_revalidate_chain)
+{
+	struct sock_filter outer_filter[] = {
+		BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
+			 offsetof(struct seccomp_data, nr)),
+		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_socket, 0, 3),
+		BPF_STMT(BPF_LD | BPF_W | BPF_ABS, syscall_arg(0)),
+		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET, 0, 1),
+		BPF_STMT(BPF_RET | BPF_K,
+			 SECCOMP_RET_ERRNO | (EACCES & SECCOMP_RET_DATA)),
+		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
+	};
+	struct sock_fprog outer_prog = {
+		.len = (unsigned short)ARRAY_SIZE(outer_filter),
+		.filter = outer_filter,
+	};
+	struct sock_filter allow_filter[] = {
+		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
+	};
+	struct sock_fprog allow_prog = {
+		.len = (unsigned short)ARRAY_SIZE(allow_filter),
+		.filter = allow_filter,
+	};
+	struct seccomp_notif req = {};
+	struct seccomp_notif_resp_redirect redir = {};
+	int status, listener;
+	pid_t pid;
+	long ret;
+
+	ret = prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+	ASSERT_EQ(0, ret) {
+		TH_LOG("Kernel does not support PR_SET_NO_NEW_PRIVS!");
+	}
+
+	/* outer (root) -> middle (permissive) -> inner (notifier). */
+	ASSERT_EQ(0, seccomp(SECCOMP_SET_MODE_FILTER, 0, &outer_prog));
+	ASSERT_EQ(0, seccomp(SECCOMP_SET_MODE_FILTER, 0, &allow_prog));
+	listener = user_notif_syscall(__NR_socket,
+				      SECCOMP_FILTER_FLAG_NEW_LISTENER |
+				      SECCOMP_FILTER_FLAG_REDIRECT);
+	ASSERT_GE(listener, 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+
+	if (pid == 0) {
+		int fd = syscall(__NR_socket, AF_UNIX, SOCK_STREAM, 0);
+
+		if (fd >= 0)
+			_exit(12);
+		if (errno != EACCES)
+			_exit(13);
+		_exit(0);
+	}
+
+	ASSERT_EQ(0, ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req));
+	EXPECT_EQ(req.data.nr, __NR_socket);
+	EXPECT_EQ(req.data.args[0], AF_UNIX);
+
+	redir.id = req.id;
+	redir.flags = SECCOMP_REDIRECT_FLAG_CONTINUE;
+	redir.args_mask = 1U << 0;
+	redir.args[0] = AF_INET;
+	ret = ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND_REDIRECT, &redir);
+	if (ret < 0 && errno == EINVAL) {
+		kill(pid, SIGKILL);
+		waitpid(pid, &status, 0);
+		close(listener);
+		SKIP(return, "Kernel lacks SECCOMP_IOCTL_NOTIF_SEND_REDIRECT");
+	}
+	EXPECT_EQ(0, ret);
+	if (ret)
+		kill(pid, SIGKILL);
+
+	EXPECT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(true, WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status)) {
+		switch (WEXITSTATUS(status)) {
+		case 12:
+			TH_LOG("exit 12: walk stopped at middle; outer bypassed");
+			break;
+		case 13:
+			TH_LOG("exit 13: socket failed with unexpected errno");
+			break;
+		default:
+			TH_LOG("child exit %d (unexpected)", WEXITSTATUS(status));
+		}
+	}
+
+	close(listener);
+}
+
+/*
+ * An outer TRACE verdict over a redirected syscall fails closed. TRACE hands
+ * the syscall to a tracer that may rewrite it, and a rewrite cannot be
+ * soundly re-composed mid-walk, so the kernel must not consult the tracer at
+ * all: the substituted call is skipped with -ENOSYS, exactly like TRACE with
+ * no tracer attached, and the tracer must not receive a PTRACE_EVENT_SECCOMP
+ * for it. Stack (outer -> inner):
+ *
+ *   outer (1st): socket(AF_INET) -> RET_TRACE, else ALLOW
+ *   inner (2nd): socket -> RET_USER_NOTIF (the redirector)
+ *
+ * The traced child calls socket(AF_UNIX); the supervisor (also the tracer)
+ * SEND_REDIRECTs arg0 to AF_INET, which the outer filter claims. The child
+ * must observe ENOSYS and the tracer a plain exit, not a seccomp event stop.
+ */
+TEST(user_notification_redirect_outer_trace)
+{
+	struct sock_filter outer_filter[] = {
+		BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
+			 offsetof(struct seccomp_data, nr)),
+		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_socket, 0, 3),
+		BPF_STMT(BPF_LD | BPF_W | BPF_ABS, syscall_arg(0)),
+		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET, 0, 1),
+		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_TRACE | 0x23),
+		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
+	};
+	struct sock_fprog outer_prog = {
+		.len = (unsigned short)ARRAY_SIZE(outer_filter),
+		.filter = outer_filter,
+	};
+	struct seccomp_notif req = {};
+	struct seccomp_notif_resp_redirect redir = {};
+	int status, listener;
+	pid_t pid;
+	long ret;
+
+	ret = prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+	ASSERT_EQ(0, ret) {
+		TH_LOG("Kernel does not support PR_SET_NO_NEW_PRIVS!");
+	}
+
+	/* Outer filter first => it becomes the outer/root of the stack. */
+	ASSERT_EQ(0, seccomp(SECCOMP_SET_MODE_FILTER, 0, &outer_prog));
+
+	/* Inner USER_NOTIF filter second (innermost); returns the listener. */
+	listener = user_notif_syscall(__NR_socket,
+				      SECCOMP_FILTER_FLAG_NEW_LISTENER |
+				      SECCOMP_FILTER_FLAG_REDIRECT);
+	ASSERT_GE(listener, 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+
+	if (pid == 0) {
+		int fd;
+
+		if (ptrace(PTRACE_TRACEME, 0, NULL, NULL))
+			_exit(14);
+		if (raise(SIGSTOP))
+			_exit(15);
+
+		fd = syscall(__NR_socket, AF_UNIX, SOCK_STREAM, 0);
+		if (fd >= 0)
+			_exit(12);
+		if (errno != ENOSYS)
+			_exit(13);
+		_exit(0);
+	}
+
+	/* Tracer handshake: enable PTRACE_EVENT_SECCOMP reporting. */
+	ASSERT_EQ(pid, waitpid(pid, &status, 0));
+	ASSERT_EQ(true, WIFSTOPPED(status));
+	ASSERT_EQ(SIGSTOP, WSTOPSIG(status));
+	ASSERT_EQ(0, ptrace(PTRACE_SETOPTIONS, pid, NULL,
+			    PTRACE_O_TRACESECCOMP));
+	ASSERT_EQ(0, ptrace(PTRACE_CONT, pid, NULL, 0));
+
+	ASSERT_EQ(0, ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req));
+	EXPECT_EQ(req.data.nr, __NR_socket);
+	EXPECT_EQ(req.data.args[0], AF_UNIX);
+
+	/* Scalar redirect of arg0 (no pin needed): AF_UNIX -> AF_INET. */
+	redir.id = req.id;
+	redir.flags = SECCOMP_REDIRECT_FLAG_CONTINUE;
+	redir.args_mask = 1U << 0;
+	redir.args[0] = AF_INET;
+	ret = ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND_REDIRECT, &redir);
+	if (ret < 0 && (errno == EINVAL || errno == EOPNOTSUPP)) {
+		kill(pid, SIGKILL);
+		waitpid(pid, &status, 0);
+		close(listener);
+		SKIP(return, "Kernel lacks SECCOMP_IOCTL_NOTIF_SEND_REDIRECT");
+	}
+	EXPECT_EQ(0, ret);
+
+	/*
+	 * A plain exit, not a ptrace stop: the tracer must never see a
+	 * PTRACE_EVENT_SECCOMP for the substituted call.
+	 */
+	EXPECT_EQ(pid, waitpid(pid, &status, 0));
+	EXPECT_EQ(true, WIFEXITED(status)) {
+		if (WIFSTOPPED(status) &&
+		    status >> 8 == (SIGTRAP | (PTRACE_EVENT_SECCOMP << 8)))
+			TH_LOG("tracer got PTRACE_EVENT_SECCOMP for the redirected call");
+		kill(pid, SIGKILL);
+		waitpid(pid, &status, 0);
+	}
+	EXPECT_EQ(0, WEXITSTATUS(status)) {
+		switch (WEXITSTATUS(status)) {
+		case 12:
+			TH_LOG("child exit 12: redirected socket ran past the outer TRACE filter");
+			break;
+		case 13:
+			TH_LOG("child exit 13: socket failed with unexpected errno (want ENOSYS)");
+			break;
+		default:
+			TH_LOG("child exit %d (unexpected)", WEXITSTATUS(status));
+		}
+	}
+
+	close(listener);
+}
+
+/*
+ * An outer USER_NOTIF verdict over a redirected syscall: the walk hands the
+ * substituted call to the outer filter's own notifier. A chain can hold only
+ * one listener (installing a second one fails with EBUSY), so an outer
+ * USER_NOTIF filter can never have a live supervisor today; its verdict
+ * resolves like any listener-less USER_NOTIF, skipping the syscall with
+ * -ENOSYS. Stack (outer -> inner):
+ *
+ *   outer (1st): socket(AF_INET) -> RET_USER_NOTIF (no listener), else ALLOW
+ *   inner (2nd): socket -> RET_USER_NOTIF (the redirector, with the listener)
+ *
+ * The child calls socket(AF_UNIX); the supervisor SEND_REDIRECTs arg0 to
+ * AF_INET, which the outer filter claims. The child must observe ENOSYS: the
+ * redirect must not slip past the outer verdict just because no listener
+ * stands behind it.
+ */
+TEST(user_notification_redirect_outer_notify)
+{
+	struct sock_filter outer_filter[] = {
+		BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
+			 offsetof(struct seccomp_data, nr)),
+		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_socket, 0, 3),
+		BPF_STMT(BPF_LD | BPF_W | BPF_ABS, syscall_arg(0)),
+		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET, 0, 1),
+		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_USER_NOTIF),
+		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
+	};
+	struct sock_fprog outer_prog = {
+		.len = (unsigned short)ARRAY_SIZE(outer_filter),
+		.filter = outer_filter,
+	};
+	struct seccomp_notif req = {};
+	struct seccomp_notif_resp_redirect redir = {};
+	int status, listener;
+	pid_t pid;
+	long ret;
+
+	ret = prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+	ASSERT_EQ(0, ret) {
+		TH_LOG("Kernel does not support PR_SET_NO_NEW_PRIVS!");
+	}
+
+	/* Outer filter first => it becomes the outer/root of the stack. */
+	ASSERT_EQ(0, seccomp(SECCOMP_SET_MODE_FILTER, 0, &outer_prog));
+
+	/* Inner USER_NOTIF filter second (innermost); returns the listener. */
+	listener = user_notif_syscall(__NR_socket,
+				      SECCOMP_FILTER_FLAG_NEW_LISTENER |
+				      SECCOMP_FILTER_FLAG_REDIRECT);
+	ASSERT_GE(listener, 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+
+	if (pid == 0) {
+		int fd = syscall(__NR_socket, AF_UNIX, SOCK_STREAM, 0);
+
+		if (fd >= 0)
+			_exit(12);
+		if (errno != ENOSYS)
+			_exit(13);
+		_exit(0);
+	}
+
+	ASSERT_EQ(0, ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req));
+	EXPECT_EQ(req.data.nr, __NR_socket);
+	EXPECT_EQ(req.data.args[0], AF_UNIX);
+
+	/* Scalar redirect of arg0 (no pin needed): AF_UNIX -> AF_INET. */
+	redir.id = req.id;
+	redir.flags = SECCOMP_REDIRECT_FLAG_CONTINUE;
+	redir.args_mask = 1U << 0;
+	redir.args[0] = AF_INET;
+	ret = ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND_REDIRECT, &redir);
+	if (ret < 0 && (errno == EINVAL || errno == EOPNOTSUPP)) {
+		kill(pid, SIGKILL);
+		waitpid(pid, &status, 0);
+		close(listener);
+		SKIP(return, "Kernel lacks SECCOMP_IOCTL_NOTIF_SEND_REDIRECT");
+	}
+	EXPECT_EQ(0, ret);
+
+	EXPECT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(true, WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status)) {
+		switch (WEXITSTATUS(status)) {
+		case 12:
+			TH_LOG("child exit 12: redirect ran past the outer USER_NOTIF filter");
+			break;
+		case 13:
+			TH_LOG("child exit 13: socket failed with unexpected errno (want ENOSYS)");
+			break;
+		default:
+			TH_LOG("child exit %d (unexpected)", WEXITSTATUS(status));
+		}
+	}
+
+	close(listener);
+}
+#endif /* __NR_socket */
+
+#ifdef __x86_64__
+/*
+ * Load-bearing ABI check: after SEND_REDIRECT, the trapped task's
+ * redirected arg register must be restored to its original value
+ * before user-mode code resumes. The kernel's restore mechanism
+ * (task_work_add(TWA_RESUME) -> seccomp_redirect_restore_cb) is
+ * what guarantees this; without a test the property is just an
+ * assertion. Bypass libc's syscall() wrapper (which caller-saves
+ * arg values and would mask a restore bug) and capture the actual
+ * arg register immediately after the SYSCALL instruction.
+ *
+ * The child issues openat with RSI = sentinel_path. The supervisor
+ * SEND_REDIRECTs args[1] (RSI) to point into the pin. The kernel:
+ *   - saves the original RSI into the knotif
+ *   - writes the pin address into RSI via syscall_set_arguments()
+ *   - runs the syscall (kernel reads path from the pin)
+ *   - on syscall_exit_to_user_mode, fires task_work which calls
+ *     syscall_set_arguments() again with the saved original
+ *   - returns to user mode
+ *
+ * If task_work fires correctly, the child observes RSI == sentinel.
+ * If broken, RSI holds the pin address (the redirected value the
+ * kernel left in pt_regs).
+ */
+TEST(user_notification_pinned_memfd_abi)
+{
+	pid_t pid;
+	long ret;
+	int status, listener, memfd;
+	struct seccomp_notif req = {};
+	struct seccomp_notif_pin_install pin = {};
+	struct seccomp_notif_resp_redirect redir = {};
+	char *sup_view;
+	const size_t PIN_SIZE = 4096;
+	const char *safe_path = "/dev/null";
+	/*
+	 * The "sentinel" is a real string the child can also pass as
+	 * the openat path. Its address is captured pre-syscall as RSI;
+	 * post-syscall RSI must equal the same address.
+	 */
+	static const char sentinel_path[] = "/seccomp_abi_sentinel";
+
+	ret = prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+	ASSERT_EQ(0, ret) {
+		TH_LOG("Kernel does not support PR_SET_NO_NEW_PRIVS!");
+	}
+
+	memfd = make_pin_memfd(_metadata, "pin-abi", PIN_SIZE,
+			       &sup_view, safe_path);
+
+	listener = user_notif_syscall(__NR_openat,
+				      SECCOMP_FILTER_FLAG_NEW_LISTENER |
+				      SECCOMP_FILTER_FLAG_REDIRECT);
+	ASSERT_GE(listener, 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+
+	if (pid == 0) {
+		register long r10_val asm("r10") = 0;
+		unsigned long rsi_after;
+		long fd;
+
+		asm volatile(
+			"syscall\n\t"
+			"mov %%rsi, %[after]"
+			: "=a"(fd), [after] "=&r"(rsi_after)
+			: "0"((long)__NR_openat),
+			  "D"((long)AT_FDCWD),
+			  "S"((unsigned long)sentinel_path),
+			  "d"((long)O_RDONLY),
+			  "r"(r10_val)
+			: "rcx", "r11", "memory"
+		);
+
+		if (fd < 0)
+			_exit(11);
+		/*
+		 * Load-bearing check: RSI immediately post-SYSCALL must
+		 * still be the sentinel pointer the child passed in. The
+		 * kernel's REDIRECT-then-restore mechanism is the only
+		 * thing that guarantees this; a broken restore would leave
+		 * the pin address in RSI.
+		 */
+		if (rsi_after != (unsigned long)sentinel_path)
+			_exit(12);
+		_exit(0);
+	}
+
+	ASSERT_EQ(0, ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req));
+	EXPECT_EQ(req.data.nr, __NR_openat);
+	EXPECT_EQ(req.data.args[1], (unsigned long)sentinel_path);
+
+	pin.id = req.id;
+	pin.memfd = memfd;
+	pin.target_addr = 0;
+	pin.size = PIN_SIZE;
+	EXPECT_EQ(0, ioctl(listener, SECCOMP_IOCTL_NOTIF_PIN_INSTALL, &pin)) {
+		if (errno == EINVAL) {
+			kill(pid, SIGKILL);
+			waitpid(pid, &status, 0);
+			SKIP(goto cleanup,
+			     "Kernel lacks pinned-memfd remote install");
+		}
+	}
+
+	redir.id = req.id;
+	redir.flags = SECCOMP_REDIRECT_FLAG_CONTINUE;
+	redir.args_mask = 1U << 1;
+	redir.ptr_mask = 1U << 1;
+	redir.memfd = memfd;
+	redir.ptr_len[1] = strlen(safe_path) + 1;
+	redir.args[1] = pin.target_addr;
+	EXPECT_EQ(0, ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND_REDIRECT,
+			   &redir)) {
+		kill(pid, SIGKILL);
+	}
+
+	EXPECT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(true, WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status)) {
+		switch (WEXITSTATUS(status)) {
+		case 11:
+			TH_LOG("child exit 11: openat returned -errno");
+			break;
+		case 12:
+			TH_LOG("child exit 12: ABI violation -- RSI not restored after redirect");
+			break;
+		default:
+			TH_LOG("child exit %d (unexpected)", WEXITSTATUS(status));
+		}
+	}
+
+cleanup:
+	munmap(sup_view, PIN_SIZE);
+	close(memfd);
+	close(listener);
+}
+
+static void redir_sigusr1_handler(int signo)
+{
+	/* _exit() is async-signal-safe; bail with a distinct code if the
+	 * signal frame was clobbered so the handler sees the wrong signo.
+	 */
+	if (signo != SIGUSR1)
+		_exit(12);
+}
+
+/*
+ * Regression test: a redirect's deferred arg-register restore must run
+ * before a signal frame is built, not after.
+ *
+ * The restore is queued as a TWA_RESUME task_work. get_signal() runs
+ * task_work_run() before it dequeues a signal, so the restore executes
+ * before handle_signal() builds the handler frame (regs->di = signo,
+ * regs->si = &siginfo, regs->dx = &ucontext): the trapped syscall's
+ * original argument values are back in pt_regs first and never clobber
+ * the frame. A broken restore that instead ran after the frame was
+ * built would overwrite those registers and enter the handler with a
+ * corrupted signal number.
+ *
+ * The child traps on pause(), the supervisor redirects arg0 (RDI), and
+ * then interrupts it with SIGUSR1. The handler must observe
+ * signo == SIGUSR1, not the leaked original RDI sentinel.
+ */
+TEST(user_notification_redirect_signal_abi)
+{
+	pid_t pid;
+	long ret;
+	int status, listener;
+	struct seccomp_notif req = {};
+	struct seccomp_notif_resp_redirect redir = {};
+	/* A recognizable original RDI the broken restore would leak in. */
+	const unsigned long RDI_SENTINEL = 0x5a5a5a5aUL;
+
+	ret = prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+	ASSERT_EQ(0, ret) {
+		TH_LOG("Kernel does not support PR_SET_NO_NEW_PRIVS!");
+	}
+
+	listener = user_notif_syscall(__NR_pause,
+				      SECCOMP_FILTER_FLAG_NEW_LISTENER |
+				      SECCOMP_FILTER_FLAG_REDIRECT);
+	ASSERT_GE(listener, 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+
+	if (pid == 0) {
+		struct sigaction sa = {
+			.sa_handler = redir_sigusr1_handler,
+		};
+		long rc;
+
+		if (sigaction(SIGUSR1, &sa, NULL))
+			_exit(10);
+
+		/* Raw pause() carrying a controlled RDI sentinel. */
+		asm volatile(
+			"syscall"
+			: "=a"(rc)
+			: "0"((long)__NR_pause),
+			  "D"(RDI_SENTINEL)
+			: "rcx", "r11", "memory");
+
+		if (rc != -EINTR)
+			_exit(11);
+		_exit(0);
+	}
+
+	ASSERT_EQ(0, ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req));
+	EXPECT_EQ(req.data.nr, __NR_pause);
+	EXPECT_EQ(req.data.args[0], RDI_SENTINEL);
+
+	/* Redirect arg0 (non-pointer); this arms the original-RDI restore. */
+	redir.id = req.id;
+	redir.flags = SECCOMP_REDIRECT_FLAG_CONTINUE;
+	redir.args_mask = 1U << 0;
+	redir.args[0] = 0;
+	EXPECT_EQ(0, ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND_REDIRECT,
+			   &redir)) {
+		int einval = (errno == EINVAL);
+
+		kill(pid, SIGKILL);
+		waitpid(pid, &status, 0);
+		if (einval)
+			SKIP(goto cleanup,
+			     "Kernel lacks SECCOMP_IOCTL_NOTIF_SEND_REDIRECT");
+		goto cleanup;
+	}
+
+	usleep(100000);
+	EXPECT_EQ(0, kill(pid, SIGUSR1));
+
+	EXPECT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(true, WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status)) {
+		switch (WEXITSTATUS(status)) {
+		case 10:
+			TH_LOG("child exit 10: sigaction failed");
+			break;
+		case 11:
+			TH_LOG("child exit 11: pause() did not return -EINTR");
+			break;
+		case 12:
+			TH_LOG("child exit 12: handler saw wrong signo (frame clobbered)");
+			break;
+		default:
+			TH_LOG("child exit %d (unexpected)", WEXITSTATUS(status));
+		}
+	}
+
+cleanup:
+	close(listener);
+}
+#endif /* __x86_64__ */
+
 #ifndef SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP
 #define SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP (1UL << 0)
 #define SECCOMP_IOCTL_NOTIF_SET_FLAGS  SECCOMP_IOW(4, __u64)
-- 
2.43.0



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

end of thread, other threads:[~2026-07-15 21:21 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-15 21:19 [PATCH v6 0/8] seccomp: non-cooperative pinned-memfd argument redirect Cong Wang
2026-07-15 21:20 ` [PATCH v6 1/8] mm: pass the target mm parameter through get_unmapped_area family Cong Wang
2026-07-15 21:20 ` [PATCH v6 2/8] mm: add __do_mmap() and vm_mmap_remote()/vm_munmap_remote() Cong Wang
2026-07-15 21:20 ` [PATCH v6 3/8] seccomp: introduce SECCOMP_IOCTL_NOTIF_PIN_INSTALL Cong Wang
2026-07-15 21:20 ` [PATCH v6 4/8] seccomp: add __NR_seccomp_* aliases for rt_sigreturn and clone/fork Cong Wang
2026-07-15 21:20 ` [PATCH v6 5/8] seccomp: add kernel-installed pinned-memfd redirect Cong Wang
2026-07-15 21:20 ` [PATCH v6 6/8] seccomp: re-validate a redirected syscall against outer filters Cong Wang
2026-07-15 21:20 ` [PATCH v6 7/8] docs/seccomp: document pinned-memfd redirect ioctls Cong Wang
2026-07-15 21:20 ` [PATCH v6] selftests/seccomp: cover non-cooperative pinned-memfd install Cong Wang

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