Minimal Linux kernel + QEMU environment for reproducing kernel vulnerability PoCs. Supports per-PoC manifests and kernel configuration so each exploit runs in the exact kernel environment it needs without polluting shared defaults.
# Prerequisites (one-time)
brew install qemu
# Docker Desktop must be installed and running
# From a fresh clone - list PoCs, then run the smoke test
./pocctl list
./pocctl run smokeThe bundled smoke manifest currently pins target.arch: arm64, so x86-64
hosts should override the arch on the command line.
# macOS prerequisites (one-time)
brew install qemu
# Docker Desktop must be installed and running
# Linux dependencies (one-time)
sudo apt install build-essential gcc flex bison bc cpio wget xz-utils \
libelf-dev libssl-dev qemu-system-x86 musl-tools gdb
# From a fresh clone
./pocctl list
./pocctl run smoke ARCH=x86_64pocctl run <id> reads pocs/<id>/poc.yaml, builds the Docker
cross-compilation image (first run only on macOS), downloads Linux and BusyBox
sources, compiles the kernel, builds the initramfs, compiles the PoC, injects
it into the rootfs, and launches QEMU.
Use ./pocctl list to print the PoCs discovered from pocs/*/poc.yaml.
| ID | Source | Description |
|---|---|---|
smoke |
pocs/smoke/poc.c |
Environment smoke test - run first to validate the full pipeline. Checks kernel symbols, BPF, namespaces, debugfs, perf settings. |
copyfail |
pocs/copyfail/poc.c |
CVE-2026-31431 - page-cache write without write permission via authencesn AEAD ESN out-of-bounds write. |
template |
pocs/template/poc.c |
Skeleton - copy this to start a new PoC. |
kernel-poc/
├── Makefile # low-level build entry point
├── pocctl # PoC controller: list/run/debug/clean by id
├── build/
│ ├── Dockerfile # cross-compilation toolchain image (macOS)
│ ├── config/
│ │ ├── kernel-common.config # debug symbols, KASAN, BPF, namespaces, ...
│ │ ├── kernel-arm64.config # PL011 UART, GIC, PSCI, ...
│ │ └── kernel-x86_64.config # 8250 UART, ...
│ ├── rootfs/
│ │ └── etc/init.d/rcS # init: mounts fs, runs /root/poc, drops shell
│ └── scripts/
│ ├── build_kernel.sh # fetch + configure + build Linux kernel
│ ├── build_rootfs.sh # build static busybox initramfs
│ ├── pack_poc.sh # compile PoC, inject into rootfs, repack
│ ├── run.sh # launch QEMU
│ └── check_deps.sh # dependency checker
├── pocs/
│ ├── smoke/
│ │ ├── poc.yaml # metadata + runtime/kernel defaults
│ │ └── poc.c # end-to-end smoke test (start here)
│ ├── template/
│ │ ├── poc.yaml # copy and edit for a new PoC
│ │ └── poc.c # PoC skeleton
│ └── copyfail/
│ ├── poc.yaml # id, kernel version, source, runtime knobs
│ ├── poc.c # CVE-2026-31431
│ └── kernel.config # AF_ALG AEAD options required by this PoC
├── src/ # downloaded kernel / busybox sources
└── out/ # build artifacts
└── <arch>/
├── Image / bzImage # kernel image passed to QEMU
├── vmlinux # unstripped ELF for GDB
└── rootfs.img # initramfs cpio with PoC injected
Each runnable PoC lives under pocs/<id>/ and is described by
pocs/<id>/poc.yaml. The directory name is the id passed to pocctl.
Important fields:
| Field | Meaning |
|---|---|
metadata.id / metadata.name |
Display metadata for ./pocctl list; keep metadata.id equal to the directory name |
target.type |
Phase 1 supports linux-kernel |
target.arch |
Optional target arch: arm64 or x86_64 |
target.kernel |
Kernel version to build |
exploit.source |
PoC C source path, relative to poc.yaml |
runtime.user |
Optional non-root user for LPE PoCs |
runtime.kaslr/smep/smap |
Security mitigation defaults |
Example:
metadata:
id: smoke
name: Smoke Test
target:
type: linux-kernel
arch: arm64
kernel: "6.1.14"
exploit:
source: poc.c
runtime:
kaslr: "false"
smep: "true"
smap: "true"Each PoC can ship its own kernel.config fragment alongside poc.c. When you
run ./pocctl run <id>, pocctl reads pocs/<id>/poc.yaml, forwards the PoC
source and runtime settings to make, and the build system:
- Merges
build/config/kernel-common.config - Merges
build/config/kernel-<arch>.config - Merges
pocs/<id>/kernel.config(if it exists) - Runs
olddefconfigto resolve any conflicts - Rebuilds the kernel incrementally (only changed modules recompile)
This keeps each PoC's requirements isolated.
Example (pocs/copyfail/kernel.config):
CONFIG_CRYPTO_USER_API_AEAD=y
CONFIG_CRYPTO_AUTHENC=y
# 1. Copy the template directory
cp -r pocs/template pocs/my-cve
# 2. Edit the PoC source
# Fill in exploit() in pocs/my-cve/poc.c
# 3. Edit pocs/my-cve/poc.yaml
# Set metadata.id/name, target.kernel, exploit.source, and runtime knobs.
# Keep metadata.id equal to the directory name used by pocctl.
# 4. Optional: add PoC-specific kernel requirements
cat > pocs/my-cve/kernel.config <<'EOF'
CONFIG_SOME_SUBSYSTEM=y
EOF
# 5. Run from the repo root
./pocctl list
./pocctl run my-cvepocctl accepts extra KEY=VALUE pairs and forwards them to the root
Makefile, so you can override manifest defaults without editing poc.yaml:
./pocctl run my-cve SMEP=n SMAP=n # disable x86_64 mitigations
./pocctl run my-cve KASLR=y # enable KASLR
./pocctl debug my-cve # launch with GDB serverThe PoC binary is compiled statically, placed at /root/poc in the initramfs,
and executed automatically on boot. A root shell follows for interactive
exploration.
| Command | Description |
|---|---|
./pocctl list |
List PoCs discovered from pocs/*/poc.yaml |
./pocctl run <id> [K=V ...] |
Build kernel + rootfs + PoC, inject it, and launch QEMU |
./pocctl shielded <id> [K=V ...] |
Run the PoC with AutoShield artifacts injected into the guest |
./pocctl debug <id> [K=V ...] |
Same as run, but starts QEMU with a GDB server on :1234 |
./pocctl clean [<id>] |
Remove build artifacts for all, or for the PoC's configured arch |
./pocctl help |
Show command help |
make remains the lower-level implementation interface:
| Target | Description |
|---|---|
make poc POC=<path> |
Build kernel + rootfs + PoC, then launch QEMU |
make poc-shielded POC=<path> SHIELD_MODE=kernel|frida AUTOSHIELD_DIR=../AutoShield |
Export AutoShield artifacts from a local private checkout, inject them into rootfs, then launch QEMU |
make kernel |
Build kernel only |
make rootfs |
Build busybox initramfs only |
make run |
Launch QEMU (plain shell, no PoC) |
make debug |
Launch QEMU with GDB server on :1234 |
make docker-image |
Build the cross-compilation Docker image (macOS, explicit) |
make setup |
Check host dependencies |
make clean |
Remove built images for current ARCH |
make distclean |
Remove all sources and built artifacts |
PoCLab does not vendor or require AutoShield. The public baseline path remains:
./pocctl run smoke
make poc POC=pocs/smoke/poc.cWhen a local private AutoShield checkout is available, use the shielded path:
./pocctl shielded smoke SHIELD_MODE=kernel AUTOSHIELD_DIR=../AutoShield
./pocctl shielded smoke SHIELD_MODE=frida AUTOSHIELD_DIR=../AutoShieldThe integration contract is artifact-based:
- PoCLab calls
make -C "$AUTOSHIELD_DIR" export SHIELD_MODE=<kernel|frida> OUT=<PoCLab/out/.../autoshield>. - PoCLab copies the exported artifacts into
/root/autoshieldinside the initramfs. /etc/init.d/rcSloads kernel modules before/root/poc, or runs/root/pocthrough the exported Frida wrapper.
Kernel mode expects at least one *.ko in the exported kernel/ artifacts. Frida mode expects run-frida-hook.sh or agent.js in the exported frida/ artifacts.
# ARCH is read from poc.yaml when present, otherwise auto-detected.
# CLI KEY=VALUE overrides take precedence.
./pocctl run smoke ARCH=x86_64 # force x86_64
./pocctl run smoke ARCH=arm64 # force arm64
# Use a different kernel version
./pocctl run smoke KERNEL_VERSION=5.15.0# Disable SMEP/SMAP for easier exploitation (x86_64 only)
./pocctl run my-cve SMEP=n SMAP=n
# Enable KASLR for realistic testing (off by default)
./pocctl run my-cve KASLR=yarm64 always has PAN (similar to SMAP) and PXN (similar to SMEP) enabled at the hardware level.
# Terminal 1: launch QEMU paused, waiting for GDB
./pocctl debug smoke
# Terminal 2: attach
gdb out/arm64/vmlinux
(gdb) target remote :1234
(gdb) break start_kernel
(gdb) continue