* double mmput in do_procmap_query() destroys address space of live process
@ 2026-02-10 15:09 Ruikai Peng
0 siblings, 0 replies; only message in thread
From: Ruikai Peng @ 2026-02-10 15:09 UTC (permalink / raw)
To: akpm, andrii; +Cc: linux-mm
We're reporting a double mmput bug in do_procmap_query(). When a
PROCMAP_QUERY ioctl requests a build ID and the user-supplied buffer
is too small to hold it, the function jumps to the out: cleanup label
after it has already called query_vma_teardown() + mmput(). The out:
label calls them a second time. The extra mmput drops mm_users to
zero, triggering __mmput which calls exit_mmap, destroying the calling
process's VMAs and page tables while it is still running. The process
crashes with SIGSEGV on return to userspace.
Any unprivileged user can trigger this with a single ioctl. With
access to /proc/<pid>/maps of another process (same UID or
CAP_SYS_PTRACE), an attacker can destroy the target's address space
while the attacker's own process survives.
AFFECTED VERSIONS
Regression since b5cbacd7f86f ("procfs: avoid fetching build ID while
holding VMA lock"), first appeared after v6.19-rc6. Not affected:
v6.18 and earlier. The original build-id code (bfc69fd05ef9, merged in
v6.11) parsed the build ID before teardown+mmput, so the goto out was
safe.
ROOT CAUSE
do_procmap_query() acquires one mm_users reference at entry via
mmget_not_zero(mm) but releases it twice on the build-id-too-large
error path. The function has two phases:
Phase 1 (VMA locked): query VMA fields, grab file reference
Phase 2 (VMA unlocked): parse build-id from the file, copy to user
The transition between phases is at lines 768-769:
query_vma_teardown(&lock_ctx); /* unlock VMA */
mmput(mm); /* release mm_users: 2 -> 1 */
After this, if build_id_parse_file() succeeds but the build ID is
larger than the user buffer, line 783 does "goto out". The out: label
at line 808-810 runs:
query_vma_teardown(&lock_ctx); /* no-op, already torn down */
mmput(mm); /* mm_users: 1 -> 0 -- BUG! */
This second mmput drops mm_users to zero and triggers __mmput ->
exit_mmap, which destroys all VMAs and frees all page tables. The
function acquired one mm_users reference but released two.
The refcount trace for a forked child:
fork: mm_users=1 mm_count=1
open /proc: mmgrab(+1) -> mm_users=1 mm_count=2
ioctl entry: mmget_not_zero(+1) -> mm_users=2 mm_count=2
1st mmput ln 769: mmput(-1) -> mm_users=1 mm_count=2
2nd mmput ln 810: mmput(-1) -> __mmput! -> mm_users=0 mm_count=2
exit_mmap: destroys VMAs + page tables
mmdrop(-1): -> mm_users=0 mm_count=1
return to user: page fault -> SIGSEGV (page tables gone)
IMPACT
The process's address space is destroyed while it is still running.
Returning to userspace causes a page fault (page tables are gone) and
the kernel delivers SIGSEGV.
- Self-targeting: any unprivileged user can crash their own process.
- Cross-process: an attacker who can open /proc/<pid>/maps of a target
(same UID, or CAP_SYS_PTRACE) can issue the ioctl against the target's
mm. The double mmput destroys the target's address space while the
attacker's process is unaffected. This is a targeted process kill that
is more disruptive than SIGKILL because it destroys the address space
mid-execution, potentially corrupting in-progress writes to files or
shared memory.
We did not find a practical path to escalation. The mm_struct itself
is not freed because mm_count remains positive (the proc fd holds a
structural reference via mmgrab in proc_mem_open). The destroyed VMAs
are cleanly removed from the maple tree before being freed, so
find_vma returns NULL rather than a dangling pointer. The process
crashes before any memory reuse window opens.
REPRODUCTION
The bug triggers when all of the following hold:
1. The queried VMA maps a file with an ELF .note.gnu.build-id section
2. build_id_size in the ioctl argument is nonzero but smaller than
the actual build ID (e.g., 1 byte vs 20 for SHA1)
3. build_id_parse_file() succeeds (i.e., it can read the file)
Trigger from userspace (the binary must be linked with a build ID,
which is the default for gcc/ld):
int fd = open("/proc/self/maps", O_RDONLY);
struct procmap_query q = {};
q.size = sizeof(q);
q.query_flags = 0x10 | 0x20; /* COVERING_OR_NEXT | FILE_BACKED */
q.query_addr = <address of any file-backed VMA>;
q.build_id_size = 1;
q.build_id_addr = (uint64_t)buf;
ioctl(fd, PROCMAP_QUERY, &q); /* triggers double mmput */
/* process crashes with SIGSEGV here */
For a complete PoC that forks a child (so the parent can report the
crash), build with:
gcc -static -O2 -o poc poc.c
The PoC parses /proc/self/maps to find a file-backed VMA, forks a
child, and has the child issue the ioctl. The child is killed by
SIGSEGV every time. Kernel log shows:
poc[65]: segfault at 42a78d ip 000000000042a78d sp 00007ffe5c090c10 error 14
Running 50 iterations produces 50 crashes.
TEST ENVIRONMENT
Kernel: v6.19-rc8 (commit e7aa57247700) including b5cbacd7f86f
Config: x86_64, PREEMPT_VOLUNTARY
QEMU: qemu-system-x86_64 -smp 2 -m 4096 -nographic
Best,
- Ruikai Peng
^ permalink raw reply [flat|nested] only message in thread
only message in thread, other threads:[~2026-02-10 15:09 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-02-10 15:09 double mmput in do_procmap_query() destroys address space of live process Ruikai Peng
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox