linux-mm.kvack.org archive mirror
 help / color / mirror / Atom feed
* 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