* [PATCH bpf-next v8 0/5] bpf: Add user memory access kfuncs for mm_struct
@ 2026-09-18 9:10 Anastasios Papagiannis
2026-09-18 9:10 ` [PATCH bpf-next v8 1/5] mm: Add copy_remote_mm_str() Anastasios Papagiannis
` (4 more replies)
0 siblings, 5 replies; 12+ messages in thread
From: Anastasios Papagiannis @ 2026-09-18 9:10 UTC (permalink / raw)
To: bpf
Cc: linux-fsdevel, linux-kernel, linux-mm, david, akpm, andrii, ast,
brauner, daniel, eddyz87, kpsingh, ljs, matt, memxor, song,
sun.jian.kdev, utilityemal77, viro, tasos.papagiannnis
On MMU systems, during exec, argument and environment strings are copied
into the new address space held by struct linux_binprm before that address
space is installed on the task_struct. Existing BPF user memory helpers
operate on the current address space or one associated with a task_struct.
Because no task_struct refers to the new address space at this point,
programs cannot access these strings from the bprm_check_security LSM
hook.
This series adds two sleepable BPF kfuncs for copying bytes or
NUL-terminated strings from a trusted struct mm_struct. It also marks
linux_binprm->mm as trusted-or-null, allowing BPF LSM programs to pass it
to the kfuncs after a NULL check and inspect exec arguments before
allowing the exec to continue.
To make the trusted-or-null annotation safe, the exec paths are hardened
to clear bprm->mm before dropping the reference it owns.
On NOMMU systems, exec argument and environment strings remain in
bprm->page[] until they are transferred to the new process stack. They
cannot be accessed through bprm->mm at the bprm_check_security hook.
The new kfuncs remain available on NOMMU for address ranges represented
by a supplied struct mm_struct.
The series adds selftests covering both kfuncs while reading argument and
environment strings from linux_binprm during exec.
Changes in v8:
- Removed CONFIG_BPF_SYSCALL #ifdefs from mm/memory.c and mm/nommu.c.
- Reformatted the selftest to use 120-character lines.
- Removed the CONFIG_MMU check from the selftests.
- Removed the extern declarations from the selftests, as they already
exist in vmlinux.h.
- Moved the checks to user space in the selftests.
Changes in v7:
- Use execvpe() instead of a hardcoded /bin/true path in the
copy_from_user_bprm selftest while retaining the controlled environment.
- Remove the unused bpf_misc.h include.
- Rewrap the invalid-flags kfunc call to stay within 80 columns.
- Fix indent in mm patches.
Changes in v6:
- Drop redundant extern keywords and CONFIG_BPF_SYSCALL guards from the
remote memory copy declarations.
- Drop the explicit bpf_copy_from_user_mm() prototype and share copy
logic through inlineable static internal helpers instead of calling
between kfunc/helper entry points.
- Validate flags and zero-length requests before acquiring the task's
mm, preserving existing behavior and ensuring internal helpers are
called only with a live mm.
- Drop the verifier relaxation for unchecked reads through trusted-or-null
pointers and its tests.
- Fix the existing LSM selftest to explicitly NULL-check bprm->mm after
marking the field trusted-or-null.
Changes in v5:
- Move the shared wrappers to mm/util.c and handle zero-length requests
at the entry points.
Changes in v4:
- Add negative verifier tests for atomic RMW and load-acquire accesses
through trusted-or-null BTF pointers.
- Preserve explicit nullability-marking coverage for tracepoint
arguments, dentry->d_inode, and sched_ext .dispatch.
- Use the already-nullable mmap_file argument for the negative store
test, avoiding dependency on the later linux_binprm->mm marking.
- Clarify the bprm->mm lifetime invariant and move its lifetime fix
before the mm_struct kfunc patch.
- Reword the trusted-or-null read change in imperative mood and remove
its redundant before-and-after summary.
- Document that the existing task-based user-memory interfaces delegate
to the new mm-based implementations, reorder the string-copy kfuncs to
remove an unnecessary declaration, and annotate the remaining
declaration with __bpf_kfunc.
Changes in v3:
- Replace the linux_binprm-specific kfuncs with generic struct mm_struct
kfuncs, as suggested by Andrii Nakryiko.
- Move the kfuncs next to the existing user memory helpers and make the
task-based variants delegate to the new mm-based implementations, as
suggested by Andrii Nakryiko.
- Clear bprm->mm before dropping its reference on exec error paths.
- Mark linux_binprm->mm as trusted-or-null.
- Allow fault-protected reads through trusted-or-null BTF pointers to
preserve compatibility with existing BPF programs, as suggested by
Andrii Nakryiko.
- Add verifier and runtime selftests for trusted-or-null BTF pointer
reads.
- Rename __copy_remote_vm_str() to __copy_remote_mm_str(), as suggested
by Andrii Nakryiko.
- Clarify that reading exec strings through bprm->mm is limited to MMU
systems, while the generic mm-based kfuncs remain available on NOMMU.
Changes in v2:
- Register the kfuncs on NOMMU systems and return -EOPNOTSUPP when called,
as suggested by Justin Suess.
- Add selftest coverage for reading environment strings, as suggested by
Justin Suess.
- Clarify that copy_remote_mm_str() leaves the destination untouched when
called with a zero-length buffer.
- Use sizeof() instead of hardcoded argument lengths in the selftests.
- Use ~0ULL for invalid-flags checks in the selftests.
v7:
https://lore.kernel.org/bpf/20260915080255.48929-1-tasos.papagiannnis@gmail.com/
v6:
https://lore.kernel.org/all/20260908135302.74963-1-tasos.papagiannnis@gmail.com/
v5:
https://lore.kernel.org/bpf/20260907165220.52431-1-tasos.papagiannnis@gmail.com/
v4:
https://lore.kernel.org/bpf/20260904145340.40212-1-tasos.papagiannnis@gmail.com/
v3:
https://lore.kernel.org/bpf/20260831092305.42062-1-tasos.papagiannnis@gmail.com/
v2:
https://lore.kernel.org/bpf/20260820131801.68759-1-tasos.papagiannnis@gmail.com/
v1:
https://lore.kernel.org/bpf/20260812111140.7762-1-tasos.papagiannnis@gmail.com/
Anastasios Papagiannis (5):
mm: Add copy_remote_mm_str()
exec: Clear bprm->mm before dropping its reference
bpf: Add user memory access kfuncs for mm_struct
bpf: Mark linux_binprm->mm as trusted-or-null
selftests/bpf: Test mm_struct user memory kfuncs with linux_binprm
fs/exec.c | 7 +-
include/linux/mm.h | 8 +-
kernel/bpf/helpers.c | 130 ++++++++++++++++--
kernel/bpf/verifier.c | 5 +
mm/internal.h | 3 +
mm/memory.c | 43 +-----
mm/nommu.c | 43 +-----
mm/util.c | 62 +++++++++
.../bpf/prog_tests/copy_from_user_bprm.c | 65 +++++++++
.../selftests/bpf/progs/copy_from_user_bprm.c | 69 ++++++++++
tools/testing/selftests/bpf/progs/lsm.c | 5 +-
11 files changed, 339 insertions(+), 101 deletions(-)
create mode 100644 tools/testing/selftests/bpf/prog_tests/copy_from_user_bprm.c
create mode 100644 tools/testing/selftests/bpf/progs/copy_from_user_bprm.c
base-commit: 961b8946acb482b6d7a39c266d623e5f9c4e873f
--
2.55.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH bpf-next v8 1/5] mm: Add copy_remote_mm_str()
2026-09-18 9:10 [PATCH bpf-next v8 0/5] bpf: Add user memory access kfuncs for mm_struct Anastasios Papagiannis
@ 2026-09-18 9:10 ` Anastasios Papagiannis
2026-09-18 10:28 ` bot+bpf-ci
2026-09-18 12:09 ` David Hildenbrand (Arm)
2026-09-18 9:10 ` [PATCH bpf-next v8 2/5] exec: Clear bprm->mm before dropping its reference Anastasios Papagiannis
` (3 subsequent siblings)
4 siblings, 2 replies; 12+ messages in thread
From: Anastasios Papagiannis @ 2026-09-18 9:10 UTC (permalink / raw)
To: bpf
Cc: linux-fsdevel, linux-kernel, linux-mm, david, akpm, andrii, ast,
brauner, daniel, eddyz87, kpsingh, ljs, matt, memxor, song,
sun.jian.kdev, utilityemal77, viro, tasos.papagiannnis
copy_remote_vm_str() gets the target address space from a struct
task_struct. This does not work for an address space that exists but is
not yet associated with a task_struct, such as the mm held by struct
linux_binprm during exec.
Add copy_remote_mm_str(), which operates directly on a struct mm_struct.
Use a common internal interface for the MMU and NOMMU implementations
and define both public wrappers in mm/util.c. Preserve the existing
copy_remote_vm_str() behavior, including handling zero-length requests
before acquiring the task's mm.
Signed-off-by: Anastasios Papagiannis <tasos.papagiannnis@gmail.com>
Acked-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
Acked-by: David Hildenbrand (Arm) <david@kernel.org>
---
include/linux/mm.h | 8 +++---
mm/internal.h | 3 +++
mm/memory.c | 43 ++------------------------------
mm/nommu.c | 43 ++------------------------------
mm/util.c | 62 ++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 73 insertions(+), 86 deletions(-)
diff --git a/include/linux/mm.h b/include/linux/mm.h
index dd09c438fa23..63f40e615754 100644
--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -3325,10 +3325,10 @@ extern int access_process_vm(struct task_struct *tsk, unsigned long addr,
extern int access_remote_vm(struct mm_struct *mm, unsigned long addr,
void *buf, int len, unsigned int gup_flags);
-#ifdef CONFIG_BPF_SYSCALL
-extern int copy_remote_vm_str(struct task_struct *tsk, unsigned long addr,
- void *buf, int len, unsigned int gup_flags);
-#endif
+int copy_remote_mm_str(struct mm_struct *mm, unsigned long addr,
+ void *buf, int len, unsigned int gup_flags);
+int copy_remote_vm_str(struct task_struct *tsk, unsigned long addr,
+ void *buf, int len, unsigned int gup_flags);
long get_user_pages_remote(struct mm_struct *mm,
unsigned long start, unsigned long nr_pages,
diff --git a/mm/internal.h b/mm/internal.h
index 38b1165212c9..557b29381355 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -25,6 +25,9 @@
struct folio_batch;
struct hstate;
+int __copy_remote_mm_str(struct mm_struct *mm, unsigned long addr,
+ void *buf, int len, unsigned int gup_flags);
+
struct huge_bootmem_page {
struct list_head list;
struct hstate *hstate;
diff --git a/mm/memory.c b/mm/memory.c
index 8b0c2c735d3d..fc6933d7e9d3 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -7326,13 +7326,12 @@ int access_process_vm(struct task_struct *tsk, unsigned long addr,
}
EXPORT_SYMBOL_GPL(access_process_vm);
-#ifdef CONFIG_BPF_SYSCALL
/*
* Copy a string from another process's address space as given in mm.
* If there is any error return -EFAULT.
*/
-static int __copy_remote_vm_str(struct mm_struct *mm, unsigned long addr,
- void *buf, int len, unsigned int gup_flags)
+int __copy_remote_mm_str(struct mm_struct *mm, unsigned long addr,
+ void *buf, int len, unsigned int gup_flags)
{
void *old_buf = buf;
int err = 0;
@@ -7408,44 +7407,6 @@ static int __copy_remote_vm_str(struct mm_struct *mm, unsigned long addr,
return buf - old_buf;
}
-/**
- * copy_remote_vm_str - copy a string from another process's address space.
- * @tsk: the task of the target address space
- * @addr: start address to read from
- * @buf: destination buffer
- * @len: number of bytes to copy
- * @gup_flags: flags modifying lookup behaviour
- *
- * The caller must hold a reference on @mm.
- *
- * Return: number of bytes copied from @addr (source) to @buf (destination);
- * not including the trailing NUL. Always guaranteed to leave NUL-terminated
- * buffer. On any error, return -EFAULT.
- */
-int copy_remote_vm_str(struct task_struct *tsk, unsigned long addr,
- void *buf, int len, unsigned int gup_flags)
-{
- struct mm_struct *mm;
- int ret;
-
- if (unlikely(len == 0))
- return 0;
-
- mm = get_task_mm(tsk);
- if (!mm) {
- *(char *)buf = '\0';
- return -EFAULT;
- }
-
- ret = __copy_remote_vm_str(mm, addr, buf, len, gup_flags);
-
- mmput(mm);
-
- return ret;
-}
-EXPORT_SYMBOL_GPL(copy_remote_vm_str);
-#endif /* CONFIG_BPF_SYSCALL */
-
/*
* Print the name of a VMA.
*/
diff --git a/mm/nommu.c b/mm/nommu.c
index 498e01ee40b0..9a810c35e7e9 100644
--- a/mm/nommu.c
+++ b/mm/nommu.c
@@ -1741,13 +1741,12 @@ int access_process_vm(struct task_struct *tsk, unsigned long addr, void *buf, in
}
EXPORT_SYMBOL_GPL(access_process_vm);
-#ifdef CONFIG_BPF_SYSCALL
/*
* Copy a string from another process's address space as given in mm.
* If there is any error return -EFAULT.
*/
-static int __copy_remote_vm_str(struct mm_struct *mm, unsigned long addr,
- void *buf, int len)
+int __copy_remote_mm_str(struct mm_struct *mm, unsigned long addr,
+ void *buf, int len, unsigned int gup_flags)
{
unsigned long addr_end;
struct vm_area_struct *vma;
@@ -1782,44 +1781,6 @@ static int __copy_remote_vm_str(struct mm_struct *mm, unsigned long addr,
return ret;
}
-/**
- * copy_remote_vm_str - copy a string from another process's address space.
- * @tsk: the task of the target address space
- * @addr: start address to read from
- * @buf: destination buffer
- * @len: number of bytes to copy
- * @gup_flags: flags modifying lookup behaviour (unused)
- *
- * The caller must hold a reference on @mm.
- *
- * Return: number of bytes copied from @addr (source) to @buf (destination);
- * not including the trailing NUL. Always guaranteed to leave NUL-terminated
- * buffer. On any error, return -EFAULT.
- */
-int copy_remote_vm_str(struct task_struct *tsk, unsigned long addr,
- void *buf, int len, unsigned int gup_flags)
-{
- struct mm_struct *mm;
- int ret;
-
- if (unlikely(len == 0))
- return 0;
-
- mm = get_task_mm(tsk);
- if (!mm) {
- *(char *)buf = '\0';
- return -EFAULT;
- }
-
- ret = __copy_remote_vm_str(mm, addr, buf, len);
-
- mmput(mm);
-
- return ret;
-}
-EXPORT_SYMBOL_GPL(copy_remote_vm_str);
-#endif /* CONFIG_BPF_SYSCALL */
-
/**
* nommu_shrink_inode_mappings - Shrink the shared mappings on an inode
* @inode: The inode to check
diff --git a/mm/util.c b/mm/util.c
index bf0513d1d3d0..47c2e3ae8496 100644
--- a/mm/util.c
+++ b/mm/util.c
@@ -1061,6 +1061,68 @@ int get_cmdline(struct task_struct *task, char *buffer, int buflen)
return res;
}
+#ifdef CONFIG_BPF_SYSCALL
+/**
+ * copy_remote_mm_str - copy a string from a remote address space.
+ * @mm: the remote address space
+ * @addr: start address to read from
+ * @buf: destination buffer
+ * @len: number of bytes to copy
+ * @gup_flags: flags modifying lookup behaviour
+ *
+ * The caller must hold a reference on @mm.
+ *
+ * Return: number of bytes copied from @addr (source) to @buf (destination),
+ * not including the trailing NUL. If @len is zero, return 0 without accessing
+ * @buf. Otherwise, @buf is always NUL-terminated. On any error, return
+ * -EFAULT.
+ */
+int copy_remote_mm_str(struct mm_struct *mm, unsigned long addr,
+ void *buf, int len, unsigned int gup_flags)
+{
+ if (unlikely(len == 0))
+ return 0;
+
+ return __copy_remote_mm_str(mm, addr, buf, len, gup_flags);
+}
+
+/**
+ * copy_remote_vm_str - copy a string from another process's address space.
+ * @tsk: the task of the target address space
+ * @addr: start address to read from
+ * @buf: destination buffer
+ * @len: number of bytes to copy
+ * @gup_flags: flags modifying lookup behaviour
+ *
+ * Return: number of bytes copied from @addr (source) to @buf (destination),
+ * not including the trailing NUL. If @len is zero, return 0 without accessing
+ * @buf. Otherwise, @buf is always NUL-terminated. On any error, return
+ * -EFAULT.
+ */
+int copy_remote_vm_str(struct task_struct *tsk, unsigned long addr,
+ void *buf, int len, unsigned int gup_flags)
+{
+ struct mm_struct *mm;
+ int ret;
+
+ if (unlikely(len == 0))
+ return 0;
+
+ mm = get_task_mm(tsk);
+ if (!mm) {
+ *(char *)buf = '\0';
+ return -EFAULT;
+ }
+
+ ret = __copy_remote_mm_str(mm, addr, buf, len, gup_flags);
+
+ mmput(mm);
+
+ return ret;
+}
+EXPORT_SYMBOL_GPL(copy_remote_vm_str);
+#endif /* CONFIG_BPF_SYSCALL */
+
int __weak memcmp_pages(struct page *page1, struct page *page2)
{
char *addr1, *addr2;
--
2.55.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH bpf-next v8 2/5] exec: Clear bprm->mm before dropping its reference
2026-09-18 9:10 [PATCH bpf-next v8 0/5] bpf: Add user memory access kfuncs for mm_struct Anastasios Papagiannis
2026-09-18 9:10 ` [PATCH bpf-next v8 1/5] mm: Add copy_remote_mm_str() Anastasios Papagiannis
@ 2026-09-18 9:10 ` Anastasios Papagiannis
2026-09-18 9:10 ` [PATCH bpf-next v8 3/5] bpf: Add user memory access kfuncs for mm_struct Anastasios Papagiannis
` (2 subsequent siblings)
4 siblings, 0 replies; 12+ messages in thread
From: Anastasios Papagiannis @ 2026-09-18 9:10 UTC (permalink / raw)
To: bpf
Cc: linux-fsdevel, linux-kernel, linux-mm, david, akpm, andrii, ast,
brauner, daniel, eddyz87, kpsingh, ljs, matt, memxor, song,
sun.jian.kdev, utilityemal77, viro, tasos.papagiannnis
Once mmput() drops the final reference to bprm->mm, the pointer must no
longer remain accessible through struct linux_binprm.
The successful exec path and the bprm initialization error path already
clear bprm->mm when ownership is transferred or released. Do the same in
free_bprm() before calling mmput().
This is required for BPF kfuncs where bprm->mm is either NULL or points
to a live mm_struct to ensure safe access.
Signed-off-by: Anastasios Papagiannis <tasos.papagiannnis@gmail.com>
Reviewed-by: Sun Jian <sun.jian.kdev@gmail.com>
---
fs/exec.c | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/fs/exec.c b/fs/exec.c
index 745f6eb5279e..4ddd403fd91c 100644
--- a/fs/exec.c
+++ b/fs/exec.c
@@ -1456,9 +1456,12 @@ void bprm_drop_loader(struct linux_binprm *bprm)
static void free_bprm(struct linux_binprm *bprm)
{
- if (bprm->mm) {
+ struct mm_struct *mm = bprm->mm;
+
+ if (mm) {
acct_arg_size(bprm, 0);
- mmput(bprm->mm);
+ bprm->mm = NULL;
+ mmput(mm);
}
if (bprm->user_ns)
put_user_ns(bprm->user_ns);
--
2.55.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH bpf-next v8 3/5] bpf: Add user memory access kfuncs for mm_struct
2026-09-18 9:10 [PATCH bpf-next v8 0/5] bpf: Add user memory access kfuncs for mm_struct Anastasios Papagiannis
2026-09-18 9:10 ` [PATCH bpf-next v8 1/5] mm: Add copy_remote_mm_str() Anastasios Papagiannis
2026-09-18 9:10 ` [PATCH bpf-next v8 2/5] exec: Clear bprm->mm before dropping its reference Anastasios Papagiannis
@ 2026-09-18 9:10 ` Anastasios Papagiannis
2026-09-18 9:30 ` sashiko-bot
2026-09-18 9:10 ` [PATCH bpf-next v8 4/5] bpf: Mark linux_binprm->mm as trusted-or-null Anastasios Papagiannis
2026-09-18 9:10 ` [PATCH bpf-next v8 5/5] selftests/bpf: Test mm_struct user memory kfuncs with linux_binprm Anastasios Papagiannis
4 siblings, 1 reply; 12+ messages in thread
From: Anastasios Papagiannis @ 2026-09-18 9:10 UTC (permalink / raw)
To: bpf
Cc: linux-fsdevel, linux-kernel, linux-mm, david, akpm, andrii, ast,
brauner, daniel, eddyz87, kpsingh, ljs, matt, memxor, song,
sun.jian.kdev, utilityemal77, viro, tasos.papagiannnis
On CONFIG_MMU kernels, when security_bprm_check() runs, the argument and
environment strings for the exec have been copied into bprm->mm. The new
address space is not associated with a task_struct until exec_mmap(), so
existing BPF user memory helpers cannot access it.
Add bpf_copy_from_user_mm() and bpf_copy_from_user_mm_str() kfuncs. Both
take a struct mm_struct pointer directly, allowing callers to access
trusted address spaces that are not associated with a task_struct.
bpf_copy_from_user_mm() has similar semantics to
bpf_copy_from_user_task(). bpf_copy_from_user_mm_str() copies one
NUL-terminated string and returns its size including the NUL terminator.
It accepts BPF_F_PAD_ZEROS to clear unused destination bytes on success.
Refactor the task-based helpers and the new mm-based kfuncs to share
static internal implementations. The task-based interfaces validate their
arguments before acquiring and holding a reference to the task's mm for
the copy. No behavior change is intended for the existing task-based
interfaces.
Register both new kfuncs and mark them KF_SLEEPABLE because accessing a
remote address space can fault.
Signed-off-by: Anastasios Papagiannis <tasos.papagiannnis@gmail.com>
Reviewed-by: Matt Bobrowski <matt@bobrowski.net>
---
kernel/bpf/helpers.c | 130 +++++++++++++++++++++++++++++++++++++++----
1 file changed, 118 insertions(+), 12 deletions(-)
diff --git a/kernel/bpf/helpers.c b/kernel/bpf/helpers.c
index 051b6654e57c..f6b3eee6098a 100644
--- a/kernel/bpf/helpers.c
+++ b/kernel/bpf/helpers.c
@@ -679,9 +679,44 @@ const struct bpf_func_proto bpf_copy_from_user_proto = {
.arg3_type = ARG_ANYTHING,
};
+static int __bpf_copy_from_user_mm(void *dst, u32 size,
+ const void __user *user_ptr,
+ struct mm_struct *mm)
+{
+ int ret;
+
+ ret = access_remote_vm(mm, (unsigned long)user_ptr, dst, size, 0);
+ if (ret == size)
+ return 0;
+
+ memset(dst, 0, size);
+ /* Return -EFAULT for partial read */
+ return ret < 0 ? ret : -EFAULT;
+}
+
+static int __bpf_copy_from_user_mm_str(void *dst, u32 size,
+ const void __user *user_ptr,
+ struct mm_struct *mm, u64 flags)
+{
+ int ret;
+
+ ret = copy_remote_mm_str(mm, (unsigned long)user_ptr, dst, size, 0);
+ if (ret < 0) {
+ if (flags & BPF_F_PAD_ZEROS)
+ memset(dst, 0, size);
+ return ret;
+ }
+
+ if (flags & BPF_F_PAD_ZEROS)
+ memset(dst + ret, 0, size - ret);
+
+ return ret + 1;
+}
+
BPF_CALL_5(bpf_copy_from_user_task, void *, dst, u32, size,
const void __user *, user_ptr, struct task_struct *, tsk, u64, flags)
{
+ struct mm_struct *mm;
int ret;
/* flags is not used yet */
@@ -691,13 +726,16 @@ BPF_CALL_5(bpf_copy_from_user_task, void *, dst, u32, size,
if (unlikely(!size))
return 0;
- ret = access_process_vm(tsk, (unsigned long)user_ptr, dst, size, 0);
- if (ret == size)
- return 0;
+ mm = get_task_mm(tsk);
+ if (!mm) {
+ memset(dst, 0, size);
+ return -EFAULT;
+ }
- memset(dst, 0, size);
- /* Return -EFAULT for partial read */
- return ret < 0 ? ret : -EFAULT;
+ ret = __bpf_copy_from_user_mm(dst, size, user_ptr, mm);
+ mmput(mm);
+
+ return ret;
}
const struct bpf_func_proto bpf_copy_from_user_task_proto = {
@@ -3659,6 +3697,68 @@ __bpf_kfunc int bpf_copy_from_user_str(void *dst, u32 dst__sz, const void __user
return ret + 1;
}
+/**
+ * bpf_copy_from_user_mm() - Copy data from an address space
+ * @dst: Destination address, in kernel space
+ * @dst__sz: Number of bytes to copy
+ * @unsafe_ptr__ign: Source address in the address space
+ * @mm: Address space to copy from
+ * @flags: Reserved for future use; must be zero
+ *
+ * Copies data from the user address space associated with @mm. The destination
+ * is zeroed if an attempted copy cannot be completed in full. Unsupported
+ * flags return -EINVAL without modifying @dst.
+ *
+ * Return: 0 on success, -EINVAL if @flags is non-zero, or -EFAULT if the copy
+ * fails or is partial.
+ */
+__bpf_kfunc int bpf_copy_from_user_mm(void *dst, u32 dst__sz,
+ const void __user *unsafe_ptr__ign,
+ struct mm_struct *mm, u64 flags)
+{
+ if (unlikely(flags))
+ return -EINVAL;
+
+ if (unlikely(!dst__sz))
+ return 0;
+
+ return __bpf_copy_from_user_mm(dst, dst__sz, unsafe_ptr__ign, mm);
+}
+
+/**
+ * bpf_copy_from_user_mm_str() - Copy a string from an address space
+ * @dst: Destination address, in kernel space. This buffer must be
+ * at least @dst__sz bytes long
+ * @dst__sz: Maximum number of bytes to copy, including the trailing NUL
+ * @unsafe_ptr__ign: Source address in the address space
+ * @mm: Address space to copy from
+ * @flags: The only supported flag is BPF_F_PAD_ZEROS
+ *
+ * Copies a NUL-terminated string from the user address space associated with
+ * @mm. If the string is too long, @dst is still NUL-terminated unless @dst__sz
+ * is zero.
+ *
+ * If the flags are valid and BPF_F_PAD_ZEROS is set, the unused portion of
+ * @dst is cleared on success and all of @dst is cleared on a copy failure.
+ * Unsupported flags return -EINVAL without modifying @dst.
+ *
+ * Return: The number of copied bytes including the NUL terminator on success,
+ * or a negative error code on failure.
+ */
+__bpf_kfunc int bpf_copy_from_user_mm_str(void *dst, u32 dst__sz,
+ const void __user *unsafe_ptr__ign,
+ struct mm_struct *mm, u64 flags)
+{
+ if (unlikely(flags & ~BPF_F_PAD_ZEROS))
+ return -EINVAL;
+
+ if (unlikely(dst__sz == 0))
+ return 0;
+
+ return __bpf_copy_from_user_mm_str(dst, dst__sz, unsafe_ptr__ign,
+ mm, flags);
+}
+
/**
* bpf_copy_from_user_task_str() - Copy a string from an task's address space
* @dst: Destination address, in kernel space. This buffer must be
@@ -3682,6 +3782,7 @@ __bpf_kfunc int bpf_copy_from_user_task_str(void *dst, u32 dst__sz,
const void __user *unsafe_ptr__ign,
struct task_struct *tsk, u64 flags)
{
+ struct mm_struct *mm;
int ret;
if (unlikely(flags & ~BPF_F_PAD_ZEROS))
@@ -3690,17 +3791,20 @@ __bpf_kfunc int bpf_copy_from_user_task_str(void *dst, u32 dst__sz,
if (unlikely(dst__sz == 0))
return 0;
- ret = copy_remote_vm_str(tsk, (unsigned long)unsafe_ptr__ign, dst, dst__sz, 0);
- if (ret < 0) {
+ mm = get_task_mm(tsk);
+ if (!mm) {
if (flags & BPF_F_PAD_ZEROS)
memset(dst, 0, dst__sz);
- return ret;
+ else
+ *(char *)dst = '\0';
+ return -EFAULT;
}
- if (flags & BPF_F_PAD_ZEROS)
- memset(dst + ret, 0, dst__sz - ret);
+ ret = __bpf_copy_from_user_mm_str(dst, dst__sz, unsafe_ptr__ign,
+ mm, flags);
+ mmput(mm);
- return ret + 1;
+ return ret;
}
/* Keep unsigned long in prototype so that kfunc is usable when emitted to
@@ -4925,6 +5029,8 @@ BTF_ID_FLAGS(func, bpf_iter_bits_new, KF_ITER_NEW)
BTF_ID_FLAGS(func, bpf_iter_bits_next, KF_ITER_NEXT | KF_RET_NULL)
BTF_ID_FLAGS(func, bpf_iter_bits_destroy, KF_ITER_DESTROY)
BTF_ID_FLAGS(func, bpf_copy_from_user_str, KF_SLEEPABLE)
+BTF_ID_FLAGS(func, bpf_copy_from_user_mm, KF_SLEEPABLE)
+BTF_ID_FLAGS(func, bpf_copy_from_user_mm_str, KF_SLEEPABLE)
BTF_ID_FLAGS(func, bpf_copy_from_user_task_str, KF_SLEEPABLE)
BTF_ID_FLAGS(func, bpf_get_kmem_cache)
BTF_ID_FLAGS(func, bpf_iter_kmem_cache_new, KF_ITER_NEW | KF_SLEEPABLE)
--
2.55.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH bpf-next v8 4/5] bpf: Mark linux_binprm->mm as trusted-or-null
2026-09-18 9:10 [PATCH bpf-next v8 0/5] bpf: Add user memory access kfuncs for mm_struct Anastasios Papagiannis
` (2 preceding siblings ...)
2026-09-18 9:10 ` [PATCH bpf-next v8 3/5] bpf: Add user memory access kfuncs for mm_struct Anastasios Papagiannis
@ 2026-09-18 9:10 ` Anastasios Papagiannis
2026-09-18 9:24 ` sashiko-bot
2026-09-18 9:10 ` [PATCH bpf-next v8 5/5] selftests/bpf: Test mm_struct user memory kfuncs with linux_binprm Anastasios Papagiannis
4 siblings, 1 reply; 12+ messages in thread
From: Anastasios Papagiannis @ 2026-09-18 9:10 UTC (permalink / raw)
To: bpf
Cc: linux-fsdevel, linux-kernel, linux-mm, david, akpm, andrii, ast,
brauner, daniel, eddyz87, kpsingh, ljs, matt, memxor, song,
sun.jian.kdev, utilityemal77, viro, tasos.papagiannnis
Mark linux_binprm->mm as a trusted-or-null nested pointer so BPF programs
can pass it to kfuncs after a NULL check.
The field is either NULL or points to a live mm_struct whenever BPF can
access a linux_binprm. On successful exec, exec_mmap() installs the new
address space before begin_new_exec() clears bprm->mm. The bprm_mm_init()
error path clears the field before mmdrop(), and free_bprm() clears it
before mmput(), as ensured by an earlier patch in this series.
Update the existing LSM selftest to check bprm->mm for NULL before
dereferencing it, as required for trusted-or-null pointers.
Signed-off-by: Anastasios Papagiannis <tasos.papagiannnis@gmail.com>
Reviewed-by: Sun Jian <sun.jian.kdev@gmail.com>
Reviewed-by: Matt Bobrowski <matt@bobrowski.net>
---
kernel/bpf/verifier.c | 5 +++++
tools/testing/selftests/bpf/progs/lsm.c | 5 ++++-
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 6c6b8d8520cd..3539a768b921 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -5997,6 +5997,10 @@ BTF_TYPE_SAFE_TRUSTED_OR_NULL(struct dentry) {
struct inode *d_inode;
};
+BTF_TYPE_SAFE_TRUSTED_OR_NULL(struct linux_binprm) {
+ struct mm_struct *mm;
+};
+
BTF_TYPE_SAFE_TRUSTED_OR_NULL(struct socket) {
struct sock *sk;
};
@@ -6051,6 +6055,7 @@ static bool type_is_trusted_or_null(struct bpf_verifier_env *env,
{
BTF_TYPE_EMIT(BTF_TYPE_SAFE_TRUSTED_OR_NULL(struct socket));
BTF_TYPE_EMIT(BTF_TYPE_SAFE_TRUSTED_OR_NULL(struct dentry));
+ BTF_TYPE_EMIT(BTF_TYPE_SAFE_TRUSTED_OR_NULL(struct linux_binprm));
BTF_TYPE_EMIT(BTF_TYPE_SAFE_TRUSTED_OR_NULL(struct vm_area_struct));
return btf_nested_type_is_trusted(&env->log, reg, field_name, btf_id,
diff --git a/tools/testing/selftests/bpf/progs/lsm.c b/tools/testing/selftests/bpf/progs/lsm.c
index 7de173daf27b..7441d66c080c 100644
--- a/tools/testing/selftests/bpf/progs/lsm.c
+++ b/tools/testing/selftests/bpf/progs/lsm.c
@@ -113,6 +113,7 @@ int BPF_PROG(test_void_hook, struct linux_binprm *bprm)
{
__u32 pid = bpf_get_current_pid_tgid() >> 32;
struct inner_map *inner_map;
+ struct mm_struct *mm;
char args[64];
__u32 key = 0;
__u64 *value;
@@ -121,7 +122,9 @@ int BPF_PROG(test_void_hook, struct linux_binprm *bprm)
bprm_count++;
bpf_copy_from_user(args, sizeof(args), (void *)bprm->vma->vm_mm->arg_start);
- bpf_copy_from_user(args, sizeof(args), (void *)bprm->mm->arg_start);
+ mm = bprm->mm;
+ if (mm)
+ bpf_copy_from_user(args, sizeof(args), (void *)mm->arg_start);
value = bpf_map_lookup_elem(&array, &key);
if (value)
--
2.55.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH bpf-next v8 5/5] selftests/bpf: Test mm_struct user memory kfuncs with linux_binprm
2026-09-18 9:10 [PATCH bpf-next v8 0/5] bpf: Add user memory access kfuncs for mm_struct Anastasios Papagiannis
` (3 preceding siblings ...)
2026-09-18 9:10 ` [PATCH bpf-next v8 4/5] bpf: Mark linux_binprm->mm as trusted-or-null Anastasios Papagiannis
@ 2026-09-18 9:10 ` Anastasios Papagiannis
4 siblings, 0 replies; 12+ messages in thread
From: Anastasios Papagiannis @ 2026-09-18 9:10 UTC (permalink / raw)
To: bpf
Cc: linux-fsdevel, linux-kernel, linux-mm, david, akpm, andrii, ast,
brauner, daniel, eddyz87, kpsingh, ljs, matt, memxor, song,
sun.jian.kdev, utilityemal77, viro, tasos.papagiannnis
Add a sleepable BPF LSM program attached to bprm_check_security to test
bpf_copy_from_user_mm() and bpf_copy_from_user_mm_str().
Starting at bprm->p, verify that bpf_copy_from_user_mm() can copy the
contiguous NUL-separated argument and environment data. Then use
bpf_copy_from_user_mm_str() to read each argument and environment string
separately, advancing the offset by the length returned from each call.
Signed-off-by: Anastasios Papagiannis <tasos.papagiannnis@gmail.com>
---
.../bpf/prog_tests/copy_from_user_bprm.c | 65 +++++++++++++++++
.../selftests/bpf/progs/copy_from_user_bprm.c | 69 +++++++++++++++++++
2 files changed, 134 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/copy_from_user_bprm.c
create mode 100644 tools/testing/selftests/bpf/progs/copy_from_user_bprm.c
diff --git a/tools/testing/selftests/bpf/prog_tests/copy_from_user_bprm.c b/tools/testing/selftests/bpf/prog_tests/copy_from_user_bprm.c
new file mode 100644
index 000000000000..310370689427
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/copy_from_user_bprm.c
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <errno.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <test_progs.h>
+
+#include "copy_from_user_bprm.skel.h"
+
+void test_copy_from_user_bprm(void)
+{
+ char data[] = "first\0second-argument\0SOME_ENV=a\0OTHER_ENV=something";
+ char arg0[] = "first";
+ char arg1[] = "second-argument";
+ char env0[] = "SOME_ENV=a";
+ char env1[] = "OTHER_ENV=something";
+ struct copy_from_user_bprm *skel;
+ pid_t child;
+ int status;
+
+ skel = copy_from_user_bprm__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "open_and_load"))
+ return;
+
+ if (!ASSERT_OK(copy_from_user_bprm__attach(skel), "attach"))
+ goto out;
+
+ child = fork();
+ if (!ASSERT_GE(child, 0, "fork"))
+ goto out;
+
+ if (!child) {
+ char *const argv[] = { arg0, arg1, NULL };
+ char *const envp[] = { env0, env1, NULL };
+
+ skel->bss->monitored_pid = getpid();
+ execvpe("true", argv, envp);
+ _exit(errno);
+ }
+
+ if (!ASSERT_EQ(waitpid(child, &status, 0), child, "waitpid"))
+ goto out;
+
+ if (ASSERT_TRUE(WIFEXITED(status), "child_exited"))
+ ASSERT_EQ(WEXITSTATUS(status), EPERM, "exec_errno");
+
+ ASSERT_EQ(skel->bss->bprm_argc, 2, "bprm_argc");
+ ASSERT_EQ(skel->bss->bprm_envc, 2, "bprm_envc");
+ ASSERT_EQ(skel->bss->data_len, sizeof(data), "data_len");
+ ASSERT_EQ(skel->bss->invalid_flags_ret, -EINVAL, "invalid_flags_ret");
+ ASSERT_EQ(skel->bss->copy_ret, 0, "copy_ret");
+ ASSERT_EQ(skel->bss->arg0_ret, sizeof(arg0), "arg0_ret");
+ ASSERT_EQ(skel->bss->arg1_ret, sizeof(arg1), "arg1_ret");
+ ASSERT_EQ(skel->bss->env0_ret, sizeof(env0), "env0_ret");
+ ASSERT_EQ(skel->bss->env1_ret, sizeof(env1), "env1_ret");
+ ASSERT_EQ(memcmp(skel->bss->data, data, sizeof(data)), 0, "data");
+ ASSERT_EQ(memcmp(skel->bss->arg0, arg0, sizeof(arg0)), 0, "arg0");
+ ASSERT_EQ(memcmp(skel->bss->arg1, arg1, sizeof(arg1)), 0, "arg1");
+ ASSERT_EQ(memcmp(skel->bss->env0, env0, sizeof(env0)), 0, "env0");
+ ASSERT_EQ(memcmp(skel->bss->env1, env1, sizeof(env1)), 0, "env1");
+
+out:
+ copy_from_user_bprm__destroy(skel);
+}
diff --git a/tools/testing/selftests/bpf/progs/copy_from_user_bprm.c b/tools/testing/selftests/bpf/progs/copy_from_user_bprm.c
new file mode 100644
index 000000000000..00ecba3ea567
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/copy_from_user_bprm.c
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include "vmlinux.h"
+
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+#include <errno.h>
+
+char _license[] SEC("license") = "GPL";
+
+int monitored_pid;
+int bprm_argc;
+int bprm_envc;
+int data_len;
+int invalid_flags_ret;
+int copy_ret;
+int arg0_ret;
+int arg1_ret;
+int env0_ret;
+int env1_ret;
+char data[64] = {};
+char arg0[32] = {};
+char arg1[32] = {};
+char env0[32] = {};
+char env1[32] = {};
+
+SEC("lsm.s/bprm_check_security")
+int BPF_PROG(check_exec_args, struct linux_binprm *bprm)
+{
+ u32 pid = bpf_get_current_pid_tgid() >> 32;
+ struct mm_struct *mm;
+ u64 offset = 0;
+
+ if (pid != monitored_pid)
+ return 0;
+
+ mm = bprm->mm;
+ if (!mm)
+ return 0;
+
+ bprm_argc = bprm->argc;
+ bprm_envc = bprm->envc;
+
+ /* this is the total size of args and envs starting from bprm->p */
+ data_len = bprm->exec - bprm->p;
+
+ invalid_flags_ret = bpf_copy_from_user_mm(data, sizeof(data), (void *)bprm->p, mm, ~0ULL);
+
+ copy_ret = bpf_copy_from_user_mm(data, sizeof(data), (void *)bprm->p, mm, 0);
+ if (copy_ret)
+ return 0;
+
+ /* arg0 is at bprm->p */
+ arg0_ret = bpf_copy_from_user_mm_str(arg0, sizeof(arg0), (void *)(bprm->p + offset), mm, BPF_F_PAD_ZEROS);
+ offset += arg0_ret;
+
+ /* arg1 is at bprm->p + arg0_ret */
+ arg1_ret = bpf_copy_from_user_mm_str(arg1, sizeof(arg1), (void *)(bprm->p + offset), mm, BPF_F_PAD_ZEROS);
+ offset += arg1_ret;
+
+ /* env0 is at bprm->p + arg0_ret + arg1_ret */
+ env0_ret = bpf_copy_from_user_mm_str(env0, sizeof(env0), (void *)(bprm->p + offset), mm, BPF_F_PAD_ZEROS);
+ offset += env0_ret;
+
+ /* env1 is at bprm->p + arg0_ret + arg1_ret + env0_ret */
+ env1_ret = bpf_copy_from_user_mm_str(env1, sizeof(env1), (void *)(bprm->p + offset), mm, BPF_F_PAD_ZEROS);
+
+ return -EPERM;
+}
--
2.55.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [PATCH bpf-next v8 4/5] bpf: Mark linux_binprm->mm as trusted-or-null
2026-09-18 9:10 ` [PATCH bpf-next v8 4/5] bpf: Mark linux_binprm->mm as trusted-or-null Anastasios Papagiannis
@ 2026-09-18 9:24 ` sashiko-bot
0 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-09-18 9:24 UTC (permalink / raw)
To: Anastasios Papagiannis; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Critical] bpf: ABI breakage by marking linux_binprm->mm as PTR_MAYBE_NULL
--
commit df643731c6bb225a7d4269f98487ad63ccaf91fc
Author: Anastasios Papagiannis <tasos.papagiannnis@gmail.com>
bpf: Mark linux_binprm->mm as trusted-or-null
This commit marks the linux_binprm->mm field as a trusted-or-null nested
pointer so it can be passed to kfuncs after a NULL check. It updates the
existing LSM selftest to reflect this new verifier requirement.
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 6c6b8d8520cdf..3539a768b9211 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -5997,6 +5997,10 @@ BTF_TYPE_SAFE_TRUSTED_OR_NULL(struct dentry) {
> struct inode *d_inode;
> };
>
> +BTF_TYPE_SAFE_TRUSTED_OR_NULL(struct linux_binprm) {
> + struct mm_struct *mm;
> +};
> +
[Severity: Critical]
Will this cause an ABI regression for existing BPF programs?
Before this patch, the bprm->mm field was treated as an untrusted pointer
(PTR_TO_BTF_ID | PTR_UNTRUSTED). The verifier permits dereferencing untrusted
pointers without a NULL check by safely rewriting these accesses to use
BPF_PROBE_MEM exception tables.
By explicitly adding it to the BTF_TYPE_SAFE_TRUSTED_OR_NULL list, the
verifier now tags it with PTR_MAYBE_NULL. The verifier strictly forbids any
direct dereference on a PTR_MAYBE_NULL pointer before an explicit NULL check.
Does this change mean that previously valid, safe BPF programs will now be
rejected at load time, violating the kernel's UAPI backward compatibility
guarantee?
To avoid this breakage while still allowing kfunc usage, could a kfunc
accessor be exposed (such as bpf_bprm_get_mm()) instead of modifying the BTF
property of the struct field?
> diff --git a/tools/testing/selftests/bpf/progs/lsm.c b/tools/testing/selftests/bpf/progs/lsm.c
> index 7de173daf27b6..7441d66c080c9 100644
> --- a/tools/testing/selftests/bpf/progs/lsm.c
> +++ b/tools/testing/selftests/bpf/progs/lsm.c
[ ... ]
> @@ -121,7 +122,9 @@ int BPF_PROG(test_void_hook, struct linux_binprm *bprm)
> bprm_count++;
>
> bpf_copy_from_user(args, sizeof(args), (void *)bprm->vma->vm_mm->arg_start);
> - bpf_copy_from_user(args, sizeof(args), (void *)bprm->mm->arg_start);
> + mm = bprm->mm;
> + if (mm)
> + bpf_copy_from_user(args, sizeof(args), (void *)mm->arg_start);
[Severity: Critical]
This selftest modification appears to demonstrate the regression in
test_void_hook. Doesn't this prove that existing BPF programs in the wild
that access bprm->mm without a NULL check are no longer accepted by the
verifier?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260918091058.297740-1-tasos.papagiannnis@gmail.com?part=4
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH bpf-next v8 3/5] bpf: Add user memory access kfuncs for mm_struct
2026-09-18 9:10 ` [PATCH bpf-next v8 3/5] bpf: Add user memory access kfuncs for mm_struct Anastasios Papagiannis
@ 2026-09-18 9:30 ` sashiko-bot
2026-09-18 12:44 ` Anastasios Papagiannis
0 siblings, 1 reply; 12+ messages in thread
From: sashiko-bot @ 2026-09-18 9:30 UTC (permalink / raw)
To: Anastasios Papagiannis; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] bpf: UAF in bpf_copy_from_user_mm due to unreferenced mm_struct
--
commit cbb90d2c797aaefaca575d8d6f41d087c38c6414
Author: Anastasios Papagiannis <tasos.papagiannnis@gmail.com>
bpf: Add user memory access kfuncs for mm_struct
This commit adds new bpf_copy_from_user_mm() and bpf_copy_from_user_mm_str()
kfuncs that allow BPF programs to access an address space directly using an
mm_struct pointer. It refactors task-based helpers to share their internal
implementations with the new mm-based kfuncs.
> diff --git a/kernel/bpf/helpers.c b/kernel/bpf/helpers.c
> index 051b6654e57c6..f6b3eee6098a0 100644
> --- a/kernel/bpf/helpers.c
> +++ b/kernel/bpf/helpers.c
[ ... ]
> @@ -3659,6 +3697,68 @@ __bpf_kfunc int bpf_copy_from_user_str(void *dst, u32 dst__sz, const void __user
[ ... ]
> +__bpf_kfunc int bpf_copy_from_user_mm(void *dst, u32 dst__sz,
> + const void __user *unsafe_ptr__ign,
> + struct mm_struct *mm, u64 flags)
> +{
> + if (unlikely(flags))
> + return -EINVAL;
> +
> + if (unlikely(!dst__sz))
> + return 0;
> +
> + return __bpf_copy_from_user_mm(dst, dst__sz, unsafe_ptr__ign, mm);
[Severity: High]
Can this result in a Use-After-Free during concurrent task exit?
When a task exits, mmput() drops mm_users to 0 and calls __mmput(), which
invokes exit_mmap(). exit_mmap() releases the mmap_write_lock and then calls
free_pgtables() without holding any locks.
If a BPF program passes a trusted mm_struct like task->mm to this kfunc, it
eventually calls access_remote_vm(mm) without validating or incrementing
mm_users (for example, by using mmget_not_zero()).
Since access_remote_vm() acquires mmap_read_lock(), if this occurs after
exit_mmap() has dropped the write lock but before it destroys the VMA tree,
vma_lookup() will succeed. This means __get_user_pages() will walk page
tables that are being concurrently freed locklessly by free_pgtables(),
resulting in a Use-After-Free.
Does this need to acquire a reference to mm_users before proceeding?
> +}
[ ... ]
> +__bpf_kfunc int bpf_copy_from_user_mm_str(void *dst, u32 dst__sz,
> + const void __user *unsafe_ptr__ign,
> + struct mm_struct *mm, u64 flags)
> +{
> + if (unlikely(flags & ~BPF_F_PAD_ZEROS))
> + return -EINVAL;
> +
> + if (unlikely(dst__sz == 0))
> + return 0;
> +
> + return __bpf_copy_from_user_mm_str(dst, dst__sz, unsafe_ptr__ign,
> + mm, flags);
[Severity: High]
Similarly, does this also suffer from the same unreferenced mm_struct
lifetime issue described above?
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260918091058.297740-1-tasos.papagiannnis@gmail.com?part=3
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH bpf-next v8 1/5] mm: Add copy_remote_mm_str()
2026-09-18 9:10 ` [PATCH bpf-next v8 1/5] mm: Add copy_remote_mm_str() Anastasios Papagiannis
@ 2026-09-18 10:28 ` bot+bpf-ci
2026-09-18 12:09 ` David Hildenbrand (Arm)
1 sibling, 0 replies; 12+ messages in thread
From: bot+bpf-ci @ 2026-09-18 10:28 UTC (permalink / raw)
To: tasos.papagiannnis, bpf
Cc: linux-fsdevel, linux-kernel, linux-mm, david, akpm, andrii, ast,
brauner, daniel, eddyz87, kpsingh, ljs, matt, memxor, song,
sun.jian.kdev, utilityemal77, viro, tasos.papagiannnis, ast,
andrii, daniel, martin.lau, eddyz87, yonghong.song, mason,
ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 3940 bytes --]
> diff --git a/include/linux/mm.h b/include/linux/mm.h
> index dd09c438fa23e..63f40e615754d 100644
> --- a/include/linux/mm.h
> +++ b/include/linux/mm.h
[ ... ]
> diff --git a/mm/internal.h b/mm/internal.h
> index 38b1165212c94..557b293813557 100644
> --- a/mm/internal.h
> +++ b/mm/internal.h
> @@ -25,6 +25,9 @@
> struct folio_batch;
> struct hstate;
>
> +int __copy_remote_mm_str(struct mm_struct *mm, unsigned long addr,
> + void *buf, int len, unsigned int gup_flags);
> +
__copy_remote_mm_str() goes from 'static' to an mm-wide helper, but its
'len >= 1' precondition is left undocumented and is now enforced in a
different translation unit from the implementations.
Both implementations begin by writing to the buffer unconditionally,
before any length check:
mm/memory.c:__copy_remote_mm_str() {
void *old_buf = buf;
int err = 0;
if (mmap_read_lock_killable(mm))
return -EINTR;
*(char *)buf = '\0';
...
}
mm/nommu.c:__copy_remote_mm_str() {
unsigned long addr_end;
struct vm_area_struct *vma;
int ret = 0;
if (mmap_read_lock_killable(mm))
return -EINTR;
*(char *)buf = '\0';
...
}
Neither ever tests 'len == 0'. The guard lives only in the two wrappers,
in a third file (mm/util.c), so calling __copy_remote_mm_str() with
len == 0 writes one byte past the end of a zero-length buffer.
Before the patch this was structurally impossible: __copy_remote_vm_str()
was 'static' in mm/memory.c and mm/nommu.c and its single caller was the
copy_remote_vm_str() immediately below it in the same file, so the guard
and the unguarded write could not drift apart. Now any file in mm/ that
includes internal.h can call it, and the declaration carries no comment
about the contract.
Should either the 'len must be >= 1' requirement be documented at the
mm/internal.h declaration, or should the 'if (unlikely(len == 0))
return 0;' test be moved into __copy_remote_mm_str() so the guard sits
with the write it protects?
> diff --git a/mm/memory.c b/mm/memory.c
> index 8b0c2c735d3de..fc6933d7e9d36 100644
> --- a/mm/memory.c
> +++ b/mm/memory.c
[ ... ]
> diff --git a/mm/nommu.c b/mm/nommu.c
> index 498e01ee40b05..9a810c35e7e90 100644
> --- a/mm/nommu.c
> +++ b/mm/nommu.c
> @@ -1741,13 +1741,12 @@ int access_process_vm(struct task_struct *tsk, unsigned long addr, void *buf, in
> }
> EXPORT_SYMBOL_GPL(access_process_vm);
>
> -#ifdef CONFIG_BPF_SYSCALL
> /*
> * Copy a string from another process's address space as given in mm.
> * If there is any error return -EFAULT.
> */
> -static int __copy_remote_vm_str(struct mm_struct *mm, unsigned long addr,
> - void *buf, int len)
> +int __copy_remote_mm_str(struct mm_struct *mm, unsigned long addr,
> + void *buf, int len, unsigned int gup_flags)
> {
The NOMMU implementation gains an 'unsigned int gup_flags' parameter that
it never references, and at the same time the patch deletes the only
documentation that said so.
The pre-patch NOMMU kernel-doc read:
* @gup_flags: flags modifying lookup behaviour (unused)
The replacement is a single shared kernel-doc in mm/util.c, compiled for
both MMU and NOMMU:
mm/util.c:copy_remote_mm_str():
* @gup_flags: flags modifying lookup behaviour
mm/util.c:copy_remote_vm_str():
* @gup_flags: flags modifying lookup behaviour
The '(unused)' caveat is gone from both blocks.
The full NOMMU body makes no use of the argument - it goes from
find_vma()/check_add_overflow() to 'ret = strscpy(buf, (char *)addr, len)'
with no reference to gup_flags.
Should the kernel-doc in mm/util.c note that the flags are ignored on
!CONFIG_MMU, or should the '(unused)' comment be restored somewhere in
the NOMMU code itself?
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/35330379263
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH bpf-next v8 1/5] mm: Add copy_remote_mm_str()
2026-09-18 9:10 ` [PATCH bpf-next v8 1/5] mm: Add copy_remote_mm_str() Anastasios Papagiannis
2026-09-18 10:28 ` bot+bpf-ci
@ 2026-09-18 12:09 ` David Hildenbrand (Arm)
2026-09-18 13:27 ` Anastasios Papagiannis
1 sibling, 1 reply; 12+ messages in thread
From: David Hildenbrand (Arm) @ 2026-09-18 12:09 UTC (permalink / raw)
To: Anastasios Papagiannis, bpf
Cc: linux-fsdevel, linux-kernel, linux-mm, akpm, andrii, ast, brauner,
daniel, eddyz87, kpsingh, ljs, matt, memxor, song, sun.jian.kdev,
utilityemal77, viro
On 9/18/26 11:10, Anastasios Papagiannis wrote:
> copy_remote_vm_str() gets the target address space from a struct
> task_struct. This does not work for an address space that exists but is
> not yet associated with a task_struct, such as the mm held by struct
> linux_binprm during exec.
>
> Add copy_remote_mm_str(), which operates directly on a struct mm_struct.
>
> Use a common internal interface for the MMU and NOMMU implementations
> and define both public wrappers in mm/util.c. Preserve the existing
> copy_remote_vm_str() behavior, including handling zero-length requests
> before acquiring the task's mm.
>
> Signed-off-by: Anastasios Papagiannis <tasos.papagiannnis@gmail.com>
> Acked-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
> Acked-by: David Hildenbrand (Arm) <david@kernel.org>
> ---
> include/linux/mm.h | 8 +++---
> mm/internal.h | 3 +++
> mm/memory.c | 43 ++------------------------------
> mm/nommu.c | 43 ++------------------------------
> mm/util.c | 62 ++++++++++++++++++++++++++++++++++++++++++++++
> 5 files changed, 73 insertions(+), 86 deletions(-)
>
> diff --git a/include/linux/mm.h b/include/linux/mm.h
> index dd09c438fa23..63f40e615754 100644
> --- a/include/linux/mm.h
> +++ b/include/linux/mm.h
> @@ -3325,10 +3325,10 @@ extern int access_process_vm(struct task_struct *tsk, unsigned long addr,
> extern int access_remote_vm(struct mm_struct *mm, unsigned long addr,
> void *buf, int len, unsigned int gup_flags);
>
> -#ifdef CONFIG_BPF_SYSCALL
> -extern int copy_remote_vm_str(struct task_struct *tsk, unsigned long addr,
> - void *buf, int len, unsigned int gup_flags);
> -#endif
> +int copy_remote_mm_str(struct mm_struct *mm, unsigned long addr,
> + void *buf, int len, unsigned int gup_flags);
> +int copy_remote_vm_str(struct task_struct *tsk, unsigned long addr,
> + void *buf, int len, unsigned int gup_flags);
>
> long get_user_pages_remote(struct mm_struct *mm,
> unsigned long start, unsigned long nr_pages,
> diff --git a/mm/internal.h b/mm/internal.h
> index 38b1165212c9..557b29381355 100644
> --- a/mm/internal.h
> +++ b/mm/internal.h
> @@ -25,6 +25,9 @@
> struct folio_batch;
> struct hstate;
>
> +int __copy_remote_mm_str(struct mm_struct *mm, unsigned long addr,
> + void *buf, int len, unsigned int gup_flags);
Two tabs please. (I thought I commented on that already)
> +
> struct huge_bootmem_page {
> struct list_head list;
> struct hstate *hstate;
> diff --git a/mm/memory.c b/mm/memory.c
> index 8b0c2c735d3d..fc6933d7e9d3 100644
> --- a/mm/memory.c
> +++ b/mm/memory.c
> @@ -7326,13 +7326,12 @@ int access_process_vm(struct task_struct *tsk, unsigned long addr,
> }
> EXPORT_SYMBOL_GPL(access_process_vm);
>
> -#ifdef CONFIG_BPF_SYSCALL
> /*
> * Copy a string from another process's address space as given in mm.
> * If there is any error return -EFAULT.
> */
> -static int __copy_remote_vm_str(struct mm_struct *mm, unsigned long addr,
> - void *buf, int len, unsigned int gup_flags)
> +int __copy_remote_mm_str(struct mm_struct *mm, unsigned long addr,
> + void *buf, int len, unsigned int gup_flags)
> {
> void *old_buf = buf;
> int err = 0;
> @@ -7408,44 +7407,6 @@ static int __copy_remote_vm_str(struct mm_struct *mm, unsigned long addr,
> return buf - old_buf;
> }
>
> -/**
> - * copy_remote_vm_str - copy a string from another process's address space.
> - * @tsk: the task of the target address space
> - * @addr: start address to read from
> - * @buf: destination buffer
> - * @len: number of bytes to copy
> - * @gup_flags: flags modifying lookup behaviour
> - *
> - * The caller must hold a reference on @mm.
> - *
> - * Return: number of bytes copied from @addr (source) to @buf (destination);
> - * not including the trailing NUL. Always guaranteed to leave NUL-terminated
> - * buffer. On any error, return -EFAULT.
> - */
> -int copy_remote_vm_str(struct task_struct *tsk, unsigned long addr,
> - void *buf, int len, unsigned int gup_flags)
Dito.
> -{
> - struct mm_struct *mm;
> - int ret;
> -
> - if (unlikely(len == 0))
> - return 0;
> -
> - mm = get_task_mm(tsk);
> - if (!mm) {
> - *(char *)buf = '\0';
> - return -EFAULT;
> - }
> -
> - ret = __copy_remote_vm_str(mm, addr, buf, len, gup_flags);
> -
> - mmput(mm);
> -
> - return ret;
> -}
> -EXPORT_SYMBOL_GPL(copy_remote_vm_str);
> -#endif /* CONFIG_BPF_SYSCALL */
> -
> /*
> * Print the name of a VMA.
> */
> diff --git a/mm/nommu.c b/mm/nommu.c
> index 498e01ee40b0..9a810c35e7e9 100644
> --- a/mm/nommu.c
> +++ b/mm/nommu.c
> @@ -1741,13 +1741,12 @@ int access_process_vm(struct task_struct *tsk, unsigned long addr, void *buf, in
> }
> EXPORT_SYMBOL_GPL(access_process_vm);
>
> -#ifdef CONFIG_BPF_SYSCALL
> /*
> * Copy a string from another process's address space as given in mm.
> * If there is any error return -EFAULT.
> */
> -static int __copy_remote_vm_str(struct mm_struct *mm, unsigned long addr,
> - void *buf, int len)
> +int __copy_remote_mm_str(struct mm_struct *mm, unsigned long addr,
> + void *buf, int len, unsigned int gup_flags)
Dito.
I really couldn't care less about the pedantic doc things from the bpf bot.
--
Cheers,
David
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH bpf-next v8 3/5] bpf: Add user memory access kfuncs for mm_struct
2026-09-18 9:30 ` sashiko-bot
@ 2026-09-18 12:44 ` Anastasios Papagiannis
0 siblings, 0 replies; 12+ messages in thread
From: Anastasios Papagiannis @ 2026-09-18 12:44 UTC (permalink / raw)
To: sashiko-bot; +Cc: bpf, sashiko-reviews, tasos.papagiannnis
> [Severity: High]
> Can this result in a Use-After-Free during concurrent task exit?
> When a task exits, mmput() drops mm_users to 0 and calls __mmput(), which
> invokes exit_mmap(). exit_mmap() releases the mmap_write_lock and then calls
> free_pgtables() without holding any locks.
> If a BPF program passes a trusted mm_struct like task->mm to this kfunc, it
> eventually calls access_remote_vm(mm) without validating or incrementing
> mm_users (for example, by using mmget_not_zero()).
> Since access_remote_vm() acquires mmap_read_lock(), if this occurs after
> exit_mmap() has dropped the write lock but before it destroys the VMA tree,
> vma_lookup() will succeed. This means __get_user_pages() will walk page
> tables that are being concurrently freed locklessly by free_pgtables(),
> resulting in a Use-After-Free.
> Does this need to acquire a reference to mm_users before proceeding?
I don't believe that this can happen in the case of lsm.s/bprm_check_security,
where bprm owns the mm throughout the lifetime of that hook.
I also cannot find a case where we can have a trusted pointer to mm_struct
where mm_users can be 0. A candidate could be security_vm_enough_memory_mm(),
but this is not in sleepable_lsm_hooks.
On the other hand, this can change in the future if anyone marks a relevant
pointer as trusted-or-null, similarly to what I did in this series.
I will update both kfuncs to use mmget_not_zero()/mmput() just to be on the
safe side for future uses of these kfuncs.
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH bpf-next v8 1/5] mm: Add copy_remote_mm_str()
2026-09-18 12:09 ` David Hildenbrand (Arm)
@ 2026-09-18 13:27 ` Anastasios Papagiannis
0 siblings, 0 replies; 12+ messages in thread
From: Anastasios Papagiannis @ 2026-09-18 13:27 UTC (permalink / raw)
To: david
Cc: akpm, andrii, ast, bpf, brauner, daniel, eddyz87, kpsingh,
linux-fsdevel, linux-kernel, linux-mm, ljs, matt, memxor, song,
sun.jian.kdev, tasos.papagiannnis, utilityemal77, viro
> Two tabs please. (I thought I commented on that already)
Sorry, I have missed some cases. It shoud be good now in v9 [1].
[1] https://lore.kernel.org/bpf/20260918131757.42802-1-tasos.papagiannnis@gmail.com/
^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-09-18 13:28 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-18 9:10 [PATCH bpf-next v8 0/5] bpf: Add user memory access kfuncs for mm_struct Anastasios Papagiannis
2026-09-18 9:10 ` [PATCH bpf-next v8 1/5] mm: Add copy_remote_mm_str() Anastasios Papagiannis
2026-09-18 10:28 ` bot+bpf-ci
2026-09-18 12:09 ` David Hildenbrand (Arm)
2026-09-18 13:27 ` Anastasios Papagiannis
2026-09-18 9:10 ` [PATCH bpf-next v8 2/5] exec: Clear bprm->mm before dropping its reference Anastasios Papagiannis
2026-09-18 9:10 ` [PATCH bpf-next v8 3/5] bpf: Add user memory access kfuncs for mm_struct Anastasios Papagiannis
2026-09-18 9:30 ` sashiko-bot
2026-09-18 12:44 ` Anastasios Papagiannis
2026-09-18 9:10 ` [PATCH bpf-next v8 4/5] bpf: Mark linux_binprm->mm as trusted-or-null Anastasios Papagiannis
2026-09-18 9:24 ` sashiko-bot
2026-09-18 9:10 ` [PATCH bpf-next v8 5/5] selftests/bpf: Test mm_struct user memory kfuncs with linux_binprm Anastasios Papagiannis
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox