* [RFC PATCH 1/6] pidfd: add pidfd_mmap()/pidfd_munmap() syscalls
2026-07-10 20:53 [RFC PATCH 0/6] um: introduce pidfd_mmap()/pidfd_munmap() syscalls Cong Wang
@ 2026-07-10 20:53 ` Cong Wang
2026-07-10 20:53 ` [RFC PATCH 2/6] um: acquire a stub pidfd via CLONE_PIDFD in seccomp mode Cong Wang
` (4 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Cong Wang @ 2026-07-10 20:53 UTC (permalink / raw)
To: Richard Weinberger, Anton Ivanov, Johannes Berg
Cc: Benjamin Berg, linux-um, linux-kernel, Cong Wang
From: Cong Wang <cwang@multikernel.io>
A supervisor that may PTRACE_ATTACH a target installs or removes a
mapping in the target's address space directly, without target-side
cooperation. The backing fd is resolved in the *caller's* fd table,
mirroring pidfd_getfd()'s cross-task install model.
pidfd_mmap() takes an extensible struct pidfd_mmap_args (clone3/openat2
style, versioned by a leading size field) since an mmap-shaped call
exceeds the 6-argument syscall limit. pidfd_munmap() is a flat 3-arg
call. Both are gated by ptrace_may_access(PTRACE_MODE_ATTACH_REALCREDS)
and wired for x86_64 and the asm-generic table.
Assisted-by: Claude:claude-opus-4.8
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
arch/x86/entry/syscalls/syscall_64.tbl | 2 +
include/linux/syscalls.h | 5 +
include/uapi/linux/pidfd.h | 18 ++++
kernel/pid.c | 132 +++++++++++++++++++++++++
scripts/syscall.tbl | 2 +
5 files changed, 159 insertions(+)
diff --git a/arch/x86/entry/syscalls/syscall_64.tbl b/arch/x86/entry/syscalls/syscall_64.tbl
index 524155d655da..12e3cbeb0d2c 100644
--- a/arch/x86/entry/syscalls/syscall_64.tbl
+++ b/arch/x86/entry/syscalls/syscall_64.tbl
@@ -396,6 +396,8 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_mmap sys_pidfd_mmap
+473 common pidfd_munmap sys_pidfd_munmap
#
# Due to a historical design error, certain syscalls are numbered differently
diff --git a/include/linux/syscalls.h b/include/linux/syscalls.h
index 874d9067a43b..301a2d80823b 100644
--- a/include/linux/syscalls.h
+++ b/include/linux/syscalls.h
@@ -78,6 +78,7 @@ struct statmount;
struct mnt_id_req;
struct ns_id_req;
struct xattr_args;
+struct pidfd_mmap_args;
struct file_attr;
#include <linux/types.h>
@@ -986,6 +987,10 @@ asmlinkage long sys_pidfd_send_signal(int pidfd, int sig,
siginfo_t __user *info,
unsigned int flags);
asmlinkage long sys_pidfd_getfd(int pidfd, int fd, unsigned int flags);
+asmlinkage long sys_pidfd_mmap(int pidfd, struct pidfd_mmap_args __user *uargs,
+ unsigned int flags);
+asmlinkage long sys_pidfd_munmap(int pidfd, unsigned long addr,
+ unsigned long len);
asmlinkage long sys_landlock_create_ruleset(const struct landlock_ruleset_attr __user *attr,
size_t size, __u32 flags);
asmlinkage long sys_landlock_add_rule(int ruleset_fd, enum landlock_rule_type rule_type,
diff --git a/include/uapi/linux/pidfd.h b/include/uapi/linux/pidfd.h
index 0919246a1611..d68a11d844e3 100644
--- a/include/uapi/linux/pidfd.h
+++ b/include/uapi/linux/pidfd.h
@@ -107,6 +107,24 @@ struct pidfd_info {
__u64 supported_mask; /* Mask flags that this kernel supports */
};
+/*
+ * Argument block for pidfd_mmap(). Extensible: @size is sizeof(struct) as
+ * known to userspace; the kernel zero-extends older/smaller structs and
+ * rejects unknown non-zero trailing bytes (see copy_struct_from_user()).
+ */
+struct pidfd_mmap_args {
+ __u64 size; /* sizeof(struct pidfd_mmap_args) */
+ __u64 addr; /* MAP_FIXED target address, or 0 to let the kernel choose */
+ __u64 len; /* length in bytes */
+ __u64 prot; /* PROT_* protection bits */
+ __u64 flags; /* MAP_* flags */
+ __u64 pgoff; /* page offset into @fd */
+ __s32 fd; /* backing fd in the caller's fd table, or -1 (MAP_ANONYMOUS) */
+ __u32 __spare; /* must be zero */
+};
+
+#define PIDFD_MMAP_ARGS_SIZE_VER0 56 /* sizeof first published struct */
+
#define PIDFS_IOCTL_MAGIC 0xFF
#define PIDFD_GET_CGROUP_NAMESPACE _IO(PIDFS_IOCTL_MAGIC, 1)
diff --git a/kernel/pid.c b/kernel/pid.c
index f55189a3d07d..d51471a36f3e 100644
--- a/kernel/pid.c
+++ b/kernel/pid.c
@@ -28,6 +28,12 @@
*/
#include <linux/mm.h>
+#include <linux/mman.h>
+#include <linux/audit.h>
+#include <linux/file.h>
+#include <linux/ptrace.h>
+#include <linux/sched/mm.h>
+#include <linux/uaccess.h>
#include <linux/export.h>
#include <linux/slab.h>
#include <linux/init.h>
@@ -972,3 +978,129 @@ SYSCALL_DEFINE3(pidfd_getfd, int, pidfd, int, fd,
return pidfd_getfd(pid, fd);
}
+
+/*
+ * Resolve @pidfd to its task's mm, gated like a ptrace attach. On success the
+ * caller owns the returned mm reference and must mmput() it. Mirrors the
+ * cross-task authorization of pidfd_getfd(): a supervisor that may
+ * PTRACE_ATTACH the target may install into its address space.
+ */
+static int pidfd_target_mm(int pidfd, struct mm_struct **mmp)
+{
+ struct task_struct *task;
+ struct mm_struct *mm;
+ unsigned int f_flags;
+
+ task = pidfd_get_task(pidfd, &f_flags);
+ if (IS_ERR(task))
+ return PTR_ERR(task);
+
+ if (!ptrace_may_access(task, PTRACE_MODE_ATTACH_REALCREDS)) {
+ put_task_struct(task);
+ return -EPERM;
+ }
+
+ mm = get_task_mm(task);
+ put_task_struct(task);
+ if (!mm)
+ return -ESRCH;
+
+ *mmp = mm;
+ return 0;
+}
+
+/**
+ * sys_pidfd_mmap() - install a mapping into another process's address space.
+ * @pidfd: pidfd of the target task.
+ * @uargs: pointer to a struct pidfd_mmap_args describing the mapping.
+ * @flags: reserved, must be zero.
+ *
+ * The backing fd (if any) is resolved in the *caller's* fd table, so a
+ * supervisor maps from its own descriptors into the target -- the target
+ * never needs to receive the fd. Gated by ptrace_may_access(); LSM/fsnotify
+ * hooks run against the caller.
+ *
+ * Returns the mapped address on success, or a negative errno.
+ */
+SYSCALL_DEFINE3(pidfd_mmap, int, pidfd, struct pidfd_mmap_args __user *, uargs,
+ unsigned int, flags)
+{
+ struct pidfd_mmap_args args;
+ struct mm_struct *mm;
+ struct file *file = NULL;
+ unsigned long ret;
+ u64 usize;
+ int err;
+
+ if (flags)
+ return -EINVAL;
+
+ if (get_user(usize, &uargs->size))
+ return -EFAULT;
+ if (usize < PIDFD_MMAP_ARGS_SIZE_VER0)
+ return -EINVAL;
+ if (usize > PAGE_SIZE)
+ return -E2BIG;
+
+ err = copy_struct_from_user(&args, sizeof(args), uargs, usize);
+ if (err)
+ return err;
+ if (args.__spare)
+ return -EINVAL;
+
+ if (args.flags & MAP_HUGETLB)
+ return -EINVAL;
+ if (args.addr != (unsigned long)args.addr ||
+ args.len != (unsigned long)args.len ||
+ args.prot != (unsigned long)args.prot ||
+ args.flags != (unsigned long)args.flags ||
+ args.pgoff != (unsigned long)args.pgoff)
+ return -EINVAL;
+
+ err = pidfd_target_mm(pidfd, &mm);
+ if (err)
+ return err;
+
+ if (!(args.flags & MAP_ANONYMOUS)) {
+ audit_mmap_fd(args.fd, args.flags);
+ file = fget(args.fd);
+ if (!file) {
+ ret = -EBADF;
+ goto out_mm;
+ }
+ }
+
+ ret = vm_mmap_remote(mm, file, args.addr, args.len, args.prot,
+ args.flags, args.pgoff, 0);
+
+ if (file)
+ fput(file);
+out_mm:
+ mmput(mm);
+ return ret;
+}
+
+/**
+ * sys_pidfd_munmap() - remove a mapping from another process's address space.
+ * @pidfd: pidfd of the target task.
+ * @addr: start address of the range to unmap.
+ * @len: length in bytes.
+ *
+ * Gated by ptrace_may_access(), like sys_pidfd_mmap().
+ *
+ * Returns 0 on success, or a negative errno.
+ */
+SYSCALL_DEFINE3(pidfd_munmap, int, pidfd, unsigned long, addr,
+ unsigned long, len)
+{
+ struct mm_struct *mm;
+ int ret;
+
+ ret = pidfd_target_mm(pidfd, &mm);
+ if (ret)
+ return ret;
+
+ ret = vm_munmap_remote(mm, addr, len);
+ mmput(mm);
+ return ret;
+}
diff --git a/scripts/syscall.tbl b/scripts/syscall.tbl
index 7a42b32b6577..f4fa7826232b 100644
--- a/scripts/syscall.tbl
+++ b/scripts/syscall.tbl
@@ -412,3 +412,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_mmap sys_pidfd_mmap
+473 common pidfd_munmap sys_pidfd_munmap
--
2.43.0
^ permalink raw reply [flat|nested] 7+ messages in thread* [RFC PATCH 2/6] um: acquire a stub pidfd via CLONE_PIDFD in seccomp mode
2026-07-10 20:53 [RFC PATCH 0/6] um: introduce pidfd_mmap()/pidfd_munmap() syscalls Cong Wang
2026-07-10 20:53 ` [RFC PATCH 1/6] pidfd: add " Cong Wang
@ 2026-07-10 20:53 ` Cong Wang
2026-07-10 20:53 ` [RFC PATCH 3/6] um: install guest mappings via pidfd_mmap() " Cong Wang
` (3 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Cong Wang @ 2026-07-10 20:53 UTC (permalink / raw)
To: Richard Weinberger, Anton Ivanov, Johannes Berg
Cc: Benjamin Berg, linux-um, linux-kernel, Cong Wang
From: Cong Wang <cwang@multikernel.io>
In SECCOMP mode the monitor will install guest mappings into the stub's
address space directly via pidfd_mmap(), so it needs a pidfd to the
stub. Acquire one atomically at clone() time via CLONE_PIDFD (returned
through the legacy-clone parent_tid argument), store it in
mm_id->stub_pidfd, and close it on teardown. The ptrace (SKAS0) mode
does not use pidfd_mmap and leaves stub_pidfd as -1.
Assisted-by: Claude:claude-opus-4.8
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
arch/um/include/shared/skas/mm_id.h | 1 +
arch/um/kernel/skas/mmu.c | 6 ++++++
arch/um/os-Linux/skas/process.c | 24 ++++++++++++++++++++++--
3 files changed, 29 insertions(+), 2 deletions(-)
diff --git a/arch/um/include/shared/skas/mm_id.h b/arch/um/include/shared/skas/mm_id.h
index 18c0621430d2..cec97189f12b 100644
--- a/arch/um/include/shared/skas/mm_id.h
+++ b/arch/um/include/shared/skas/mm_id.h
@@ -16,6 +16,7 @@ struct mm_id {
int syscall_data_len;
/* Only used with SECCOMP mode */
+ int stub_pidfd; /* pidfd to the stub, or -1 */
int sock;
int syscall_fd_num;
int syscall_fd_map[STUB_MAX_FDS];
diff --git a/arch/um/kernel/skas/mmu.c b/arch/um/kernel/skas/mmu.c
index b5017096028b..441dcf94ec9c 100644
--- a/arch/um/kernel/skas/mmu.c
+++ b/arch/um/kernel/skas/mmu.c
@@ -54,6 +54,7 @@ int init_new_context(struct task_struct *task, struct mm_struct *mm)
goto out;
new_id->stack = stack;
+ new_id->stub_pidfd = -1;
new_id->syscall_data_len = 0;
new_id->syscall_fd_num = 0;
@@ -103,6 +104,11 @@ void destroy_context(struct mm_struct *mm)
mmu->id.pid = -1;
}
+ if (mmu->id.stub_pidfd >= 0) {
+ os_close_file(mmu->id.stub_pidfd);
+ mmu->id.stub_pidfd = -1;
+ }
+
if (using_seccomp && mmu->id.sock)
os_close_file(mmu->id.sock);
diff --git a/arch/um/os-Linux/skas/process.c b/arch/um/os-Linux/skas/process.c
index d6c22f8aa06d..3dd97ca7999a 100644
--- a/arch/um/os-Linux/skas/process.c
+++ b/arch/um/os-Linux/skas/process.c
@@ -34,6 +34,10 @@
#include <asm-generic/rwonce.h>
#include "../internal.h"
+#ifndef CLONE_PIDFD
+#define CLONE_PIDFD 0x00001000
+#endif
+
int is_skas_winch(int pid, int fd, void *data)
{
return pid == getpgrp();
@@ -448,6 +452,9 @@ int start_userspace(struct mm_id *mm_id)
void *stack;
unsigned long sp;
int status, n, err;
+ int stub_pidfd = -1;
+
+ mm_id->stub_pidfd = -1;
/* setup a temporary stack page */
stack = mmap(NULL, UM_KERN_PAGE_SIZE,
@@ -474,15 +481,25 @@ int start_userspace(struct mm_id *mm_id)
if (using_seccomp)
proc_data->futex = FUTEX_IN_CHILD;
+ /*
+ * In SECCOMP mode, acquire a pidfd to the stub via CLONE_PIDFD (it is
+ * returned through the legacy-clone parent_tid argument). The monitor
+ * installs guest mappings into the stub's mm directly via pidfd_mmap(),
+ * so the stub itself never needs the mmap capability. The ptrace mode
+ * does not use it and drives the stub directly.
+ */
mm_id->pid = clone(userspace_tramp, (void *) sp,
- CLONE_VFORK | CLONE_VM | SIGCHLD,
- (void *)&tramp_data);
+ CLONE_VFORK | CLONE_VM | (using_seccomp ? CLONE_PIDFD : 0) |
+ SIGCHLD,
+ (void *)&tramp_data, &stub_pidfd);
if (mm_id->pid < 0) {
err = -errno;
printk(UM_KERN_ERR "%s : clone failed, errno = %d\n",
__func__, errno);
goto out_close;
}
+ if (using_seccomp)
+ mm_id->stub_pidfd = stub_pidfd;
if (using_seccomp) {
wait_stub_done_seccomp(mm_id, 1, 1);
@@ -534,8 +551,11 @@ int start_userspace(struct mm_id *mm_id)
out_close:
close(tramp_data.sockpair[0]);
close(tramp_data.sockpair[1]);
+ if (stub_pidfd >= 0)
+ close(stub_pidfd);
mm_id->pid = -1;
+ mm_id->stub_pidfd = -1;
return err;
}
--
2.43.0
^ permalink raw reply [flat|nested] 7+ messages in thread* [RFC PATCH 3/6] um: install guest mappings via pidfd_mmap() in seccomp mode
2026-07-10 20:53 [RFC PATCH 0/6] um: introduce pidfd_mmap()/pidfd_munmap() syscalls Cong Wang
2026-07-10 20:53 ` [RFC PATCH 1/6] pidfd: add " Cong Wang
2026-07-10 20:53 ` [RFC PATCH 2/6] um: acquire a stub pidfd via CLONE_PIDFD in seccomp mode Cong Wang
@ 2026-07-10 20:53 ` Cong Wang
2026-07-10 20:53 ` [RFC PATCH 4/6] um: forbid mmap/munmap in the stub seccomp filter Cong Wang
` (2 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Cong Wang @ 2026-07-10 20:53 UTC (permalink / raw)
To: Richard Weinberger, Anton Ivanov, Johannes Berg
Cc: Benjamin Berg, linux-um, linux-kernel, Cong Wang
From: Cong Wang <cwang@multikernel.io>
In SECCOMP mode, map()/unmap() now install into the stub's address
space directly from the monitor via pidfd_mmap()/pidfd_munmap(),
resolving the physmem fd in the monitor's own fd table. The stub neither
receives the fd nor executes mmap/munmap itself.
The ptrace (SKAS0) mode is unchanged: it keeps batching STUB_SYSCALL_MMAP
/MUNMAP for the stub to execute, which works on any host kernel and
needs no new syscall. Only the seccomp path, where the stub is
sandboxed, is converted.
Assisted-by: Claude:claude-opus-4.8
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
arch/um/os-Linux/skas/mem.c | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/arch/um/os-Linux/skas/mem.c b/arch/um/os-Linux/skas/mem.c
index 8b9921ac3ef8..f751a1ea1c4c 100644
--- a/arch/um/os-Linux/skas/mem.c
+++ b/arch/um/os-Linux/skas/mem.c
@@ -9,6 +9,8 @@
#include <errno.h>
#include <string.h>
#include <sys/mman.h>
+#include <sys/syscall.h>
+#include <linux/pidfd.h>
#include <init.h>
#include <as-layout.h>
#include <mm_id.h>
@@ -236,6 +238,21 @@ int map(struct mm_id *mm_idp, unsigned long virt, unsigned long len, int prot,
{
struct stub_syscall *sc;
+ if (using_seccomp) {
+ struct pidfd_mmap_args args = {
+ .size = sizeof(args),
+ .addr = virt,
+ .len = len,
+ .prot = prot,
+ .flags = MAP_SHARED | MAP_FIXED,
+ .pgoff = offset >> UM_KERN_PAGE_SHIFT,
+ .fd = phys_fd,
+ };
+ long res = syscall(__NR_pidfd_mmap, mm_idp->stub_pidfd, &args, 0);
+
+ return res < 0 ? -errno : 0;
+ }
+
/* Compress with previous syscall if that is possible */
sc = syscall_stub_get_previous(mm_idp, STUB_SYSCALL_MMAP, virt);
if (sc && sc->mem.prot == prot &&
@@ -268,6 +285,13 @@ int unmap(struct mm_id *mm_idp, unsigned long addr, unsigned long len)
{
struct stub_syscall *sc;
+ if (using_seccomp) {
+ long res = syscall(__NR_pidfd_munmap, mm_idp->stub_pidfd,
+ addr, len);
+
+ return res < 0 ? -errno : 0;
+ }
+
/* Compress with previous syscall if that is possible */
sc = syscall_stub_get_previous(mm_idp, STUB_SYSCALL_MUNMAP, addr);
if (sc) {
--
2.43.0
^ permalink raw reply [flat|nested] 7+ messages in thread* [RFC PATCH 4/6] um: forbid mmap/munmap in the stub seccomp filter
2026-07-10 20:53 [RFC PATCH 0/6] um: introduce pidfd_mmap()/pidfd_munmap() syscalls Cong Wang
` (2 preceding siblings ...)
2026-07-10 20:53 ` [RFC PATCH 3/6] um: install guest mappings via pidfd_mmap() " Cong Wang
@ 2026-07-10 20:53 ` Cong Wang
2026-07-10 20:53 ` [RFC PATCH 5/6] um: install guest mappings via pidfd_mmap() in both modes Cong Wang
2026-07-10 20:53 ` [RFC PATCH 6/6] selftests/pidfd: add pidfd_mmap()/pidfd_munmap() tests Cong Wang
5 siblings, 0 replies; 7+ messages in thread
From: Cong Wang @ 2026-07-10 20:53 UTC (permalink / raw)
To: Richard Weinberger, Anton Ivanov, Johannes Berg
Cc: Benjamin Berg, linux-um, linux-kernel, Cong Wang
From: Cong Wang <cwang@multikernel.io>
In seccomp mode the monitor now installs all guest mappings via
pidfd_mmap(), so the stub has no legitimate need to mmap/munmap after
its filter is in place (the pre-filter bootstrap mmaps are unaffected).
Drop the STUB_MMAP_NR and __NR_munmap allowlist arms and fix the jump
offsets; both now hit SECCOMP_RET_KILL_PROCESS. A hijacked stub can no
longer map arbitrary physmem. The filter exists only in seccomp mode,
so ptrace (SKAS0) is untouched.
Assisted-by: Claude:claude-opus-4.8
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
arch/um/kernel/skas/stub_exe.c | 20 +++++++++++---------
1 file changed, 11 insertions(+), 9 deletions(-)
diff --git a/arch/um/kernel/skas/stub_exe.c b/arch/um/kernel/skas/stub_exe.c
index cbafaa684e66..a88274449168 100644
--- a/arch/um/kernel/skas/stub_exe.c
+++ b/arch/um/kernel/skas/stub_exe.c
@@ -165,16 +165,18 @@ noinline static void real_init(void)
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
offsetof(struct seccomp_data, nr)),
- /* [10-16] Check against permitted syscalls */
+ /*
+ * [10-14] Check against permitted syscalls. mmap and
+ * munmap are deliberately absent: in seccomp mode the
+ * monitor installs all guest mappings via pidfd_mmap(),
+ * so a stub that attempts mmap/munmap hits the KILL at
+ * [15].
+ */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_futex,
- 7, 0),
- BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,__NR_recvmsg,
- 6, 0),
- BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,__NR_close,
5, 0),
- BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, STUB_MMAP_NR,
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_recvmsg,
4, 0),
- BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_munmap,
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_close,
3, 0),
#ifdef __i386__
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_set_thread_area,
@@ -186,10 +188,10 @@ noinline static void real_init(void)
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_rt_sigreturn,
1, 0),
- /* [17] Not one of the permitted syscalls */
+ /* [15] Not one of the permitted syscalls */
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
- /* [18] Permitted call for the stub */
+ /* [16] Permitted call for the stub */
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
--
2.43.0
^ permalink raw reply [flat|nested] 7+ messages in thread* [RFC PATCH 5/6] um: install guest mappings via pidfd_mmap() in both modes
2026-07-10 20:53 [RFC PATCH 0/6] um: introduce pidfd_mmap()/pidfd_munmap() syscalls Cong Wang
` (3 preceding siblings ...)
2026-07-10 20:53 ` [RFC PATCH 4/6] um: forbid mmap/munmap in the stub seccomp filter Cong Wang
@ 2026-07-10 20:53 ` Cong Wang
2026-07-10 20:53 ` [RFC PATCH 6/6] selftests/pidfd: add pidfd_mmap()/pidfd_munmap() tests Cong Wang
5 siblings, 0 replies; 7+ messages in thread
From: Cong Wang @ 2026-07-10 20:53 UTC (permalink / raw)
To: Richard Weinberger, Anton Ivanov, Johannes Berg
Cc: Benjamin Berg, linux-um, linux-kernel, Cong Wang
From: Cong Wang <cwang@multikernel.io>
Route both seccomp and ptrace (SKAS0) map()/unmap() through
pidfd_mmap()/pidfd_munmap(): the monitor resolves the physmem fd in its
own fd table and installs the mapping into the stub's mm directly, so the
stub never executes mmap/munmap itself in either mode.
Neither mode drives the stub to execute syscalls anymore, so the whole
stub-syscall batcher and the seccomp-only SCM_RIGHTS fd-passing become
dead code and are removed. The seccomp model's inherent risks are not
addressed by pidfd_mmap() (a malicious guest can still block SIGALRM to
dodge scheduling), so that note is preserved in stub.c.
This is an RFC demonstration of pidfd_mmap()'s reach; it assumes a host
kernel that provides the syscall and does not add a fallback for older
hosts intentionally.
Assisted-by: Claude:claude-opus-4.8
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
arch/um/include/shared/os.h | 4 -
arch/um/include/shared/skas/mm_id.h | 7 -
arch/um/include/shared/skas/stub-data.h | 28 ---
arch/um/kernel/skas/mmu.c | 5 -
arch/um/kernel/skas/stub.c | 151 ++----------
arch/um/kernel/skas/stub_exe.c | 16 +-
arch/um/os-Linux/skas/mem.c | 295 ++----------------------
arch/um/os-Linux/skas/process.c | 86 +------
arch/um/os-Linux/start_up.c | 8 +-
9 files changed, 67 insertions(+), 533 deletions(-)
diff --git a/arch/um/include/shared/os.h b/arch/um/include/shared/os.h
index b26e94292fc1..c6917583ca64 100644
--- a/arch/um/include/shared/os.h
+++ b/arch/um/include/shared/os.h
@@ -280,10 +280,6 @@ extern long long os_persistent_clock_emulation(void);
extern long long os_nsecs(void);
/* skas/mem.c */
-int syscall_stub_flush(struct mm_id *mm_idp);
-struct stub_syscall *syscall_stub_alloc(struct mm_id *mm_idp);
-void syscall_stub_dump_error(struct mm_id *mm_idp);
-
int map(struct mm_id *mm_idp, unsigned long virt,
unsigned long len, int prot, int phys_fd,
unsigned long long offset);
diff --git a/arch/um/include/shared/skas/mm_id.h b/arch/um/include/shared/skas/mm_id.h
index cec97189f12b..0f74e1728a1e 100644
--- a/arch/um/include/shared/skas/mm_id.h
+++ b/arch/um/include/shared/skas/mm_id.h
@@ -8,18 +8,11 @@
#include <linux/compiler_types.h>
-#define STUB_MAX_FDS 4
-
struct mm_id {
int pid;
unsigned long stack;
- int syscall_data_len;
- /* Only used with SECCOMP mode */
int stub_pidfd; /* pidfd to the stub, or -1 */
- int sock;
- int syscall_fd_num;
- int syscall_fd_map[STUB_MAX_FDS];
};
struct mutex *__get_turnstile(struct mm_id *mm_id);
diff --git a/arch/um/include/shared/skas/stub-data.h b/arch/um/include/shared/skas/stub-data.h
index 27db38e95df9..eaa208bcdaff 100644
--- a/arch/um/include/shared/skas/stub-data.h
+++ b/arch/um/include/shared/skas/stub-data.h
@@ -31,36 +31,8 @@ struct stub_init_data {
unsigned long signal_restorer;
};
-#define STUB_NEXT_SYSCALL(s) \
- ((struct stub_syscall *) (((unsigned long) s) + (s)->cmd_len))
-
-enum stub_syscall_type {
- STUB_SYSCALL_UNSET = 0,
- STUB_SYSCALL_MMAP,
- STUB_SYSCALL_MUNMAP,
-};
-
-struct stub_syscall {
- struct {
- unsigned long addr;
- unsigned long length;
- unsigned long offset;
- int fd;
- int prot;
- } mem;
-
- enum stub_syscall_type syscall;
-};
-
struct stub_data {
- long err;
-
- int syscall_data_len;
- /* 128 leaves enough room for additional fields in the struct */
- struct stub_syscall syscall_data[(UM_KERN_PAGE_SIZE - 128) / sizeof(struct stub_syscall)] __aligned(16);
-
/* data shared with signal handler (only used in seccomp mode) */
- short restart_wait;
unsigned int futex;
int signal;
unsigned short si_offset;
diff --git a/arch/um/kernel/skas/mmu.c b/arch/um/kernel/skas/mmu.c
index 441dcf94ec9c..b2c9cf023789 100644
--- a/arch/um/kernel/skas/mmu.c
+++ b/arch/um/kernel/skas/mmu.c
@@ -55,8 +55,6 @@ int init_new_context(struct task_struct *task, struct mm_struct *mm)
new_id->stack = stack;
new_id->stub_pidfd = -1;
- new_id->syscall_data_len = 0;
- new_id->syscall_fd_num = 0;
scoped_guard(spinlock_irqsave, &mm_list_lock) {
/* Insert into list, used for lookups when the child dies */
@@ -109,9 +107,6 @@ void destroy_context(struct mm_struct *mm)
mmu->id.stub_pidfd = -1;
}
- if (using_seccomp && mmu->id.sock)
- os_close_file(mmu->id.sock);
-
free_pages(mmu->id.stack, ilog2(STUB_DATA_PAGES));
}
diff --git a/arch/um/kernel/skas/stub.c b/arch/um/kernel/skas/stub.c
index e09216a20cb5..08443f2eb09d 100644
--- a/arch/um/kernel/skas/stub.c
+++ b/arch/um/kernel/skas/stub.c
@@ -6,119 +6,46 @@
#include <sysdep/stub.h>
#include <linux/futex.h>
-#include <sys/socket.h>
#include <errno.h>
/*
- * Known security issues
+ * Known security issues (SECCOMP userspace)
*
- * Userspace can jump to this address to execute *any* syscall that is
- * permitted by the stub. As we will return afterwards, it can do
- * whatever it likes, including:
- * - Tricking the kernel into handing out the memory FD
- * - Using this memory FD to read/write all physical memory
- * - Running in parallel to the kernel processing a syscall
- * (possibly creating data races?)
- * - Blocking e.g. SIGALRM to avoid time based scheduling
+ * The stub shares its address space with the untrusted guest application.
+ * Installing guest mappings via pidfd_mmap() closed some earlier holes --
+ * the stub no longer holds the physmem fd and no longer executes mmap/munmap
+ * on the guest's behalf (both are rejected by the SECCOMP filter) -- but the
+ * following are inherent to the model and remain unaddressed:
*
- * To avoid this, the permitted location for each syscall needs to be
- * checked for in the SECCOMP filter (which is reasonably simple). Also,
- * more care will need to go into considerations how the code might be
- * tricked by using a prepared stack (or even modifying the stack from
- * another thread in case SMP support is added).
- *
- * As for the SIGALRM, the best counter measure will be to check in the
- * kernel that the process is reporting back the SIGALRM in a timely
- * fashion.
+ * - A malicious guest can influence the stub's signal state (e.g. via a
+ * prepared stack restored on rt_sigreturn) and block SIGALRM to avoid
+ * time-based scheduling. The intended counter measure is for the monitor
+ * to check that the process reports SIGALRM back in a timely fashion; that
+ * is not yet implemented.
+ * - The permitted location of each syscall the stub does issue must be
+ * pinned down in the SECCOMP filter, and more care is needed around a
+ * prepared stack (or the stack being modified from another thread should
+ * SMP support be added).
*/
-static __always_inline int syscall_handler(int fd_map[STUB_MAX_FDS])
-{
- struct stub_data *d = get_stub_data();
- int i;
- unsigned long res;
- int fd;
-
- for (i = 0; i < d->syscall_data_len; i++) {
- struct stub_syscall *sc = &d->syscall_data[i];
-
- switch (sc->syscall) {
- case STUB_SYSCALL_MMAP:
- if (fd_map)
- fd = fd_map[sc->mem.fd];
- else
- fd = sc->mem.fd;
-
- res = stub_syscall6(STUB_MMAP_NR,
- sc->mem.addr, sc->mem.length,
- sc->mem.prot,
- MAP_SHARED | MAP_FIXED,
- fd, sc->mem.offset);
- if (res != sc->mem.addr) {
- d->err = res;
- d->syscall_data_len = i;
- return -1;
- }
- break;
- case STUB_SYSCALL_MUNMAP:
- res = stub_syscall2(__NR_munmap,
- sc->mem.addr, sc->mem.length);
- if (res) {
- d->err = res;
- d->syscall_data_len = i;
- return -1;
- }
- break;
- default:
- d->err = -95; /* EOPNOTSUPP */
- d->syscall_data_len = i;
- return -1;
- }
- }
-
- d->err = 0;
- d->syscall_data_len = 0;
-
- return 0;
-}
-
-void __section(".__syscall_stub")
-stub_syscall_handler(void)
-{
- syscall_handler(NULL);
-
- trap_myself();
-}
void __section(".__syscall_stub")
stub_signal_interrupt(int sig, siginfo_t *info, void *p)
{
struct stub_data *d = get_stub_data();
- char rcv_data;
- union {
- char data[CMSG_SPACE(sizeof(int) * STUB_MAX_FDS)];
- struct cmsghdr align;
- } ctrl = {};
- struct iovec iov = {
- .iov_base = &rcv_data,
- .iov_len = 1,
- };
- struct msghdr msghdr = {
- .msg_iov = &iov,
- .msg_iovlen = 1,
- .msg_control = &ctrl,
- .msg_controllen = sizeof(ctrl),
- };
ucontext_t *uc = p;
- struct cmsghdr *fd_msg;
- int *fd_map;
- int num_fds;
long res;
d->signal = sig;
d->si_offset = (unsigned long)info - (unsigned long)&d->sigstack[0];
d->mctx_offset = (unsigned long)&uc->uc_mcontext - (unsigned long)&d->sigstack[0];
-restart_wait:
+ /*
+ * Hand the guest trap to the monitor and block until it resumes us:
+ * wake the monitor (FUTEX_WAKE) and FUTEX_WAIT until it is done. In
+ * seccomp mode the monitor installs any address-space changes itself
+ * via pidfd_mmap(), so the stub no longer executes syscalls on its
+ * behalf and never receives an fd.
+ */
d->futex = FUTEX_IN_KERN;
do {
res = stub_syscall3(__NR_futex, (unsigned long)&d->futex,
@@ -133,40 +60,6 @@ stub_signal_interrupt(int sig, siginfo_t *info, void *p)
if (res < 0 && res != -EAGAIN)
stub_syscall1(__NR_exit_group, 1);
- if (d->syscall_data_len) {
- /* Read passed FDs (if any) */
- do {
- res = stub_syscall3(__NR_recvmsg, 0, (unsigned long)&msghdr, 0);
- } while (res == -EINTR);
-
- /* We should never have a receive error (other than -EAGAIN) */
- if (res < 0 && res != -EAGAIN)
- stub_syscall1(__NR_exit_group, 1);
-
- /* Receive the FDs */
- num_fds = 0;
- fd_msg = msghdr.msg_control;
- fd_map = (void *)CMSG_DATA(fd_msg);
- if (res == iov.iov_len && msghdr.msg_controllen > sizeof(struct cmsghdr))
- num_fds = (fd_msg->cmsg_len - CMSG_LEN(0)) / sizeof(int);
-
- /* Try running queued syscalls. */
- res = syscall_handler(fd_map);
-
- while (num_fds)
- stub_syscall2(__NR_close, fd_map[--num_fds], 0);
- } else {
- res = 0;
- }
-
- if (res < 0 || d->restart_wait) {
- /* Report SIGSYS if we restart. */
- d->signal = SIGSYS;
- d->restart_wait = 0;
-
- goto restart_wait;
- }
-
/* Restore arch dependent state that is not part of the mcontext */
stub_seccomp_restore_state(&d->arch_data);
diff --git a/arch/um/kernel/skas/stub_exe.c b/arch/um/kernel/skas/stub_exe.c
index a88274449168..af1d011be0a4 100644
--- a/arch/um/kernel/skas/stub_exe.c
+++ b/arch/um/kernel/skas/stub_exe.c
@@ -166,15 +166,13 @@ noinline static void real_init(void)
offsetof(struct seccomp_data, nr)),
/*
- * [10-14] Check against permitted syscalls. mmap and
- * munmap are deliberately absent: in seccomp mode the
- * monitor installs all guest mappings via pidfd_mmap(),
- * so a stub that attempts mmap/munmap hits the KILL at
- * [15].
+ * [10-13] Check against permitted syscalls. mmap, munmap
+ * and recvmsg are deliberately absent: in seccomp mode
+ * the monitor installs all guest mappings via
+ * pidfd_mmap() and no longer passes fds to the stub, so a
+ * stub that attempts mmap/munmap hits the KILL at [14].
*/
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_futex,
- 5, 0),
- BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_recvmsg,
4, 0),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_close,
3, 0),
@@ -188,10 +186,10 @@ noinline static void real_init(void)
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_rt_sigreturn,
1, 0),
- /* [15] Not one of the permitted syscalls */
+ /* [14] Not one of the permitted syscalls */
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
- /* [16] Permitted call for the stub */
+ /* [15] Permitted call for the stub */
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
diff --git a/arch/um/os-Linux/skas/mem.c b/arch/um/os-Linux/skas/mem.c
index f751a1ea1c4c..9330f97f8beb 100644
--- a/arch/um/os-Linux/skas/mem.c
+++ b/arch/um/os-Linux/skas/mem.c
@@ -22,287 +22,34 @@
#include <sysdep/stub.h>
#include "../internal.h"
-extern char __syscall_stub_start[];
-
-void syscall_stub_dump_error(struct mm_id *mm_idp)
-{
- struct stub_data *proc_data = (void *)mm_idp->stack;
- struct stub_syscall *sc;
-
- if (proc_data->syscall_data_len < 0 ||
- proc_data->syscall_data_len >= ARRAY_SIZE(proc_data->syscall_data))
- panic("Syscall data was corrupted by stub (len is: %d, expected maximum: %d)!",
- proc_data->syscall_data_len,
- mm_idp->syscall_data_len);
-
- sc = &proc_data->syscall_data[proc_data->syscall_data_len];
-
- printk(UM_KERN_ERR "%s : length = %d, last offset = %d",
- __func__, mm_idp->syscall_data_len,
- proc_data->syscall_data_len);
- printk(UM_KERN_ERR "%s : stub syscall type %d failed, return value = 0x%lx\n",
- __func__, sc->syscall, proc_data->err);
-
- print_hex_dump(UM_KERN_ERR, " syscall data: ", 0,
- 16, 4, sc, sizeof(*sc), 0);
-
- if (using_seccomp) {
- printk(UM_KERN_ERR "%s: FD map num: %d", __func__,
- mm_idp->syscall_fd_num);
- print_hex_dump(UM_KERN_ERR,
- " FD map: ", 0, 16,
- sizeof(mm_idp->syscall_fd_map[0]),
- mm_idp->syscall_fd_map,
- sizeof(mm_idp->syscall_fd_map), 0);
- }
-}
-
-static inline unsigned long *check_init_stack(struct mm_id * mm_idp,
- unsigned long *stack)
-{
- if (stack == NULL) {
- stack = (unsigned long *) mm_idp->stack + 2;
- *stack = 0;
- }
- return stack;
-}
-
-static unsigned long syscall_regs[MAX_REG_NR];
-
-static int __init init_syscall_regs(void)
-{
- get_safe_registers(syscall_regs, NULL);
-
- syscall_regs[REGS_IP_INDEX] = STUB_CODE +
- ((unsigned long) stub_syscall_handler -
- (unsigned long) __syscall_stub_start);
- syscall_regs[REGS_SP_INDEX] = STUB_DATA +
- offsetof(struct stub_data, sigstack) +
- sizeof(((struct stub_data *) 0)->sigstack) -
- sizeof(void *);
-
- return 0;
-}
-
-__initcall(init_syscall_regs);
-
-static inline long do_syscall_stub(struct mm_id *mm_idp)
-{
- struct stub_data *proc_data = (void *)mm_idp->stack;
- int n, i;
- int err, pid = mm_idp->pid;
-
- /* Inform process how much we have filled in. */
- proc_data->syscall_data_len = mm_idp->syscall_data_len;
-
- if (using_seccomp) {
- proc_data->restart_wait = 1;
- wait_stub_done_seccomp(mm_idp, 0, 1);
- } else {
- n = ptrace_setregs(pid, syscall_regs);
- if (n < 0) {
- printk(UM_KERN_ERR "Registers -\n");
- for (i = 0; i < MAX_REG_NR; i++)
- printk(UM_KERN_ERR "\t%d\t0x%lx\n", i, syscall_regs[i]);
- panic("%s : PTRACE_SETREGS failed, errno = %d\n",
- __func__, -n);
- }
-
- err = ptrace(PTRACE_CONT, pid, 0, 0);
- if (err)
- panic("Failed to continue stub, pid = %d, errno = %d\n",
- pid, errno);
-
- wait_stub_done(pid);
- }
-
- /*
- * proc_data->err will be negative if there was an (unexpected) error.
- * In that case, syscall_data_len points to the last executed syscall,
- * otherwise it will be zero (but we do not need to rely on that).
- */
- if (proc_data->err < 0) {
- syscall_stub_dump_error(mm_idp);
-
- /* Store error code in case someone tries to add more syscalls */
- mm_idp->syscall_data_len = proc_data->err;
- } else {
- mm_idp->syscall_data_len = 0;
- }
-
- if (using_seccomp)
- mm_idp->syscall_fd_num = 0;
-
- return mm_idp->syscall_data_len;
-}
-
-int syscall_stub_flush(struct mm_id *mm_idp)
-{
- int res;
-
- if (mm_idp->syscall_data_len == 0)
- return 0;
-
- /* If an error happened already, report it and reset the state. */
- if (mm_idp->syscall_data_len < 0) {
- res = mm_idp->syscall_data_len;
- mm_idp->syscall_data_len = 0;
- return res;
- }
-
- res = do_syscall_stub(mm_idp);
- mm_idp->syscall_data_len = 0;
-
- return res;
-}
-
-struct stub_syscall *syscall_stub_alloc(struct mm_id *mm_idp)
-{
- struct stub_syscall *sc;
- struct stub_data *proc_data = (struct stub_data *) mm_idp->stack;
-
- if (mm_idp->syscall_data_len > 0 &&
- mm_idp->syscall_data_len == ARRAY_SIZE(proc_data->syscall_data))
- do_syscall_stub(mm_idp);
-
- if (mm_idp->syscall_data_len < 0) {
- /* Return dummy to retain error state. */
- sc = &proc_data->syscall_data[0];
- } else {
- sc = &proc_data->syscall_data[mm_idp->syscall_data_len];
- mm_idp->syscall_data_len += 1;
- }
- memset(sc, 0, sizeof(*sc));
-
- return sc;
-}
-
-static struct stub_syscall *syscall_stub_get_previous(struct mm_id *mm_idp,
- int syscall_type,
- unsigned long virt)
-{
- if (mm_idp->syscall_data_len > 0) {
- struct stub_data *proc_data = (void *) mm_idp->stack;
- struct stub_syscall *sc;
-
- sc = &proc_data->syscall_data[mm_idp->syscall_data_len - 1];
-
- if (sc->syscall == syscall_type &&
- sc->mem.addr + sc->mem.length == virt)
- return sc;
- }
-
- return NULL;
-}
-
-static int get_stub_fd(struct mm_id *mm_idp, int fd)
-{
- int i;
-
- /* Find an FD slot (or flush and use first) */
- if (!using_seccomp)
- return fd;
-
- /* Already crashed, value does not matter */
- if (mm_idp->syscall_data_len < 0)
- return 0;
-
- /* Find existing FD in map if we can allocate another syscall */
- if (mm_idp->syscall_data_len <
- ARRAY_SIZE(((struct stub_data *)NULL)->syscall_data)) {
- for (i = 0; i < mm_idp->syscall_fd_num; i++) {
- if (mm_idp->syscall_fd_map[i] == fd)
- return i;
- }
-
- if (mm_idp->syscall_fd_num < STUB_MAX_FDS) {
- i = mm_idp->syscall_fd_num;
- mm_idp->syscall_fd_map[i] = fd;
-
- mm_idp->syscall_fd_num++;
-
- return i;
- }
- }
-
- /* FD map full or no syscall space available, continue after flush */
- do_syscall_stub(mm_idp);
- mm_idp->syscall_fd_map[0] = fd;
- mm_idp->syscall_fd_num = 1;
-
- return 0;
-}
-
+/*
+ * Install (map) or remove (unmap) a guest mapping in the stub's address space.
+ * Both seccomp and ptrace modes now go through pidfd_mmap()/pidfd_munmap():
+ * the monitor resolves the physmem fd in its own fd table and installs the
+ * mapping into the stub's mm directly, so the stub never has to execute
+ * mmap/munmap itself. This makes the ptrace stub-syscall batcher unnecessary
+ * for memory management and shows the primitive works uniformly across modes.
+ */
int map(struct mm_id *mm_idp, unsigned long virt, unsigned long len, int prot,
int phys_fd, unsigned long long offset)
{
- struct stub_syscall *sc;
-
- if (using_seccomp) {
- struct pidfd_mmap_args args = {
- .size = sizeof(args),
- .addr = virt,
- .len = len,
- .prot = prot,
- .flags = MAP_SHARED | MAP_FIXED,
- .pgoff = offset >> UM_KERN_PAGE_SHIFT,
- .fd = phys_fd,
- };
- long res = syscall(__NR_pidfd_mmap, mm_idp->stub_pidfd, &args, 0);
-
- return res < 0 ? -errno : 0;
- }
+ struct pidfd_mmap_args args = {
+ .size = sizeof(args),
+ .addr = virt,
+ .len = len,
+ .prot = prot,
+ .flags = MAP_SHARED | MAP_FIXED,
+ .pgoff = offset >> UM_KERN_PAGE_SHIFT,
+ .fd = phys_fd,
+ };
+ long res = syscall(__NR_pidfd_mmap, mm_idp->stub_pidfd, &args, 0);
- /* Compress with previous syscall if that is possible */
- sc = syscall_stub_get_previous(mm_idp, STUB_SYSCALL_MMAP, virt);
- if (sc && sc->mem.prot == prot &&
- sc->mem.offset == MMAP_OFFSET(offset - sc->mem.length)) {
- int prev_fd = sc->mem.fd;
-
- if (using_seccomp)
- prev_fd = mm_idp->syscall_fd_map[sc->mem.fd];
-
- if (phys_fd == prev_fd) {
- sc->mem.length += len;
- return 0;
- }
- }
-
- phys_fd = get_stub_fd(mm_idp, phys_fd);
-
- sc = syscall_stub_alloc(mm_idp);
- sc->syscall = STUB_SYSCALL_MMAP;
- sc->mem.addr = virt;
- sc->mem.length = len;
- sc->mem.prot = prot;
- sc->mem.fd = phys_fd;
- sc->mem.offset = MMAP_OFFSET(offset);
-
- return 0;
+ return res < 0 ? -errno : 0;
}
int unmap(struct mm_id *mm_idp, unsigned long addr, unsigned long len)
{
- struct stub_syscall *sc;
-
- if (using_seccomp) {
- long res = syscall(__NR_pidfd_munmap, mm_idp->stub_pidfd,
- addr, len);
-
- return res < 0 ? -errno : 0;
- }
-
- /* Compress with previous syscall if that is possible */
- sc = syscall_stub_get_previous(mm_idp, STUB_SYSCALL_MUNMAP, addr);
- if (sc) {
- sc->mem.length += len;
- return 0;
- }
-
- sc = syscall_stub_alloc(mm_idp);
- sc->syscall = STUB_SYSCALL_MUNMAP;
- sc->mem.addr = addr;
- sc->mem.length = len;
+ long res = syscall(__NR_pidfd_munmap, mm_idp->stub_pidfd, addr, len);
- return 0;
+ return res < 0 ? -errno : 0;
}
diff --git a/arch/um/os-Linux/skas/process.c b/arch/um/os-Linux/skas/process.c
index 3dd97ca7999a..cd6e40451576 100644
--- a/arch/um/os-Linux/skas/process.c
+++ b/arch/um/os-Linux/skas/process.c
@@ -157,39 +157,7 @@ void wait_stub_done_seccomp(struct mm_id *mm_idp, int running, int wait_sigsys)
int ret;
do {
- const char byte = 0;
- struct iovec iov = {
- .iov_base = (void *)&byte,
- .iov_len = sizeof(byte),
- };
- union {
- char data[CMSG_SPACE(sizeof(mm_idp->syscall_fd_map))];
- struct cmsghdr align;
- } ctrl;
- struct msghdr msgh = {
- .msg_iov = &iov,
- .msg_iovlen = 1,
- };
-
if (!running) {
- if (mm_idp->syscall_fd_num) {
- unsigned int fds_size =
- sizeof(int) * mm_idp->syscall_fd_num;
- struct cmsghdr *cmsg;
-
- msgh.msg_control = ctrl.data;
- msgh.msg_controllen = CMSG_SPACE(fds_size);
- cmsg = CMSG_FIRSTHDR(&msgh);
- cmsg->cmsg_level = SOL_SOCKET;
- cmsg->cmsg_type = SCM_RIGHTS;
- cmsg->cmsg_len = CMSG_LEN(fds_size);
- memcpy(CMSG_DATA(cmsg), mm_idp->syscall_fd_map,
- fds_size);
-
- CATCH_EINTR(syscall(__NR_sendmsg, mm_idp->sock,
- &msgh, 0));
- }
-
data->signal = 0;
data->futex = FUTEX_IN_CHILD;
CATCH_EINTR(syscall(__NR_futex, &data->futex,
@@ -482,15 +450,13 @@ int start_userspace(struct mm_id *mm_id)
proc_data->futex = FUTEX_IN_CHILD;
/*
- * In SECCOMP mode, acquire a pidfd to the stub via CLONE_PIDFD (it is
- * returned through the legacy-clone parent_tid argument). The monitor
- * installs guest mappings into the stub's mm directly via pidfd_mmap(),
- * so the stub itself never needs the mmap capability. The ptrace mode
- * does not use it and drives the stub directly.
+ * Acquire a pidfd to the stub via CLONE_PIDFD (it is returned through
+ * the legacy-clone parent_tid argument). The monitor installs guest
+ * mappings into the stub's mm directly via pidfd_mmap() in both seccomp
+ * and ptrace modes, so it needs the pidfd regardless of mode.
*/
mm_id->pid = clone(userspace_tramp, (void *) sp,
- CLONE_VFORK | CLONE_VM | (using_seccomp ? CLONE_PIDFD : 0) |
- SIGCHLD,
+ CLONE_VFORK | CLONE_VM | CLONE_PIDFD | SIGCHLD,
(void *)&tramp_data, &stub_pidfd);
if (mm_id->pid < 0) {
err = -errno;
@@ -498,8 +464,7 @@ int start_userspace(struct mm_id *mm_id)
__func__, errno);
goto out_close;
}
- if (using_seccomp)
- mm_id->stub_pidfd = stub_pidfd;
+ mm_id->stub_pidfd = stub_pidfd;
if (using_seccomp) {
wait_stub_done_seccomp(mm_id, 1, 1);
@@ -539,10 +504,7 @@ int start_userspace(struct mm_id *mm_id)
}
close(tramp_data.sockpair[0]);
- if (using_seccomp)
- mm_id->sock = tramp_data.sockpair[1];
- else
- close(tramp_data.sockpair[1]);
+ close(tramp_data.sockpair[1]);
return 0;
@@ -620,28 +582,15 @@ void userspace(struct uml_pt_regs *regs)
fatal_sigsegv();
}
- /* Must have been reset by the syscall caller */
- if (proc_data->restart_wait != 0)
- panic("Programming error: Flag to only run syscalls in child was not cleared!");
-
- /* Mark pending syscalls for flushing */
- proc_data->syscall_data_len = mm_id->syscall_data_len;
-
+ /*
+ * SECCOMP mode no longer batches stub syscalls (mmap is
+ * installed by the monitor via pidfd_mmap()); this only
+ * relays the guest trap to the monitor.
+ */
wait_stub_done_seccomp(mm_id, 0, 0);
sig = proc_data->signal;
- if (sig == SIGTRAP && proc_data->err != 0) {
- printk(UM_KERN_ERR "%s - Error flushing stub syscalls",
- __func__);
- syscall_stub_dump_error(mm_id);
- mm_id->syscall_data_len = proc_data->err;
- fatal_sigsegv();
- }
-
- mm_id->syscall_data_len = 0;
- mm_id->syscall_fd_num = 0;
-
err = get_stub_state(regs, proc_data, NULL);
if (err) {
printk(UM_KERN_ERR "%s - failed to get regs: %d",
@@ -667,17 +616,6 @@ void userspace(struct uml_pt_regs *regs)
} else {
int pid = mm_id->pid;
- /* Flush out any pending syscalls */
- err = syscall_stub_flush(mm_id);
- if (err) {
- if (err == -ENOMEM)
- report_enomem();
-
- printk(UM_KERN_ERR "%s - Error flushing stub syscalls: %d",
- __func__, -err);
- fatal_sigsegv();
- }
-
/*
* This can legitimately fail if the process loads a
* bogus value into a segment register. It will
diff --git a/arch/um/os-Linux/start_up.c b/arch/um/os-Linux/start_up.c
index 054ac03bbf5e..518107622a2d 100644
--- a/arch/um/os-Linux/start_up.c
+++ b/arch/um/os-Linux/start_up.c
@@ -311,9 +311,11 @@ static bool __init init_seccomp(void)
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANON, 0, 0);
- /* Use the syscall data area as stack, we just need something */
- sp = (unsigned long)&seccomp_test_stub_data->syscall_data +
- sizeof(seccomp_test_stub_data->syscall_data) -
+ /*
+ * We just need some valid stack; use the first page of the shared
+ * stub_data region (sigstack occupies the following page).
+ */
+ sp = (unsigned long)&seccomp_test_stub_data->sigstack[0] -
sizeof(void *);
pid = clone(seccomp_helper, (void *)sp, CLONE_VFORK | CLONE_VM, NULL);
--
2.43.0
^ permalink raw reply [flat|nested] 7+ messages in thread* [RFC PATCH 6/6] selftests/pidfd: add pidfd_mmap()/pidfd_munmap() tests
2026-07-10 20:53 [RFC PATCH 0/6] um: introduce pidfd_mmap()/pidfd_munmap() syscalls Cong Wang
` (4 preceding siblings ...)
2026-07-10 20:53 ` [RFC PATCH 5/6] um: install guest mappings via pidfd_mmap() in both modes Cong Wang
@ 2026-07-10 20:53 ` Cong Wang
5 siblings, 0 replies; 7+ messages in thread
From: Cong Wang @ 2026-07-10 20:53 UTC (permalink / raw)
To: Richard Weinberger, Anton Ivanov, Johannes Berg
Cc: Benjamin Berg, linux-um, linux-kernel, Cong Wang
From: Cong Wang <cwang@multikernel.io>
Exercise the new syscalls against a forked target reached by pidfd:
- install_and_read: map a parent-owned memfd into the target, verify the
target's new mapping shows the memfd contents (read via
process_vm_readv), that MAP_SHARED writes propagate, and that
pidfd_munmap() removes it. This also covers the key semantic that the
backing fd is resolved in the *caller's* fd table (the target never
sees the memfd).
- anonymous: MAP_ANONYMOUS install + write-back round-trip.
- bad_args: __spare != 0, undersized @size, non-zero reserved flags, and
a bad backing fd are all rejected.
- no_ptrace_access: a non-dumpable target (no ptrace access) yields
-EPERM, matching the ptrace_may_access() gate.
The suite skips cleanly (TAP "SKIP") when the kernel lacks the syscall.
The pidfd_mmap ABI definitions are kept in pidfd.h alongside the existing
local uapi copies (struct pidfd_info, etc.).
Assisted-by: Claude:claude-opus-4.8
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
tools/testing/selftests/pidfd/Makefile | 3 +-
tools/testing/selftests/pidfd/pidfd.h | 34 +++
.../testing/selftests/pidfd/pidfd_mmap_test.c | 234 ++++++++++++++++++
3 files changed, 270 insertions(+), 1 deletion(-)
create mode 100644 tools/testing/selftests/pidfd/pidfd_mmap_test.c
diff --git a/tools/testing/selftests/pidfd/Makefile b/tools/testing/selftests/pidfd/Makefile
index 4211f91e9af8..f1c421bf725a 100644
--- a/tools/testing/selftests/pidfd/Makefile
+++ b/tools/testing/selftests/pidfd/Makefile
@@ -4,7 +4,8 @@ CFLAGS += -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES) -pthread -Wall
TEST_GEN_PROGS := pidfd_test pidfd_fdinfo_test pidfd_open_test \
pidfd_poll_test pidfd_wait pidfd_getfd_test pidfd_setns_test \
pidfd_file_handle_test pidfd_bind_mount pidfd_info_test \
- pidfd_xattr_test pidfd_setattr_test pidfd_autoreap_test
+ pidfd_xattr_test pidfd_setattr_test pidfd_autoreap_test \
+ pidfd_mmap_test
TEST_GEN_PROGS_EXTENDED := pidfd_exec_helper
diff --git a/tools/testing/selftests/pidfd/pidfd.h b/tools/testing/selftests/pidfd/pidfd.h
index 5a4e78c10f43..91e65283d5fe 100644
--- a/tools/testing/selftests/pidfd/pidfd.h
+++ b/tools/testing/selftests/pidfd/pidfd.h
@@ -203,6 +203,28 @@ struct pidfd_info {
__u64 supported_mask;
};
+#ifndef __NR_pidfd_mmap
+#define __NR_pidfd_mmap 472
+#endif
+
+#ifndef __NR_pidfd_munmap
+#define __NR_pidfd_munmap 473
+#endif
+
+#ifndef PIDFD_MMAP_ARGS_SIZE_VER0
+struct pidfd_mmap_args {
+ __u64 size;
+ __u64 addr;
+ __u64 len;
+ __u64 prot;
+ __u64 flags;
+ __u64 pgoff;
+ __s32 fd;
+ __u32 __spare;
+};
+#define PIDFD_MMAP_ARGS_SIZE_VER0 56
+#endif
+
/*
* The kernel reserves 300 pids via RESERVED_PIDS in kernel/pid.c
* That means, when it wraps around any pid < 300 will be skipped.
@@ -267,6 +289,18 @@ static inline int sys_pidfd_getfd(int pidfd, int fd, int flags)
return syscall(__NR_pidfd_getfd, pidfd, fd, flags);
}
+static inline long sys_pidfd_mmap(int pidfd, struct pidfd_mmap_args *args,
+ unsigned int flags)
+{
+ return syscall(__NR_pidfd_mmap, pidfd, args, flags);
+}
+
+static inline long sys_pidfd_munmap(int pidfd, unsigned long addr,
+ unsigned long len)
+{
+ return syscall(__NR_pidfd_munmap, pidfd, addr, len);
+}
+
static inline int sys_memfd_create(const char *name, unsigned int flags)
{
return syscall(__NR_memfd_create, name, flags);
diff --git a/tools/testing/selftests/pidfd/pidfd_mmap_test.c b/tools/testing/selftests/pidfd/pidfd_mmap_test.c
new file mode 100644
index 000000000000..39d623ce7b9d
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_mmap_test.c
@@ -0,0 +1,234 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syscall.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/prctl.h>
+#include <sys/socket.h>
+#include <sys/uio.h>
+#include <sys/wait.h>
+
+#include "pidfd.h"
+#include "kselftest_harness.h"
+
+#define UID_NOBODY 65535
+#define PATTERN 0xab
+#define MAP_LEN (4 * 1024)
+
+static struct pidfd_mmap_args mmap_args(int fd)
+{
+ struct pidfd_mmap_args args = {
+ .size = sizeof(args),
+ .addr = 0, /* let the kernel choose */
+ .len = MAP_LEN,
+ .prot = PROT_READ | PROT_WRITE,
+ .flags = MAP_SHARED,
+ .pgoff = 0,
+ .fd = fd,
+ };
+
+ return args;
+}
+
+static int read_remote(pid_t pid, unsigned long addr, void *buf, size_t len)
+{
+ struct iovec local = { .iov_base = buf, .iov_len = len };
+ struct iovec remote = { .iov_base = (void *)addr, .iov_len = len };
+
+ return process_vm_readv(pid, &local, 1, &remote, 1, 0) == (ssize_t)len ?
+ 0 : -1;
+}
+
+static int target(int sk)
+{
+ char buf;
+ int ret;
+
+ prctl(PR_SET_PDEATHSIG, SIGKILL);
+
+ if (send(sk, "R", 1, 0) != 1) /* ready */
+ return -1;
+
+ while ((ret = recv(sk, &buf, sizeof(buf), 0)) > 0) {
+ if (buf == 'P' && prctl(PR_SET_DUMPABLE, 0) < 0)
+ return -1;
+ if (send(sk, &buf, 1, 0) != 1)
+ return -1;
+ }
+
+ return ret < 0 ? -1 : 0;
+}
+
+FIXTURE(pidfd_mmap)
+{
+ pid_t pid;
+ int pidfd;
+ int sk; /* parent side of the socketpair */
+ int memfd; /* lives in the PARENT fd table */
+ void *local; /* the parent's own mapping of memfd */
+};
+
+FIXTURE_SETUP(pidfd_mmap)
+{
+ struct pidfd_mmap_args probe;
+ int sk_pair[2];
+ char c;
+
+ /*
+ * The backing fd is resolved in the *caller's* (parent's) fd table,
+ * so the target never needs to see it.
+ */
+ self->memfd = sys_memfd_create("pidfd_mmap", 0);
+ ASSERT_GE(self->memfd, 0);
+ ASSERT_EQ(0, ftruncate(self->memfd, MAP_LEN));
+
+ self->local = mmap(NULL, MAP_LEN, PROT_READ | PROT_WRITE, MAP_SHARED,
+ self->memfd, 0);
+ ASSERT_NE(MAP_FAILED, self->local);
+ memset(self->local, PATTERN, MAP_LEN);
+
+ ASSERT_EQ(0, socketpair(PF_LOCAL, SOCK_SEQPACKET, 0, sk_pair));
+ self->sk = sk_pair[0];
+
+ self->pid = fork();
+ ASSERT_GE(self->pid, 0);
+ if (self->pid == 0) {
+ close(sk_pair[0]);
+ _exit(target(sk_pair[1]) ? EXIT_FAILURE : EXIT_SUCCESS);
+ }
+ close(sk_pair[1]);
+
+ self->pidfd = sys_pidfd_open(self->pid, 0);
+ ASSERT_GE(self->pidfd, 0);
+
+ ASSERT_EQ(1, recv(self->sk, &c, 1, 0)); /* wait for "R" */
+
+ /* Skip the whole suite if the kernel does not have pidfd_mmap(). */
+ probe = mmap_args(self->memfd);
+ sys_pidfd_mmap(self->pidfd, &probe, 1); /* reserved flags -> EINVAL */
+ if (errno == ENOSYS)
+ SKIP(return, "pidfd_mmap() is not supported");
+}
+
+FIXTURE_TEARDOWN(pidfd_mmap)
+{
+ EXPECT_EQ(0, close(self->pidfd));
+ EXPECT_EQ(0, close(self->sk)); /* tells the target to exit */
+ munmap(self->local, MAP_LEN);
+ close(self->memfd);
+ EXPECT_EQ(0, wait_for_pid(self->pid));
+}
+
+TEST_F(pidfd_mmap, install_and_read)
+{
+ struct pidfd_mmap_args args = mmap_args(self->memfd);
+ char buf[MAP_LEN];
+ long addr;
+ int i;
+
+ addr = sys_pidfd_mmap(self->pidfd, &args, 0);
+ ASSERT_GE(addr, 0);
+ ASSERT_EQ(0, addr & (sysconf(_SC_PAGESIZE) - 1)); /* page aligned */
+
+ /* The target's new mapping must show the memfd contents. */
+ ASSERT_EQ(0, read_remote(self->pid, addr, buf, MAP_LEN));
+ for (i = 0; i < MAP_LEN; i++)
+ ASSERT_EQ((unsigned char)PATTERN, (unsigned char)buf[i]);
+
+ /* MAP_SHARED: a write the parent makes to memfd is visible remotely. */
+ memset(self->local, 0xcd, MAP_LEN);
+ ASSERT_EQ(0, read_remote(self->pid, addr, buf, sizeof(buf)));
+ ASSERT_EQ((unsigned char)0xcd, (unsigned char)buf[0]);
+
+ /* pidfd_munmap() removes it. */
+ ASSERT_EQ(0, sys_pidfd_munmap(self->pidfd, addr, MAP_LEN));
+ EXPECT_EQ(-1, read_remote(self->pid, addr, buf, MAP_LEN));
+}
+
+TEST_F(pidfd_mmap, anonymous)
+{
+ struct pidfd_mmap_args args = mmap_args(-1);
+ struct iovec local, remote;
+ char in[64], out[64];
+ long addr;
+
+ args.flags = MAP_PRIVATE | MAP_ANONYMOUS;
+
+ addr = sys_pidfd_mmap(self->pidfd, &args, 0);
+ ASSERT_GE(addr, 0);
+
+ memset(out, 0x5a, sizeof(out));
+ local.iov_base = out;
+ local.iov_len = sizeof(out);
+ remote.iov_base = (void *)addr;
+ remote.iov_len = sizeof(out);
+ ASSERT_EQ((ssize_t)sizeof(out),
+ process_vm_writev(self->pid, &local, 1, &remote, 1, 0));
+
+ ASSERT_EQ(0, read_remote(self->pid, addr, in, sizeof(in)));
+ ASSERT_EQ((unsigned char)0x5a, (unsigned char)in[0]);
+
+ ASSERT_EQ(0, sys_pidfd_munmap(self->pidfd, addr, MAP_LEN));
+}
+
+TEST_F(pidfd_mmap, bad_args)
+{
+ struct pidfd_mmap_args args;
+
+ /* The reserved padding must be zero. */
+ args = mmap_args(self->memfd);
+ args.__spare = 1;
+ EXPECT_EQ(-1, sys_pidfd_mmap(self->pidfd, &args, 0));
+ EXPECT_EQ(EINVAL, errno);
+
+ /* @size below the first published version is rejected. */
+ args = mmap_args(self->memfd);
+ args.size = 8;
+ EXPECT_EQ(-1, sys_pidfd_mmap(self->pidfd, &args, 0));
+ EXPECT_EQ(EINVAL, errno);
+
+ /* The reserved flags argument must be zero. */
+ args = mmap_args(self->memfd);
+ EXPECT_EQ(-1, sys_pidfd_mmap(self->pidfd, &args, 1));
+ EXPECT_EQ(EINVAL, errno);
+
+ /* A bad backing fd (resolved in the caller's table) is -EBADF. */
+ args = mmap_args(-1);
+ EXPECT_EQ(-1, sys_pidfd_mmap(self->pidfd, &args, 0));
+ EXPECT_EQ(EBADF, errno);
+
+ /* A bad pidfd fails. */
+ args = mmap_args(self->memfd);
+ EXPECT_EQ(-1, sys_pidfd_mmap(-1, &args, 0));
+}
+
+TEST_F(pidfd_mmap, no_ptrace_access)
+{
+ struct pidfd_mmap_args args = mmap_args(self->memfd);
+ int uid = getuid();
+ char c;
+
+ /* Drop privilege so CAP_SYS_PTRACE can't bypass the dumpable check. */
+ if (uid == 0)
+ ASSERT_EQ(0, seteuid(UID_NOBODY));
+
+ ASSERT_EQ(1, send(self->sk, "P", 1, 0)); /* target: PR_SET_DUMPABLE 0 */
+ ASSERT_EQ(1, recv(self->sk, &c, 1, 0));
+
+ EXPECT_EQ(-1, sys_pidfd_mmap(self->pidfd, &args, 0));
+ EXPECT_EQ(EPERM, errno);
+
+ if (uid == 0)
+ ASSERT_EQ(0, seteuid(0));
+}
+
+TEST_HARNESS_MAIN
--
2.43.0
^ permalink raw reply [flat|nested] 7+ messages in thread