USENIX OSDI 2026

UCSan

A Compilation-based Under-Constrained Execution Engine
Mingjun Yin · Zhaorui Li · Ju Chen · Haochen Zeng · Chengyu Song
University of California, Riverside
UCSan · USENIX OSDI 2026
Motivation

Finding bugs in large systems is hard

Linux kernel — thousands of components Dynamic testing fuzzer / concolic engine — precise harness each needs a harness + VM / disk / device → deep code & drivers stay out of reach Static analysis covers everything… UBITect: 147,643 warnings ✗ only 52 ever confirmed ✓

Neither scales precise analysis to the individual components of a large system.

UCSan · USENIX OSDI 20262
The gap & our idea

Test components in isolation — at native speed

The dream: extract any subset of code, make it self-contained, analyze it precisely

Under-constrained execution (UC-KLEE) automates this via lazy initialization — allocate & initialize memory objects on first dereference. (Manual extraction — TCP stack, LKL — is labor-intensive and unmaintainable.)

The gap: every existing UC engine is an interpreter — watch them race

Native
finished ✓
KLEE
≈ 3,000× slower…
Angr
≈ 321,000× ☠

Same workload — interpreters re-decode every instruction. File systems, the TCP stack, and drivers are out of reach.

Our idea — UCSan: under-constrained execution as a compiler pass

  • An LLVM pass + runtime library: compiles any set of C/C++ functions into a self-contained executable → native speed
  • Decoupled from the dynamic analysis — combine with concolic execution, fuzzing, or model checking
UCSan · USENIX OSDI 20263
The running example

Running example: linked-list sum

cal(list_t* head) deep inside the kernel if (sum>100) assert(false) goal: reach this path fuzzer input no main() — needs a harness head = ??? ? what object? how big? what values?

Goal: analyze cal() in isolation — cover every path, including the assert(false) on line 12.

No tool can start here. Fuzzer input bounces off: there is no main(), and nobody built a list in memory.

head dangles into nothing. UCSan must answer on the fly: what does it point to · how big is it · what's inside?

UCSan · USENIX OSDI 20264
Compile-time transformation

What UCSan compiles in

Generated entry point — main()

A shadow pointer per pointer argument, deserialize the seed into head, then call cal() → a self-contained binary.

Pointer translation — check_ptr()

Every memory access is translated before use. Aliased pointers (curr, curr->v, head->next) share one shadow pointer — like a segment register. Translated pointers are ephemeral: the object may be reallocated or resized later.

External functions

Map to a wrapper (kmalloc → malloc) · assume pure · or havoc pointer args. (None in this example.)

All on LLVM IR — zero source changes; any code that emits IR works.

UCSan · USENIX OSDI 20265
Two ideas — thirty seconds

Just-In-Time Initialization (JITI) & pseudo- vs. real pointers

what the program sees — pseudo-pointers head = 0x1 curr = -3 logical values — point at nothing real shadow pointer head_s ↦ ∅ (no object yet) ↦ obj1 · base = −3 knows the real backing object; shared by all aliases — works like a segment register memory: not allocated v next Just-In-Time Initialization (JITI) allocated on the fly · phantom · zero-init obj1 : node_t v0 next0 container_of: 0x1 − sizeof(int) = −3 pointer arithmetic stays pseudo — no translation; the cast reveals the real type: node_t &curr->v (−3) — first dereference JITI: malloc obj1 (node_t) · base := −3 −3 ↦ obj1->v  (segment offset 0) p_next = &obj1 + (0x1 − (−3)) = &obj1->next real pointer = object_base + (current_offset − base_offset) — one access, then discarded

Watch for these in the next three runs: every pointer the program touches is a pseudo-pointer · check_ptr turns it into a real pointer just in time · JITI materializes memory the first time it's needed.

⌗ hover — peek at the instrumented code
UCSan · USENIX OSDI 20266
Plug in an analysis

Exploring paths needs a solver — so plug one in

The binary runs — but exploring paths needs new seeds. UCSan is decoupled, so that’s just composition.

UCSan UC execution at native speed + SymSan concolic tracing + SMT solving + Thoroupy exploration & seed orchestration UCSan† run binary path constraints SMT solve new seed

= UCSan — a complete under-constrained concolic engine; this loop drives the exploration you’re about to watch.

UCSan · USENIX OSDI 20267
Concolic exploration ① ② ③

Seed 0 — start with an empty seed

phantom · zero-init (JITI) Super Object (SO) head 0x0 never allocated obj1 : node_t v next Seed 0 = ∅ (contains nothing at all) head = 0x0 sum = 0 → no assert

get_concrete(head): not in seed → phantom object, zero-filled → head = 0x0

while(head): 0 → loop skipped, sum = 0 → assert NOT triggered

Solver negates head ≠ null → assign head = 0x1 (a pseudo-pointer — any non-null value works) ⟹ Seed 1

UCSan · USENIX OSDI 20268
Concolic exploration ④ ⑤ ⑥ ⑦

Seed 1 — JIT-initialize on first access

Super Object (SO) head 0x1 Seed 1 = { SO: head = 0x1 } sum = 0 0 + obj1.v ← symbolic! phantom · zero-init (JITI) obj1 : node_t v 0 sym next 0x0 0x1 → obj1.list.next (off 0x4) container_of: v via base −3

check_ptrJITI allocates obj1 (phantom): size from container_ofnode_t, not list_t · pointed-by SO@0 recorded

pseudo-pointer head->next (0x1) → real obj1.list.next; shadow ptr stores logical base −3 (1−4), so any pointer arithmetic keeps working

obj1.next = 0 → loop exits · sum = 0+obj1.v is symbolic → solve sum>100 ∧ obj1.next≠0Seed 2

