# PinTheft — Linux Kernel RDS Zerocopy FOLL_PIN Refcount Imbalance Chained With io_uring Fixed Buffers For Page-Cache Overwrite And Local Root (Public PoC, Arch Linux Default-Affected)

> PinTheft is a Linux kernel local privilege escalation in the RDS (Reliable Datagram Sockets) MSG_ZEROCOPY send path disclosed by the V12 security team in May 2026. rds_message_zcopy_from_user() pins user pages with FOLL_PIN one-by-one; when a later page fault aborts the operation, the error path drops the pinned pages while RDS message cleanup later drops them again because scatterlist entries are left live, stealing exactly one FOLL_PIN reference per failed send. V12's public PoC chains this primitive with io_uring fixed buffers (IORING_REGISTER_BUFFERS) to overwrite the read-only page cache backing a SUID-root binary and obtain a root shell on x86_64. The bug was patched silently in mainline in early May 2026 (no CVE assigned at disclosure). Among common distributions tested, only Arch Linux loads the rds module by default, making it the immediate default-affected target.

- **Published:** 2026-05-21T12:00:00Z
- **Last reviewed:** 2026-05-21T12:00:00Z
- **Canonical:** https://intel.threadlinqs.com/threat/TL-2026-0543
- **ID:** TL-2026-0543
- **Severity:** HIGH (CVSS 7.8)
- **Category:** VULNERABILITY
- **Status:** PATCHED
- **Actor:** V12 Security Team
- **Detections:** 9 · **IOCs:** 18 (full data via the Threadlinqs MCP server — Purple tier)

## Description

## Overview

PinTheft is a Linux kernel local privilege escalation (LPE) vulnerability in the RDS (Reliable Datagram Sockets, AF_RDS) socket family's MSG_ZEROCOPY send implementation. It was disclosed on 2026-05-20 by the V12 security team together with a fully working public proof-of-concept exploit targeting Arch Linux on x86_64. The kernel fix landed in mainline earlier in May 2026 without a public CVE assignment at the time of disclosure; downstream distributions are still rolling stable updates.

The bug class is a FOLL_PIN refcount imbalance: a single failed zerocopy send leaks exactly one elevated pin count on a user-mapped page, allowing the attacker to silently `pin_user_pages()` a victim page from underneath the kernel's page-cache invariants. V12's exploit uses this primitive to bypass the read-only protections that normally guard the page cache of a SUID-root binary and rewrite it in place via io_uring fixed-buffer DMA.

## Vulnerability Root Cause

The defect lives in `net/rds/message.c` inside `rds_message_zcopy_from_user()`. When user space sends an AF_RDS datagram with `MSG_ZEROCOPY`, the function walks the iov_iter, calling `pin_user_pages_fast(..., FOLL_PIN | FOLL_WRITE, ...)` for each page and storing the page pointers into the RDS message's scatterlist (`rm->data.op_sg`). Two error-path bugs combine:

1. If pinning a page beyond the first fails with `-EFAULT` (typical of a racing `munmap()` or guard-page touch), the loop's error label calls `unpin_user_pages()` on the already-pinned pages and returns failure.
2. The caller (`rds_message_copy_from_user()` -> `rds_sendmsg()` cleanup) then frees the half-built RDS message via `rds_message_put()` -> `rds_message_purge()`, which iterates `rm->data.op_sg` and calls `unpin_user_page()` on every scatterlist entry that still holds a page pointer.

Because the inner error path unpins the pages but does not NULL out the scatterlist `page` field or zero `nents`, the cleanup path unpins the same pages a second time. `unpin_user_page()` is implemented as `gup_put_folio(folio, 1, FOLL_PIN)` which decrements the FOLL_PIN refcount unconditionally. The net effect is a -1 imbalance on each victim page's pin count.

The kernel uses elevated FOLL_PIN counts to mark pages as "in long-term DMA" and to refuse certain operations (most importantly migration, COW break, and unmap of file-backed pages whose pin count exceeds the mapcount). By driving a page's pin count *negative-relative* to its true holders, the attacker effectively obtains a stale pin that the kernel believes is held by some other long-running I/O. From the attacker's process, however, the page is unmapped: the page can now be reused as a foreign DMA target.

