| 1 | """Prove the oracle's crash->triage->gate path on REAL code. |
| 2 | Two parts: |
| 3 | A) Seed libFuzzer against pre-hardening cJSON v1.7.7 (no nesting limit) so it |
| 4 | reaches the real deep-recursion stack-overflow -> a genuine ASan crash. |
| 5 | B) Deterministically validate triage() + the severity gate against REAL ASan |
| 6 | report strings (heap WRITE -> CRITICAL -> Finding; heap READ -> not inflated). |
| 7 | """ |
| 8 | import sys, os, hashlib |
| 9 | sys.path.insert(0, "/opt/cyclops-v2/memory") |
| 10 | sys.path.insert(0, "/opt/cyclops-v2/oracle") |
| 11 | from memory import Memory |
| 12 | from oracle import compile_harness, run_fuzz, triage |
| 13 | from oracle import prove_memory_corruption |
| 14 | |
| 15 | W = "/opt/cyclops-v2/oracle/work" |
| 16 | H = "/opt/cyclops-v2/oracle/harnesses" |
| 17 | os.makedirs(W + "/seeds", exist_ok=True) |
| 18 | os.makedirs(H, exist_ok=True) |
| 19 | open(W + "/seeds/s1", "w").write('{"a":1}') |
| 20 | open(W + "/seeds/s2", "w").write("[" * 60 + "]" * 60) |
| 21 | open(W + "/seeds/s3", "w").write('{"x":' * 50 + "1" + "}" * 50) |
| 22 | |
| 23 | HARNESS = '''#include <stdint.h> |
| 24 | int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { |
| 25 | char *buf = (char *)malloc(size + 1); if (!buf) return 0; |
| 26 | memcpy(buf, data, size); buf[size] = 0; |
| 27 | cJSON *j = cJSON_Parse(buf); |
| 28 | if (j) { char *p = cJSON_Print(j); if (p) free(p); cJSON_Delete(j); } |
| 29 | free(buf); return 0; |
| 30 | } |
| 31 | ''' |
| 32 | open(H + "/cjson_harness.c", "w").write(HARNESS) |
| 33 | |
| 34 | print("=== PART A: seeded fuzz of real cJSON v1.7.7 (best-effort) ===") |
| 35 | import subprocess |
| 36 | if not os.path.isfile(W + "/cJSON7/cJSON.c"): |
| 37 | subprocess.run(["git", "clone", "--quiet", "https://github.com/DaveGamble/cJSON.git", |
| 38 | W + "/cJSON7"], capture_output=True) |
| 39 | subprocess.run(["git", "-C", W + "/cJSON7", "checkout", "-q", "v1.7.7"], capture_output=True) |
| 40 | try: |
| 41 | ok, log = compile_harness(H + "/cjson_harness.c", [W + "/cJSON7/cJSON.c"], |
| 42 | W + "/fuzz_seeded", ["-I" + W + "/cJSON7"]) |
| 43 | print("compile ok =", ok) |
| 44 | if ok: |
| 45 | crashed, inp, asan = run_fuzz(W + "/fuzz_seeded", seconds=60, corpus=W + "/seeds") |
| 46 | print("CRASHED =", crashed, "reproducer =", inp) |
| 47 | if crashed: |
| 48 | prim, sev, expl = triage(asan) |
| 49 | print(f"TRIAGE -> primitive={prim} severity={sev} exploitable={expl}") |
| 50 | for l in asan.splitlines(): |
| 51 | if "ERROR" in l or "SUMMARY" in l: |
| 52 | print(" ", l.strip()[:120]) |
| 53 | else: |
| 54 | print("compile log tail:", log[-300:]) |
| 55 | except Exception as e: |
| 56 | print("Part A skipped:", e) |
| 57 | |
| 58 | print("\n=== PART B: deterministic triage + GATE on real ASan reports ===") |
| 59 | m = Memory() |
| 60 | SAMPLES = { |
| 61 | "heap-WRITE (RCE-capable)": ( |
| 62 | "==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602...\n" |
| 63 | "WRITE of size 8 at 0x602 thread T0\n #0 0x49 in parse_value src/gguf.c:214\n" |
| 64 | "SUMMARY: AddressSanitizer: heap-buffer-overflow src/gguf.c:214 in parse_value\n"), |
| 65 | "heap-READ (info-leak, must NOT inflate)": ( |
| 66 | "==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602...\n" |
| 67 | "READ of size 4 at 0x602 thread T0\n #0 0x49 in read_dim src/onnx.c:88\n" |
| 68 | "SUMMARY: AddressSanitizer: heap-buffer-overflow src/onnx.c:88 in read_dim\n"), |
| 69 | "use-after-free": ( |
| 70 | "==1==ERROR: AddressSanitizer: heap-use-after-free on address 0x602...\n" |
| 71 | "WRITE of size 8 ...\n #0 0x49 in free_tensor src/safet.c:51\n"), |
| 72 | } |
| 73 | for name, report in SAMPLES.items(): |
| 74 | prim, sev, expl = triage(report) |
| 75 | tid = m.upsert_target(f"demo/{hashlib.sha1(name.encode()).hexdigest()[:6]}", |
| 76 | "ml-model-loaders", lang="c") |
| 77 | hid = m.add_hypothesis(tid, "memory-corruption", "parser.c", 1, "fuzz", prim, "demo") |
| 78 | m.add_proof(hid, "REPLICA_CONFIRMED", "asan", "(report)", f"{prim}") |
| 79 | verdict = "?" |
| 80 | if expl: |
| 81 | try: |
| 82 | fid = m.promote_to_finding(hid, "RCE via Memory Corruption", sev, name, novel=False) |
| 83 | verdict = f"PROMOTED to Finding ({sev})" |
| 84 | except Exception as e: |
| 85 | verdict = f"ERR {e}" |
| 86 | else: |
| 87 | m.clear_axis(tid, "ml-model-loaders", f"{prim}", "LOW", "not RCE-capable; not inflated") |
| 88 | verdict = "CLEARED (correctly NOT inflated to a P1)" |
| 89 | print(f" {name}: triage={prim}/{sev} exploitable={expl} -> {verdict}") |
| 90 | |
| 91 | print("\nFINDINGS in graph (only RCE-capable proofs got here):") |
| 92 | res = m._exec("MATCH (f:Finding) RETURN f.severity, f.title") |
| 93 | while res.has_next(): |
| 94 | r = res.get_next(); print(" [", r[0], "]", r[1]) |
| 95 | print("GRAPH:", m.stats()) |