UCSan · USENIX OSDI 20269
Concolic exploration ⑧ ⑨ ⑩

Seed 2 — replay objects & trigger the bug

SO head 0x1 Seed 2 = { SO: head=0x1 · obj1: v=101, next=0x1 } sum = 101 💥 assert(false)! 101 > 100 — bug reached loaded from seed obj1 : node_t v101 next0x1 phantom · zero-init (JITI) obj2 : node_t v0 next0x0 point-to (obj1 off 4)

obj1 loaded from seed (via pointed-by recorded at ⑤) → obj1.next = 0x1 → re-enter loop

check_ptr → JITI obj2 (phantom); record point-to (obj1 off 4 → obj2); obj2.next = 0 ends the loop

sum = 101 → assert(false) FIRES ✓   all paths covered — no harness, no manual setup

UCSan · USENIX OSDI 202610
Generalize

Everything you saw is automatic — and analysis-agnostic

LLVM IRany front end — e.g. kernel LTO build ; cal.bcdefine i32 @cal(ptr %head)
YAML configentry · scope · external fns entry:  calscope:  [foo, bar]kmalloc: → malloc
UCSan pass instrument memory accesses · handle external calls · drop out-of-scope code *p  ⇒ *check_ptr(p, p_s)kmalloc() ⇒ malloc()✂ out-of-scope functions
analysis pass (optional)pipelined on the same IR SymSan: sum>100 ⇒ SMTor fuzzer · model checker
self-contained executableUCSan + PUT + analysis runtimes; a normal user-space program $ ./cal_uc seed2assert(false) hit!

Config is the harness-killer: name any function as entry, any set of functions as scope. Write it by hand — or generate it with static analysis or an LLM.

Output runs anywhere: no VM, no disk image, no device. Kernel code becomes a normal user-space process.

UCSan · USENIX OSDI 202611
Performance

RQ1: Native-speed execution

Linked-list micro-benchmark (20 nodes)

UCSan
9s
KLEE
20s
Angr
79s

Takeaways

  • UCSan beats KLEE by orders of magnitude on every nbench test
  • Angr: no single iteration in 18h — crashed (OOM)
  • UCSan's extra cost is just SymSan's symbolic tracing
nbench — slowdown vs native execution (log scale, lower is better)
UCSan (≈ 2–33×) UCSan (≈ 2–56×) KLEE (≈ 1,700–50,600×)
UCSan · USENIX OSDI 202612
Compatibility

RQ2: Compiles real kernel code

Compiled three Linux kernel versions: 4.14 · 5.10 (Android LTS) · 6.16 (latest stable)

Analysis scopes generated automatically: function pointers in static initializers as entries + call-graph analysis.

kernel modules
>80%
compile successfully
v5.10 · 15k scopes
96.2%
analysis scopes compiled
v6.16 · 157k scopes
88.9%
analysis scopes compiled

Remaining failures: unhandled inline assembly — the same limitation KLEE has. Unlike KLEE, UCSan offers a path forward: complex blocks can be modeled with a custom wrapper function (the same mechanism as external functions; LLMs can help generate them).

UCSan · USENIX OSDI 202613
Impact · UBITect

RQ3: Filtering static-analysis warnings

Task

Confirm/refute UBITect's use-before-init warnings — 2-minute, 2 GB budget per warning. UCSan vs the KLEE-based engine built for this exact task (IncreLux).

Warnings finished within the 2-min budget

UCSan
95.46% (63,957)
KLEE
41.29%

6.36× faster average time-to-finish · 15.06× faster per warning (median 4.62s vs timeout)

Time-to-finish (2-min timeout)

25th% Median 75th% Mean
UCSan 3.98s 4.62s 6.21s 14.37s
KLEE 92.13s T.O. T.O. 105.71s

That speed buys results

  • 111 more warnings confirmed than the KLEE-based engine
  • ≥ 15 real UBI bugs, later patched upstream — could have been caught years earlier
UCSan · USENIX OSDI 202614
Impact · CVEs

RQ3: Reproducing real vulnerabilities — no harness

Reproduce known bugs with only a scope + entry function (most configs generated automatically, incl. by a coding agent).

NVD CVEs
30 / 30
OSS + kernel — all reproduced
AFGen CVEs
69 / 94
re-discovered
SyzSpec kernel bugs
26 / 38
no fuzzer · no syscall specs

All without hand-crafted test harnesses; most misses are path explosion or incomplete scope, with a few from unsupported features (e.g. concurrency).

UCSan · USENIX OSDI 202615

Conclusion

UCSan: the first compilation-based under-constrained execution engine — native speed, general, decoupled from the analysis.

▸ Enables under-constrained concolic execution at scale — finishing far more analysis tasks and finding real bugs without harnesses.

▸ Validated on the Linux kernel and 150+ real CVEs & kernel bugs across three datasets.

Open source (Apache 2.0):  github.com/R-Fuzz/UCSan
UCSan · USENIX OSDI 202616

Thank you!

Questions?  ·  [email protected]  ·  github.com/R-Fuzz/UCSan

ACKNOWLEDGMENTS

We thank the anonymous reviewers for their insightful comments, and our shepherd for the valuable suggestions. This work is supported, in part, by the National Science Foundation (Grant No. 2046026), the Google ASPIRE Fund (2023 Award), and the United States Air Force and DARPA (Agreement No. FA8750-24-2-0002). Any opinions, findings, conclusions, or recommendations expressed are those of the authors and do not necessarily reflect the views of the funding agencies.

UCSan · USENIX OSDI 202617
→ / click to advance · O outline · N notes · P presenter · F fullscreen

Outline — UCSan · OSDI 2026

    Speaker notes