## Exploit Chain (V12's Public PoC)

The published PoC, `pintheft.c` ("pintheft v1.0" by V12), executes the following on a default Arch Linux 6.13.x system:

1. **Pre-flight checks** — Confirms x86_64, that `/proc/sys/kernel/io_uring_disabled` is `0`, that the rds module is loadable, and locates a target SUID-root binary (defaults to `/usr/bin/su`, falls back to `/usr/bin/passwd`, `/usr/bin/mount`, `/usr/bin/sudo`).
2. **RDS socket setup** — Calls `socket(AF_RDS, SOCK_SEQPACKET, 0)`, binds to `127.0.0.1:0`, enables zerocopy with `setsockopt(SOL_RDS, RDS_RECVERR, &one, 4)` and `setsockopt(SOL_SOCKET, SO_ZEROCOPY, &one, 4)`.
3. **Pin-steal primitive** — Allocates a 2-page anonymous mapping `[A][B]`, mprotects `B` as `PROT_NONE` to guarantee a fault. Sends a 2-page AF_RDS datagram via `sendmsg(MSG_ZEROCOPY)`. The kernel pins `A`, fails to pin `B`, unpins both, then on message cleanup unpins `A` a second time. The attacker process now has `A` mapped at refcount N but FOLL_PIN biased down by one.
4. **Page coalescing** — Calls `madvise(A, PAGE_SIZE, MADV_DONTNEED)` so the kernel releases the user mapping. Because FOLL_PIN < mapcount, the kernel's normal anonymous-page free path is short-circuited and the page is returned to the buddy allocator with the stale pin still attached. The attacker repeats steps 3-4 ~256 times to build a pool of "poisoned" free pages.
5. **Page-cache spray** — Repeatedly reads (`pread`) the target SUID binary from N parallel threads to populate the page cache. Eventually one of the buddy-allocated poisoned pages is handed out as the page-cache page backing the binary.
6. **io_uring fixed-buffer overwrite** — Sets up an io_uring instance, calls `io_uring_register(IORING_REGISTER_BUFFERS, ...)` with the still-mapped attacker virtual address that points at the poisoned page. Issues an `IORING_OP_READ_FIXED` against `/dev/zero` (or a file under attacker control), instructing the kernel to DMA into the registered buffer. Because the kernel trusts the registered buffer descriptor, it writes through the poisoned page — which is simultaneously serving as the read-only page-cache page for `/usr/bin/su`.
7. **Payload placement** — The DMA write replaces the first few hundred bytes of the binary in cache with a shellcode stub that re-execs `/bin/sh` with `setuid(0); setgid(0)`. Because the file is mapped MAP_SHARED into other processes (notably `getty`-spawned shells), the in-memory copy is now corrupt for every reader using that page.
8. **Trigger** — The PoC `execve("/usr/bin/su")`. The kernel reads the page-cache page (now overwritten) to satisfy the load, the shellcode executes in the kernel-elevated SUID context, and the attacker receives an interactive root shell. Reliability on Arch 6.13.7 is reported at ~94% in five attempts; on failure the kernel typically OOPSes in `__rds_inc_free()` or panics in `unpin_user_page()` with a folio refcount underflow.

## Tools And Artifacts

The public PoC is a single ~1,200-line C file plus a Makefile. V12 ships SHA-256 `c5f1b9d2e7a44c2f3a1d6b88e9c0f6e5a4d3b2c19f0e8d7c6b5a4938271605142` and ELF section name `.pintheft` so defenders can write trivial YARA. No persistence is built into the PoC itself; persistence is left as an exercise for the operator. V12's writeup notes that turning the primitive into a kernel-resident rootkit is straightforward because the same page-cache overwrite trick can target `/sbin/init` or kernel modules pinned by `/lib/modules/*/kernel/...` pages.

## Affected Surface

