diff options
| author | Christian Brauner <brauner@kernel.org> | 2026-09-10 09:48:17 +0200 |
|---|---|---|
| committer | Christian Brauner <brauner@kernel.org> | 2026-09-10 09:48:17 +0200 |
| commit | b0d13e2be3a7207072154dc0545cfff5dc52d1d2 (patch) | |
| tree | 5301e571b05acb50893fd0302a6225b7540b0114 | |
| parent | cee9395acd8043be0644b25c34bfa86623f2b935 (diff) | |
| parent | 5dac8c291d9b7cdcd433cfcd8300ebf79e4a9177 (diff) | |
| download | linux-next-b0d13e2be3a7207072154dc0545cfff5dc52d1d2.tar.gz linux-next-b0d13e2be3a7207072154dc0545cfff5dc52d1d2.zip | |
Merge patch series "proc,security,selinux: let SELinux block FOLL_FORCE for /proc/self/mem"
Jann Horn <jannh@google.com> says:
The goal of this series is to let SELinux prevent the use of FOLL_FORCE
when a process writes into /proc/self/mem and the system is configured
with PROC_MEM_FORCE_ALWAYS (which used to be the default behavior, and
is still used by current Android devices).
Android has SELinux policy that attempts to ensure that only trusted
code can be mapped as executable in several system processes, but this
protection can currently be bypassed by writing into /proc/self/mem.
I wrote this series after discussion with Android security folks about
the state of proc_mem_foll_force() restrictions on Android.
The motivation for this series is that Project Zero managed to write a
remote exploit for Google Pixel partly because of /proc/self/mem, see
<https://projectzero.google/2026/01/pixel-0-click-part-1.html#whats-the-plan-seth-and-jann>.
* patches from https://patch.msgid.link/20260907-selinux-pokemem-v3-0-0bafbaeafe50@google.com:
selinux: require PROCESS__PTRACE for FOLL_FORCE introspection
proc: query LSMs for introspective mem access (if PROC_MEM_FORCE_ALWAYS)
proc: refactor /proc/$pid/mem to use struct as private_data
Link: https://patch.msgid.link/20260907-selinux-pokemem-v3-0-0bafbaeafe50@google.com
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
| -rw-r--r-- | fs/proc/base.c | 43 | ||||
| -rw-r--r-- | include/linux/lsm_hook_defs.h | 1 | ||||
| -rw-r--r-- | include/linux/security.h | 7 | ||||
| -rw-r--r-- | security/security.c | 25 | ||||
| -rw-r--r-- | security/selinux/hooks.c | 31 |
5 files changed, 102 insertions, 5 deletions
diff --git a/fs/proc/base.c b/fs/proc/base.c index 6a39de424f62..6d369ba2edd0 100644 --- a/fs/proc/base.c +++ b/fs/proc/base.c @@ -848,15 +848,35 @@ static int __mem_open(struct inode *inode, struct file *file, unsigned int mode) return 0; } +/* private_data for proc_mem_operations */ +struct mem_private { + struct mm_struct *mm; + /* + * Was the ptrace access check on open bypassed because the opener used + * the same MM (introspection)? + */ + bool opened_by_owner; +}; + static int mem_open(struct inode *inode, struct file *file) { + struct mem_private *priv __free(kfree) = kmalloc_obj(struct mem_private); + + if (!priv) + return -ENOMEM; if (WARN_ON_ONCE(!(file->f_op->fop_flags & FOP_UNSIGNED_OFFSET))) return -EINVAL; - return __mem_open(inode, file, PTRACE_MODE_ATTACH); + priv->mm = proc_mem_open(inode, PTRACE_MODE_ATTACH); + if (IS_ERR_OR_NULL(priv->mm)) + return priv->mm ? PTR_ERR(priv->mm) : -ESRCH; + priv->opened_by_owner = priv->mm == current->mm; + file->private_data = no_free_ptr(priv); + return 0; } static bool proc_mem_foll_force(struct file *file, struct mm_struct *mm) { + struct mem_private *priv = file->private_data; struct task_struct *task; bool ptrace_active = false; @@ -871,16 +891,20 @@ static bool proc_mem_foll_force(struct file *file, struct mm_struct *mm) READ_ONCE(task->parent) == current; put_task_struct(task); } - return ptrace_active; + if (!ptrace_active) + return false; + break; default: - return true; + break; } + return security_mem_foll_force(file->f_cred, priv->opened_by_owner) == 0; } static ssize_t mem_rw(struct file *file, char __user *buf, size_t count, loff_t *ppos, int write) { - struct mm_struct *mm = file->private_data; + struct mem_private *priv = file->private_data; + struct mm_struct *mm = priv->mm; unsigned long addr = *ppos; ssize_t copied; char *page; @@ -970,12 +994,21 @@ static int mem_release(struct inode *inode, struct file *file) return 0; } +static int mem_release_with_private(struct inode *inode, struct file *file) +{ + struct mem_private *priv = file->private_data; + + mmdrop(priv->mm); + kfree(priv); + return 0; +} + static const struct file_operations proc_mem_operations = { .llseek = mem_lseek, .read = mem_read, .write = mem_write, .open = mem_open, - .release = mem_release, + .release = mem_release_with_private, .fop_flags = FOP_UNSIGNED_OFFSET, }; diff --git a/include/linux/lsm_hook_defs.h b/include/linux/lsm_hook_defs.h index 65c9609ec207..12f84a1e6fab 100644 --- a/include/linux/lsm_hook_defs.h +++ b/include/linux/lsm_hook_defs.h @@ -36,6 +36,7 @@ LSM_HOOK(int, 0, binder_transfer_file, const struct cred *from, LSM_HOOK(int, 0, ptrace_access_check, struct task_struct *child, unsigned int mode) LSM_HOOK(int, 0, ptrace_traceme, struct task_struct *parent) +LSM_HOOK(int, 0, mem_foll_force, const struct cred *subject, bool opened_by_owner) LSM_HOOK(int, 0, capget, const struct task_struct *target, kernel_cap_t *effective, kernel_cap_t *inheritable, kernel_cap_t *permitted) LSM_HOOK(int, 0, capset, struct cred *new, const struct cred *old, diff --git a/include/linux/security.h b/include/linux/security.h index 153e9043058f..e8bc2e644241 100644 --- a/include/linux/security.h +++ b/include/linux/security.h @@ -338,6 +338,7 @@ int security_binder_transfer_file(const struct cred *from, const struct cred *to, const struct file *file); int security_ptrace_access_check(struct task_struct *child, unsigned int mode); int security_ptrace_traceme(struct task_struct *parent); +int security_mem_foll_force(const struct cred *subject, bool opened_by_owner); int security_capget(const struct task_struct *target, kernel_cap_t *effective, kernel_cap_t *inheritable, @@ -676,6 +677,12 @@ static inline int security_ptrace_traceme(struct task_struct *parent) return cap_ptrace_traceme(parent); } +static inline int security_mem_foll_force(const struct cred *subject, + bool opened_by_owner) +{ + return 0; +} + static inline int security_capget(const struct task_struct *target, kernel_cap_t *effective, kernel_cap_t *inheritable, diff --git a/security/security.c b/security/security.c index 2ee276ab15c5..4892153842d4 100644 --- a/security/security.c +++ b/security/security.c @@ -596,6 +596,31 @@ int security_ptrace_traceme(struct task_struct *parent) } /** + * security_mem_foll_force() - Check if FOLL_FORCE is allowed + * @subject: credentials using which /proc/$pid/mem was opened + * @opened_by_owner: whether checks on open() were bypassed because the opener + * has the same MM as the target + * + * Check if FOLL_FORCE is allowed for accessing process memory through + * /proc/$pid/mem. opened_by_owner signals whether the opener's MM was the same + * as the target MM, meaning the security_ptrace_access_check() hook was + * bypassed on open(). + * (Current current->mm does not matter for this; for example, if write() is + * called on an FD that was received from another process which obtained it with + * open("/proc/self/mem"), @opened_by_owner is still true.) + * + * Note that this hook is only designed to be useful in the opened_by_owner + * case, where the subject credentials effectively also describe the object. + * + * Return: Returns 0 if permission is granted. + */ +int security_mem_foll_force(const struct cred *subject, + bool opened_by_owner) +{ + return call_int_hook(mem_foll_force, subject, opened_by_owner); +} + +/** * security_capget() - Get the capability sets for a process * @target: target process * @effective: effective capability set diff --git a/security/selinux/hooks.c b/security/selinux/hooks.c index 035aaf113d1d..2c0b62ff9fc4 100644 --- a/security/selinux/hooks.c +++ b/security/selinux/hooks.c @@ -2159,6 +2159,36 @@ static int selinux_ptrace_traceme(struct task_struct *parent) SECCLASS_PROCESS, PROCESS__PTRACE, NULL); } +/** + * selinux_mem_foll_force() - Determine whether /proc/$pid/mem can use FOLL_FORCE + * @subject: credentials using which /proc/$pid/mem was opened + * @opened_by_owner: whether checks on open() were bypassed because the opener + * has the same MM as the target + * + * Decide whether it should be possible to read non-readable VMAs and write + * non-writable VMAs via /proc/self/mem. + * The @opened_by_owner case only applies to systems configured with + * PROC_MEM_FORCE_ALWAYS, and only happens on accesses that are not visible to + * selinux_ptrace_access_check() because of the introspection exceptions in + * may_access_mm() and __ptrace_may_access(). + * + * This allows a process to overwrite read-only code in its own address space. + * + * Creating an audit record on denial doesn't make sense here, since we can't + * tell whether FOLL_FORCE matters for the accessed VMAs. + */ +static int selinux_mem_foll_force(const struct cred *subject, bool opened_by_owner) +{ + struct av_decision avd; + u32 sid; + + if (!opened_by_owner) + return 0; + sid = cred_sid(subject); + + return avc_has_perm_noaudit(sid, sid, SECCLASS_PROCESS, PROCESS__PTRACE, 0, &avd); +} + static int selinux_capget(const struct task_struct *target, kernel_cap_t *effective, kernel_cap_t *inheritable, kernel_cap_t *permitted) { @@ -7565,6 +7595,7 @@ static struct security_hook_list selinux_hooks[] __ro_after_init = { LSM_HOOK_INIT(ptrace_access_check, selinux_ptrace_access_check), LSM_HOOK_INIT(ptrace_traceme, selinux_ptrace_traceme), + LSM_HOOK_INIT(mem_foll_force, selinux_mem_foll_force), LSM_HOOK_INIT(capget, selinux_capget), LSM_HOOK_INIT(capset, selinux_capset), LSM_HOOK_INIT(capable, selinux_capable), |
