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) — Threadlinqs Intelligence
As of 2026-05-30, 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) is a high-severity vulnerability threat attributed to V12 Security Team, tracked by Threadlinqs Intelligence with 9 detection rules (Splunk SPL, Microsoft KQL, Sigma) and 18 indicators of compromise.
Threat ID: TL-2026-0543 · Severity: HIGH · CVSS: 7.8 · Status: PATCHED · Category: VULNERABILITY
Attribution: V12 Security Team · UNKNOWN
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
## 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 sti
Weaknesses (CWE)
CWE-415, CWE-416, CWE-672, CWE-911, CWE-269
Target sectors: technology, government, research, education, developer-workstations, cloud-hosting, any-arch-linux-host
Target regions: Global
Detections & IOCs
As of 2026-07-28, this threat has 9 detection rule(s) across Splunk SPL, Microsoft KQL and Sigma, and 18 indicator(s) of compromise. Detection query text and full IOC values are available to authenticated users and programmatically via the Threadlinqs MCP server (Purple tier). View plans.
VULNERABILITY, HIGH, threat intelligence, cybersecurity, T1588, T1587, T1078, T1106, T1059, T1574, T1068, T1548, T1601, T1542