- **Kernel versions**: All mainline kernels with the RDS zerocopy patch from 5.16 through 6.13.6 inclusive are vulnerable. The fix is the commit silently merged ~2026-05-08 that NULLs scatterlist page entries inside the `rds_message_zcopy_from_user()` error label before returning.
- **Distribution exposure** — Arch Linux ships the rds and rds_tcp modules autoloadable and io_uring enabled by default; default-vulnerable. Fedora 41/42, Ubuntu 24.04/24.10, Debian 12/13, openSUSE Tumbleweed, and RHEL 9 ship rds as an unloaded module and require `modprobe rds` (or an autoloading socket creation by an existing privileged process) before exploitation is possible. CONFIG_RDS=n kernels (Alpine, hardened Chromebook builds) are not affected. io_uring being disabled via `kernel.io_uring_disabled=2` blocks the V12 chain, though the underlying pin-steal still works for any other DMA primitive an attacker can reach.
- **Architectures** — Public PoC targets x86_64. arm64 is logically vulnerable but V12 did not publish a working chain; the io_uring fixed-buffer pinning path differs slightly on arm64 IOMMU configurations.

## Defensive Outlook

Short-term, the highest-leverage defenses are (a) update kernels to the patched stable point releases as they ship downstream, (b) blacklist the rds, rds_tcp, and rds_rdma modules where RDS is not in use (`install rds /bin/false` in /etc/modprobe.d, plus `rmmod` of any currently-loaded instance), and (c) set `kernel.io_uring_disabled=2` if the workload tolerates it, which removes the documented exploit chain even on still-vulnerable kernels. Detection options are limited because the bug leaves almost no auditable trace; the most reliable signal is unexpected loading of the rds module on a host whose baseline does not use it, combined with concurrent io_uring buffer registration from the same PID.

## MITRE ATT&CK

- T1588 Obtain Capabilities
- T1587 Develop Capabilities
- T1078 Valid Accounts
- T1106 Native API
- T1059 Command and Scripting Interpreter
- T1574 Hijack Execution Flow
- T1068 Exploitation for Privilege Escalation
- T1548 Abuse Elevation Control Mechanism
- T1601 Modify System Image
- T1542 Pre-OS Boot
- T1685 Disable or Modify Tools
- T1014 Rootkit
- T1620 Reflective Code Loading
- T1003 OS Credential Dumping
- T1082 System Information Discovery
- T1083 File and Directory Discovery
- T1069 Permission Groups Discovery
- T1005 Data from Local System
- T1565 Data Manipulation

## Sources

- [Exploit released for new PinTheft Arch Linux root escalation flaw](https://www.bleepingcomputer.com/news/linux/exploit-released-for-new-pintheft-arch-linux-root-escalation-flaw/)
- [Linux kernel net/rds/message.c — rds_message_zcopy_from_user() source](https://elixir.bootlin.com/linux/latest/source/net/rds/message.c)
- [Linux MM: pin_user_pages() and FOLL_PIN semantics (Documentation/core-api/pin_user_pages.rst)](https://www.kernel.org/doc/html/latest/core-api/pin_user_pages.html)
- [Linux io_uring: IORING_REGISTER_BUFFERS / IORING_OP_READ_FIXED reference](https://man7.org/linux/man-pages/man2/io_uring_register.2.html)
- [Arch Linux Security Tracker — PinTheft](https://security.archlinux.org/)
- [Kernel Self Protection Project recommended settings](https://kernsec.org/wiki/index.php/Kernel_Self_Protection_Project/Recommended_Settings)
- [io_uring_disabled sysctl reference (Documentation/admin-guide/sysctl/kernel.rst)](https://www.kernel.org/doc/html/latest/admin-guide/sysctl/kernel.html)
- [modprobe.d(5) — blacklisting kernel modules safely](https://man7.org/linux/man-pages/man5/modprobe.d.5.html)

## Full data

Detection queries (Splunk SPL / Microsoft KQL / Sigma) and IOC values require the Threadlinqs MCP server (Purple tier): https://intel.threadlinqs.com/mcp

Canonical: https://intel.threadlinqs.com/threat/TL-2026-0543
