0sec-cli 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/0sec.js +322 -0
- package/README.md +63 -52
- package/attacks/data-exfiltration/pii-leakage.yaml +27 -0
- package/attacks/encoding-bypass/base64-encoding.yaml +24 -0
- package/attacks/jailbreak/dan-roleplay.yaml +27 -0
- package/attacks/jailbreak/hypothetical-scenario.yaml +25 -0
- package/attacks/jailbreak/multilingual-bypass.yaml +22 -0
- package/attacks/output-manipulation/harmful-content.yaml +25 -0
- package/attacks/prompt-injection/context-manipulation.yaml +32 -0
- package/attacks/prompt-injection/direct-injection.yaml +28 -0
- package/attacks/prompt-injection/indirect-injection.yaml +33 -0
- package/attacks/system-prompt-extraction/direct-ask.yaml +30 -0
- package/attacks/system-prompt-extraction/markdown-exfil.yaml +26 -0
- package/attacks/tool-misuse/ssrf-via-tools.yaml +27 -0
- package/chunks/adapt-loop-ECQG7454.js +18 -0
- package/chunks/adgraph-JLGA6RYI.js +54 -0
- package/chunks/agent/skills/frameworks/entra-id.yaml +63 -0
- package/chunks/agent/skills/frameworks/graphql-introspection.yaml +116 -0
- package/chunks/agent/skills/frameworks/nextjs.yaml +82 -0
- package/chunks/agent/skills/frameworks/python-web.yaml +85 -0
- package/chunks/agent/skills/frameworks/supabase.yaml +83 -0
- package/chunks/agent/skills/frameworks/wordpress-deep.yaml +122 -0
- package/chunks/agent/skills/techniques/ad-attack-paths.yaml +57 -0
- package/chunks/agent/skills/techniques/advisory-disclosure.yaml +59 -0
- package/chunks/agent/skills/techniques/assumption-mining.yaml +53 -0
- package/chunks/agent/skills/techniques/blind-exploitation.yaml +143 -0
- package/chunks/agent/skills/techniques/crypto-misuse.yaml +88 -0
- package/chunks/agent/skills/techniques/cve-poc-adaptation.yaml +82 -0
- package/chunks/agent/skills/techniques/entra-attack-paths.yaml +55 -0
- package/chunks/agent/skills/techniques/http-conformance-diff.yaml +48 -0
- package/chunks/agent/skills/techniques/jwt-attacks.yaml +137 -0
- package/chunks/agent/skills/techniques/kernel-weaponization.yaml +53 -0
- package/chunks/agent/skills/techniques/llm-excessive-agency.yaml +55 -0
- package/chunks/agent/skills/techniques/llm-insecure-output-handling.yaml +56 -0
- package/chunks/agent/skills/techniques/llm-prompt-injection.yaml +54 -0
- package/chunks/agent/skills/techniques/llm-prompt-layer-write.yaml +68 -0
- package/chunks/agent/skills/techniques/llm-rag-poisoning.yaml +57 -0
- package/chunks/agent/skills/techniques/llm-safety-eval.yaml +51 -0
- package/chunks/agent/skills/techniques/npm-ecosystem.yaml +51 -0
- package/chunks/agent/skills/techniques/poc-verification.yaml +55 -0
- package/chunks/agent/skills/techniques/race-condition.yaml +122 -0
- package/chunks/agent/skills/techniques/scoped-fix.yaml +49 -0
- package/chunks/agent/skills/techniques/seedless-depth-review.yaml +59 -0
- package/chunks/agent/skills/techniques/spec-differential.yaml +48 -0
- package/chunks/agent/skills/techniques/variant-hunting.yaml +55 -0
- package/chunks/agent/skills/vulnerabilities/cardano-eutxo-validators.yaml +72 -0
- package/chunks/agent/skills/vulnerabilities/command-injection.yaml +99 -0
- package/chunks/agent/skills/vulnerabilities/deserialization-chains.yaml +106 -0
- package/chunks/agent/skills/vulnerabilities/native-memory-safety.yaml +93 -0
- package/chunks/agent/skills/vulnerabilities/path-traversal.yaml +78 -0
- package/chunks/agent/skills/vulnerabilities/prototype-pollution.yaml +132 -0
- package/chunks/agent/skills/vulnerabilities/request-smuggling.yaml +129 -0
- package/chunks/agent/skills/vulnerabilities/sqli-advanced.yaml +102 -0
- package/chunks/agent/skills/vulnerabilities/ssrf-bypass.yaml +84 -0
- package/chunks/agent/skills/vulnerabilities/ssti-exploitation.yaml +112 -0
- package/chunks/agent/skills/vulnerabilities/structural-sqli.yaml +91 -0
- package/chunks/appsec-catalog-CBWHGTEL.js +24 -0
- package/chunks/artifact-scraper-GJEZMOHF.js +34 -0
- package/chunks/assumption-mining-BVMMUZHB.js +79 -0
- package/chunks/chunk-2CJ776PV.js +2430 -0
- package/chunks/chunk-2RMLOJVB.js +47 -0
- package/chunks/chunk-2SANI5RH.js +4728 -0
- package/chunks/chunk-3MOLBTLS.js +953 -0
- package/chunks/chunk-3QFDYBZQ.js +5108 -0
- package/chunks/chunk-47TQJWDH.js +3729 -0
- package/chunks/chunk-4Y4KQJXE.js +600 -0
- package/chunks/chunk-53G27VPS.js +136 -0
- package/chunks/chunk-57ZENEX2.js +169 -0
- package/chunks/chunk-5G3ZXFBW.js +182 -0
- package/chunks/chunk-6LKRLK2R.js +542 -0
- package/chunks/chunk-7DQEV5QI.js +287 -0
- package/chunks/chunk-A6CLR72I.js +235 -0
- package/chunks/chunk-BKZFDZ23.js +361 -0
- package/chunks/chunk-CLHCDHP4.js +471 -0
- package/chunks/chunk-D6S3IIPG.js +518 -0
- package/chunks/chunk-DN25OQQA.js +1070 -0
- package/chunks/chunk-DW5UWPFY.js +275 -0
- package/chunks/chunk-F3WBKITT.js +687 -0
- package/chunks/chunk-H2FFLZNK.js +3 -0
- package/chunks/chunk-H44E2CRN.js +193 -0
- package/chunks/chunk-HDWV7PGI.js +596 -0
- package/chunks/chunk-HE7LCA7G.js +381 -0
- package/chunks/chunk-HT6P7RY3.js +2597 -0
- package/chunks/chunk-IOL7D5YV.js +515 -0
- package/chunks/chunk-IORC6MQX.js +72828 -0
- package/chunks/chunk-IR537GON.js +49 -0
- package/chunks/chunk-K26SZ37E.js +5735 -0
- package/chunks/chunk-KKGE5RQE.js +1094 -0
- package/chunks/chunk-KLTNTE2Z.js +121 -0
- package/chunks/chunk-KTQDLNSR.js +1732 -0
- package/chunks/chunk-LOHTE223.js +2572 -0
- package/chunks/chunk-LP3HHYQU.js +245 -0
- package/chunks/chunk-MMLDQR4H.js +852 -0
- package/chunks/chunk-MYVT64FN.js +16 -0
- package/chunks/chunk-O462Y7P2.js +33019 -0
- package/chunks/chunk-OEFNRYI2.js +150 -0
- package/chunks/chunk-P6WKNFWX.js +1968 -0
- package/chunks/chunk-QI233I24.js +333 -0
- package/chunks/chunk-QJQKHO7G.js +917 -0
- package/chunks/chunk-QKICO43A.js +808 -0
- package/chunks/chunk-QOKTUOU2.js +102 -0
- package/chunks/chunk-RDZYQQLW.js +596 -0
- package/chunks/chunk-RJWOYEOG.js +271 -0
- package/chunks/chunk-RRMJC3ZE.js +105 -0
- package/chunks/chunk-RWONANDA.js +973 -0
- package/chunks/chunk-SAFFWQW4.js +5921 -0
- package/chunks/chunk-SF4KZ4O3.js +502 -0
- package/chunks/chunk-SNKTC4BP.js +1422 -0
- package/chunks/chunk-SOG2U7B3.js +662 -0
- package/chunks/chunk-SZJCPG2I.js +110 -0
- package/chunks/chunk-UJ4IK5PO.js +2010 -0
- package/chunks/chunk-UM3ZNQIM.js +9329 -0
- package/chunks/chunk-VGRDNSHA.js +3249 -0
- package/chunks/chunk-VQG4FT5D.js +685 -0
- package/chunks/chunk-WKNNZVJS.js +2798 -0
- package/chunks/chunk-WU6AFRAZ.js +1167 -0
- package/chunks/chunk-WVTBZEQO.js +1531 -0
- package/chunks/chunk-YLMN3N25.js +4054 -0
- package/chunks/commands-U3AMV5ZU.js +16795 -0
- package/chunks/corpus-v1.json +403 -0
- package/chunks/cost-ledger-ZCKMBJEN.js +13 -0
- package/chunks/data/appsec-archetypes.json +102 -0
- package/chunks/data/chromium-archetypes.json +204 -0
- package/chunks/data/freebsd-archetypes.json +171 -0
- package/chunks/data/kernel-archetypes.json +611 -0
- package/chunks/db-26NGQFKO.js +16 -0
- package/chunks/disclose-A6AEIEDA.js +132 -0
- package/chunks/dist-FDKALV4R.js +2772 -0
- package/chunks/dist-GL66KMUX.js +74 -0
- package/chunks/dist-JR67XKYO.js +159 -0
- package/chunks/eval-runner-DDLE5RQ3.js +27 -0
- package/chunks/example-manifest.json +95 -0
- package/chunks/exploit-agent-XP23ISN7.js +98 -0
- package/chunks/exploit-autoclimb-E6T7RRSC.js +81 -0
- package/chunks/exploit-climb-2FAUP6XU.js +358 -0
- package/chunks/fix-IG7LYXH3.js +12 -0
- package/chunks/github-issues-MJ6OYOOU.js +157 -0
- package/chunks/harness-HWOYFKFY.js +22 -0
- package/chunks/http-conformance-DU66MZIU.js +11 -0
- package/chunks/http-sender-GWH2IYEA.js +10 -0
- package/chunks/hunt-scan-XPELSPQQ.js +38 -0
- package/chunks/identity-6ZAIIWOR.js +191 -0
- package/chunks/kernel-primitive-TENE3R7T.js +44 -0
- package/chunks/kernel-vm-runner-4F6QSNFY.js +62 -0
- package/chunks/memsafety-scan-J7SECV5W.js +16 -0
- package/chunks/native-loop-NTONUDP7.js +54 -0
- package/chunks/npm-detectors-KB5Y5ZFX.js +69 -0
- package/chunks/npm-dynamic-discovery-AHROOZDB.js +13 -0
- package/chunks/orchestrate-ZUWAUWBC.js +62 -0
- package/chunks/pipeline-FCARFZU3.js +14 -0
- package/chunks/pre-recon-cve-66EB6G4M.js +351 -0
- package/chunks/prepare-MYY743TK.js +13 -0
- package/chunks/process-3Q7QJIOZ.js +13 -0
- package/chunks/replay-runner-RRR4E2A7.js +39 -0
- package/chunks/run-ESNN4V5W.js +32632 -0
- package/chunks/runtime-62ZQAH7H.js +43 -0
- package/chunks/runtime-J7PLXZNM.js +12 -0
- package/chunks/scan-stream-FHI2FYZE.js +96 -0
- package/chunks/scope-BI7BF4ZY.js +18 -0
- package/chunks/session-store-BCFQYDCE.js +28 -0
- package/chunks/source-files-PWRQR6LY.js +12 -0
- package/chunks/specdrift-WCLH6TTQ.js +19 -0
- package/chunks/variant-candidates-6KM6F6MD.js +13 -0
- package/chunks/web-recon-prepass-DPVCBRZA.js +1148 -0
- package/dashboard/assets/0sec-icon-66SreztZ.gif +0 -0
- package/dashboard/assets/bot-yILPnRgD.js +1 -0
- package/dashboard/assets/chevron-down-BeDJ-Vka.js +1 -0
- package/dashboard/assets/circle-alert-BEnMBMT-.js +1 -0
- package/dashboard/assets/client-DRYsuOGl.js +9 -0
- package/dashboard/assets/copy-Bi5vzoF9.js +1 -0
- package/dashboard/assets/desktop-CXTQonNm.js +32 -0
- package/dashboard/assets/desktop-NOekk4QS.css +1 -0
- package/dashboard/assets/dist-CmX-h7a4.js +1 -0
- package/dashboard/assets/dist-DAwYQmf_.js +45 -0
- package/dashboard/assets/findings-page-CACW9nw2.js +5 -0
- package/dashboard/assets/format-PoISqsES.js +1 -0
- package/dashboard/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dashboard/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dashboard/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dashboard/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dashboard/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/dashboard/assets/jsx-runtime-C7oxC63R.js +1 -0
- package/dashboard/assets/live-page-DrgQqQK5.js +1 -0
- package/dashboard/assets/meta-tile-CcskIV_o.js +1 -0
- package/dashboard/assets/operations-BKE8T-vY.css +2 -0
- package/dashboard/assets/operations-D8nUP5_m.js +4 -0
- package/dashboard/assets/operations-app-CQQhG54n.js +2 -0
- package/dashboard/assets/overview-page-DTGS8dPo.js +1 -0
- package/dashboard/assets/page-header-MFVvEtZW.js +1 -0
- package/dashboard/assets/play-Dwo-9xt6.js +1 -0
- package/dashboard/assets/scans-page-ChVtq8Us.js +1 -0
- package/dashboard/assets/search-CiooTLck.js +1 -0
- package/dashboard/assets/siren-CCBwTSuk.js +1 -0
- package/dashboard/assets/table-BYS-nIVA.js +1 -0
- package/dashboard/assets/tabs-DivxbhQy.js +1 -0
- package/dashboard/desktop.html +21 -0
- package/dashboard/index.html +15 -0
- package/package.json +30 -18
- package/bin/0sec.cjs +0 -361
|
@@ -0,0 +1,1531 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __0secCreateRequire } from "node:module";
|
|
3
|
+
const require = __0secCreateRequire(import.meta.url);
|
|
4
|
+
import {
|
|
5
|
+
buildPlaybookInjection,
|
|
6
|
+
parseSanitizerLog,
|
|
7
|
+
verifyOracleByCategory
|
|
8
|
+
} from "./chunk-P6WKNFWX.js";
|
|
9
|
+
import {
|
|
10
|
+
allowlistedChildEnv
|
|
11
|
+
} from "./chunk-KLTNTE2Z.js";
|
|
12
|
+
|
|
13
|
+
// packages/core/dist/stages/memsafety-scan.js
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
|
|
16
|
+
// packages/core/dist/triage/userspace-fuzz-runner.js
|
|
17
|
+
import { execFile } from "node:child_process";
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
21
|
+
import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
22
|
+
var MEMSAFETY_ARTIFACT_SCHEMA = "0sec-memsafety-artifacts/v1";
|
|
23
|
+
var MAX_RETAINED_CRASHES = 16;
|
|
24
|
+
var MAX_RETAINED_REPRODUCER_BYTES = 1024 * 1024;
|
|
25
|
+
var MAX_RETAINED_LOG_BYTES = 256 * 1024;
|
|
26
|
+
var DEFAULT_RETAINED_ARTIFACT_BYTES = 4 * 1024 * 1024;
|
|
27
|
+
var MIN_RETAINED_ARTIFACT_BYTES = 64 * 1024;
|
|
28
|
+
var ARTIFACT_MANIFEST_RESERVE_BYTES = 64 * 1024;
|
|
29
|
+
var TOOLING_HINTS = {
|
|
30
|
+
cargo: "cargo not found \u2014 install the Rust toolchain via https://rustup.rs",
|
|
31
|
+
"cargo-fuzz": "cargo-fuzz not found \u2014 install with `cargo install cargo-fuzz` (requires a nightly toolchain for `cargo fuzz run`)",
|
|
32
|
+
"cargo-fuzz-harness": "cargo-fuzz needs exactly one harness \u2014 pass `--harness <name>` or provide a source tree with one cargo-fuzz target",
|
|
33
|
+
miri: "miri not found \u2014 install with `rustup +nightly component add miri`",
|
|
34
|
+
clang: "clang not found \u2014 install LLVM/clang to compile the libFuzzer harness"
|
|
35
|
+
};
|
|
36
|
+
function commandAvailable(command, args) {
|
|
37
|
+
return new Promise((resolveProbe) => {
|
|
38
|
+
execFile(command, args, { timeout: 1e4, env: fuzzChildEnv() }, (error) => {
|
|
39
|
+
resolveProbe(!error);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async function cargoAvailable() {
|
|
44
|
+
return commandAvailable("cargo", ["--version"]);
|
|
45
|
+
}
|
|
46
|
+
async function cargoFuzzAvailable() {
|
|
47
|
+
return commandAvailable("cargo", ["fuzz", "--help"]);
|
|
48
|
+
}
|
|
49
|
+
async function miriAvailable() {
|
|
50
|
+
return commandAvailable("cargo", ["+nightly", "miri", "--version"]);
|
|
51
|
+
}
|
|
52
|
+
async function clangAvailable() {
|
|
53
|
+
return commandAvailable("clang", ["--version"]);
|
|
54
|
+
}
|
|
55
|
+
var ASAN_PRIMITIVE = {
|
|
56
|
+
"heap-use-after-free": "use-after-free",
|
|
57
|
+
"stack-use-after-return": "use-after-free",
|
|
58
|
+
"stack-use-after-scope": "use-after-free",
|
|
59
|
+
"double-free": "double-free",
|
|
60
|
+
"attempting-double-free": "double-free",
|
|
61
|
+
"stack-buffer-overflow": "stack-oob"
|
|
62
|
+
};
|
|
63
|
+
function primitiveFromSanitizerKind(kind, rw) {
|
|
64
|
+
if (kind === "heap-buffer-overflow" || kind === "global-buffer-overflow") {
|
|
65
|
+
return rw === "write" ? "heap-oob-write" : "heap-oob-read";
|
|
66
|
+
}
|
|
67
|
+
const direct = ASAN_PRIMITIVE[kind];
|
|
68
|
+
if (direct)
|
|
69
|
+
return direct;
|
|
70
|
+
if (kind === "signed-integer-overflow" || kind === "shift-exponent") {
|
|
71
|
+
return "integer-overflow";
|
|
72
|
+
}
|
|
73
|
+
if (kind === "null-pointer-use")
|
|
74
|
+
return "null-deref";
|
|
75
|
+
return "unknown";
|
|
76
|
+
}
|
|
77
|
+
function signatureFromFrames(kind, frames, rawOutput) {
|
|
78
|
+
const top = frames.map((f) => normaliseFrame(f)).filter(Boolean).slice(0, 5);
|
|
79
|
+
const basis = top.length > 0 ? `${kind}
|
|
80
|
+
${top.join("\n")}` : `${kind}
|
|
81
|
+
${rawOutput.trim().split("\n").slice(0, 3).join("\n")}`;
|
|
82
|
+
return createHash("sha256").update(basis).digest("hex").slice(0, 16);
|
|
83
|
+
}
|
|
84
|
+
function normaliseFrame(frame) {
|
|
85
|
+
return frame.replace(/0x[0-9a-fA-F]+/g, "").replace(/\+0x[0-9a-fA-F]+(\/0x[0-9a-fA-F]+)?/g, "").replace(/:\d+(:\d+)?\b/g, "").replace(/\s+/g, " ").trim();
|
|
86
|
+
}
|
|
87
|
+
function parseCrashOutput(output, hint) {
|
|
88
|
+
const text = output.trim();
|
|
89
|
+
if (!text)
|
|
90
|
+
return null;
|
|
91
|
+
const sanitizer = parseSanitizerLog(text);
|
|
92
|
+
if (sanitizer) {
|
|
93
|
+
const frames = sanitizer.frames.map((f) => [f.functionName, f.file && `${f.file}:${f.line ?? ""}`].filter(Boolean).join(" "));
|
|
94
|
+
const kind = sanitizer.sanitizer === "asan" ? "asan" : "ubsan";
|
|
95
|
+
const rw = sanitizer.primitive === "read" || sanitizer.primitive === "write" ? sanitizer.primitive : "unknown";
|
|
96
|
+
return {
|
|
97
|
+
kind,
|
|
98
|
+
signature: signatureFromFrames(sanitizer.kind, frames, text),
|
|
99
|
+
rawOutput: text,
|
|
100
|
+
inputPath: hint?.inputPath,
|
|
101
|
+
stack: frames.length > 0 ? frames : void 0,
|
|
102
|
+
primitive: primitiveFromSanitizerKind(sanitizer.kind, rw)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (/error: Undefined Behavior|Miri caught|error\[E\d+\].*unsafe/i.test(text)) {
|
|
106
|
+
const frames = parseMiriFrames(text);
|
|
107
|
+
return {
|
|
108
|
+
kind: "miri",
|
|
109
|
+
signature: signatureFromFrames("miri", frames, text),
|
|
110
|
+
rawOutput: text,
|
|
111
|
+
inputPath: hint?.inputPath,
|
|
112
|
+
stack: frames.length > 0 ? frames : void 0,
|
|
113
|
+
primitive: primitiveFromMiri(text)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (/thread '.*' panicked at|panicked at/i.test(text)) {
|
|
117
|
+
const frames = parseRustBacktrace(text);
|
|
118
|
+
return {
|
|
119
|
+
kind: "panic",
|
|
120
|
+
signature: signatureFromFrames("panic", frames, text),
|
|
121
|
+
rawOutput: text,
|
|
122
|
+
inputPath: hint?.inputPath,
|
|
123
|
+
stack: frames.length > 0 ? frames : void 0,
|
|
124
|
+
primitive: primitiveFromPanic(text)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (hint?.kind === "timeout") {
|
|
128
|
+
return {
|
|
129
|
+
kind: "timeout",
|
|
130
|
+
signature: signatureFromFrames("timeout", [], text),
|
|
131
|
+
rawOutput: text,
|
|
132
|
+
inputPath: hint?.inputPath,
|
|
133
|
+
primitive: "unknown"
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (/out-of-memory|libFuzzer: out-of-memory|rss limit exceeded/i.test(text)) {
|
|
137
|
+
return {
|
|
138
|
+
kind: "oom",
|
|
139
|
+
signature: signatureFromFrames("oom", [], text),
|
|
140
|
+
rawOutput: text,
|
|
141
|
+
inputPath: hint?.inputPath,
|
|
142
|
+
primitive: "unknown"
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (hint?.kind === "segfault" || /SEGV|segmentation fault|deadly signal|SIGSEGV/i.test(text)) {
|
|
146
|
+
return {
|
|
147
|
+
kind: "segfault",
|
|
148
|
+
signature: signatureFromFrames("segfault", parseRustBacktrace(text), text),
|
|
149
|
+
rawOutput: text,
|
|
150
|
+
inputPath: hint?.inputPath,
|
|
151
|
+
primitive: "null-deref"
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
function parseMiriFrames(text) {
|
|
157
|
+
const frames = [];
|
|
158
|
+
const locRe = /-->\s+(.+?:\d+(?::\d+)?)/g;
|
|
159
|
+
let m;
|
|
160
|
+
while ((m = locRe.exec(text)) !== null)
|
|
161
|
+
frames.push(m[1]);
|
|
162
|
+
return frames.length > 0 ? frames : parseRustBacktrace(text);
|
|
163
|
+
}
|
|
164
|
+
function parseRustBacktrace(text) {
|
|
165
|
+
const frames = [];
|
|
166
|
+
const symRe = /^\s*\d+:\s+(.+)$/gm;
|
|
167
|
+
let m;
|
|
168
|
+
while ((m = symRe.exec(text)) !== null)
|
|
169
|
+
frames.push(m[1].trim());
|
|
170
|
+
return frames;
|
|
171
|
+
}
|
|
172
|
+
function primitiveFromMiri(text) {
|
|
173
|
+
const lower = text.toLowerCase();
|
|
174
|
+
if (lower.includes("use-after-free") || lower.includes("dangling")) {
|
|
175
|
+
return "use-after-free";
|
|
176
|
+
}
|
|
177
|
+
if (lower.includes("uninitialized"))
|
|
178
|
+
return "uninit-read";
|
|
179
|
+
if (lower.includes("out-of-bounds") || lower.includes("out of bounds")) {
|
|
180
|
+
return "heap-oob-read";
|
|
181
|
+
}
|
|
182
|
+
if (lower.includes("null pointer") || lower.includes("null reference")) {
|
|
183
|
+
return "null-deref";
|
|
184
|
+
}
|
|
185
|
+
if (lower.includes("type") && lower.includes("validity"))
|
|
186
|
+
return "type-confusion";
|
|
187
|
+
return "unknown";
|
|
188
|
+
}
|
|
189
|
+
function primitiveFromPanic(text) {
|
|
190
|
+
const lower = text.toLowerCase();
|
|
191
|
+
if (lower.includes("index out of bounds"))
|
|
192
|
+
return "heap-oob-read";
|
|
193
|
+
if (lower.includes("overflow"))
|
|
194
|
+
return "integer-overflow";
|
|
195
|
+
return "unknown";
|
|
196
|
+
}
|
|
197
|
+
var FUZZ_TOOLCHAIN_ENV_PASSTHROUGH = [
|
|
198
|
+
"CARGO_HOME",
|
|
199
|
+
"RUSTUP_HOME",
|
|
200
|
+
"RUSTUP_TOOLCHAIN",
|
|
201
|
+
"CARGO_TARGET_DIR"
|
|
202
|
+
];
|
|
203
|
+
function fuzzChildEnv() {
|
|
204
|
+
const extras = {};
|
|
205
|
+
for (const key of FUZZ_TOOLCHAIN_ENV_PASSTHROUGH) {
|
|
206
|
+
const value = process.env[key];
|
|
207
|
+
if (value !== void 0)
|
|
208
|
+
extras[key] = value;
|
|
209
|
+
}
|
|
210
|
+
return allowlistedChildEnv(extras);
|
|
211
|
+
}
|
|
212
|
+
function runChild(command, args, cwd, timeoutMs) {
|
|
213
|
+
return new Promise((resolveRun) => {
|
|
214
|
+
execFile(command, args, { cwd, timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024, killSignal: "SIGKILL", env: fuzzChildEnv() }, (error, stdout, stderr) => {
|
|
215
|
+
const stdoutText = String(stdout ?? "");
|
|
216
|
+
const output = `${stdoutText}${String(stderr ?? "")}`;
|
|
217
|
+
const timedOut = Boolean(error && error.killed);
|
|
218
|
+
const signalled = Boolean(error && error.signal);
|
|
219
|
+
resolveRun({
|
|
220
|
+
output,
|
|
221
|
+
stdout: stdoutText,
|
|
222
|
+
timedOut,
|
|
223
|
+
signalled,
|
|
224
|
+
failed: Boolean(error)
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
function dedupeBySignature(crashes) {
|
|
230
|
+
const seen = /* @__PURE__ */ new Map();
|
|
231
|
+
for (const crash of crashes) {
|
|
232
|
+
if (!seen.has(crash.signature))
|
|
233
|
+
seen.set(crash.signature, crash);
|
|
234
|
+
}
|
|
235
|
+
return Array.from(seen.values());
|
|
236
|
+
}
|
|
237
|
+
function pathIsWithin(root, candidate) {
|
|
238
|
+
const rel = relative(root, candidate);
|
|
239
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
240
|
+
}
|
|
241
|
+
function resolveCargoFuzzDir(sourceRoot, fuzzDir) {
|
|
242
|
+
const raw = fuzzDir?.trim() || "fuzz";
|
|
243
|
+
if (isAbsolute(raw))
|
|
244
|
+
return void 0;
|
|
245
|
+
const candidate = resolve(sourceRoot, raw);
|
|
246
|
+
if (!pathIsWithin(sourceRoot, candidate))
|
|
247
|
+
return void 0;
|
|
248
|
+
try {
|
|
249
|
+
const canonical = realpathSync(candidate);
|
|
250
|
+
return canonical === candidate && pathIsWithin(sourceRoot, canonical) ? canonical : void 0;
|
|
251
|
+
} catch {
|
|
252
|
+
return void 0;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function isCargoFuzzHarnessName(value) {
|
|
256
|
+
return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(value);
|
|
257
|
+
}
|
|
258
|
+
function isStableSourceDirectory(sourceRoot, directory) {
|
|
259
|
+
try {
|
|
260
|
+
const canonical = realpathSync(directory);
|
|
261
|
+
return canonical === directory && pathIsWithin(sourceRoot, canonical);
|
|
262
|
+
} catch {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function resolveArtifactRoot(artifactDir, sourceRoot) {
|
|
267
|
+
if (!artifactDir)
|
|
268
|
+
return void 0;
|
|
269
|
+
mkdirSync(artifactDir, { recursive: true, mode: 448 });
|
|
270
|
+
const canonical = realpathSync(artifactDir);
|
|
271
|
+
if (pathIsWithin(sourceRoot, canonical)) {
|
|
272
|
+
throw new Error("artifactDir must resolve outside the prepared source tree");
|
|
273
|
+
}
|
|
274
|
+
return artifactDir;
|
|
275
|
+
}
|
|
276
|
+
function makeArtifactDir(artifactDir) {
|
|
277
|
+
if (artifactDir) {
|
|
278
|
+
return { dir: mkdtempSync(join(artifactDir, "0sec-uf-")), ephemeral: false };
|
|
279
|
+
}
|
|
280
|
+
return { dir: mkdtempSync(join(tmpdir(), "0sec-uf-")), ephemeral: true };
|
|
281
|
+
}
|
|
282
|
+
function corpusCount(dir) {
|
|
283
|
+
try {
|
|
284
|
+
return readdirSync(dir).filter((name) => {
|
|
285
|
+
try {
|
|
286
|
+
return statSync(join(dir, name)).isFile();
|
|
287
|
+
} catch {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}).length;
|
|
291
|
+
} catch {
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async function runUserspaceFuzzLoop(opts) {
|
|
296
|
+
const log = opts.logger ?? ((line) => console.log(line));
|
|
297
|
+
const sourceRoot = resolve(opts.target.sourceRoot);
|
|
298
|
+
const canonicalSourceRoot = existsSync(sourceRoot) ? realpathSync(sourceRoot) : sourceRoot;
|
|
299
|
+
const runOpts = {
|
|
300
|
+
...opts,
|
|
301
|
+
target: { ...opts.target, sourceRoot: canonicalSourceRoot }
|
|
302
|
+
};
|
|
303
|
+
const start = Date.now();
|
|
304
|
+
const timeoutSec = opts.timeoutSec ?? parseInt(process.env["0SEC_USERSPACE_FUZZ_TIMEOUT_SEC"]?.trim() || "60", 10);
|
|
305
|
+
const timeoutMs = Math.max(1, timeoutSec) * 1e3;
|
|
306
|
+
const artifactDir = resolveArtifactRoot(opts.artifactDir, canonicalSourceRoot);
|
|
307
|
+
const artifactMaxBytes = artifactDir ? resolveRetainedArtifactBytes(opts.artifactMaxBytes) : null;
|
|
308
|
+
const { dir: workDir, ephemeral } = makeArtifactDir(artifactDir);
|
|
309
|
+
const toolingMissing = [];
|
|
310
|
+
let retained = false;
|
|
311
|
+
try {
|
|
312
|
+
const result = runOpts.target.language === "rust" ? await runRustLoop(runOpts, { workDir, timeoutMs, toolingMissing, log, start }) : await runCLoop(runOpts, { workDir, timeoutMs, toolingMissing, log, start });
|
|
313
|
+
if (!ephemeral && artifactMaxBytes !== null) {
|
|
314
|
+
retained = persistCapturedArtifacts(result, workDir, canonicalSourceRoot, artifactMaxBytes, log);
|
|
315
|
+
}
|
|
316
|
+
return result;
|
|
317
|
+
} finally {
|
|
318
|
+
if (ephemeral || !retained)
|
|
319
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function persistCapturedArtifacts(result, workDir, sourceRoot, maxBytes, log) {
|
|
323
|
+
if (result.crashes.length === 0)
|
|
324
|
+
return false;
|
|
325
|
+
const runDir = basename(workDir);
|
|
326
|
+
const logsDir = join(workDir, "logs");
|
|
327
|
+
const reproducersDir = join(workDir, "reproducers");
|
|
328
|
+
mkdirSync(logsDir, { recursive: true, mode: 448 });
|
|
329
|
+
mkdirSync(reproducersDir, { recursive: true, mode: 448 });
|
|
330
|
+
let remaining = maxBytes - ARTIFACT_MANIFEST_RESERVE_BYTES;
|
|
331
|
+
const crashes = result.crashes.slice(0, MAX_RETAINED_CRASHES).map((crash, index) => {
|
|
332
|
+
const id = safeArtifactId(crash.signature, index);
|
|
333
|
+
let reproducer = null;
|
|
334
|
+
if (crash.inputPath && remaining > 0) {
|
|
335
|
+
const reproducerRef = `${runDir}/reproducers/${id}.input`;
|
|
336
|
+
reproducer = copyBoundedReproducer(crash.inputPath, join(reproducersDir, `${id}.input`), reproducerRef, sourceRoot, Math.min(MAX_RETAINED_REPRODUCER_BYTES, remaining));
|
|
337
|
+
if (reproducer) {
|
|
338
|
+
remaining -= reproducer.sizeBytes;
|
|
339
|
+
crash.inputPath = join(reproducersDir, `${id}.input`);
|
|
340
|
+
crash.artifactRef = reproducer.ref;
|
|
341
|
+
} else {
|
|
342
|
+
log(`[userspace-fuzz] did not retain reproducer for ${crash.signature} (missing, non-regular, changed while read, or exceeds the remaining evidence budget)`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const logRef = `${runDir}/logs/${id}.log`;
|
|
346
|
+
const logFile = writeBoundedUtf8File(join(logsDir, `${id}.log`), crash.rawOutput, Math.min(MAX_RETAINED_LOG_BYTES, remaining));
|
|
347
|
+
if (logFile)
|
|
348
|
+
remaining -= logFile.sizeBytes;
|
|
349
|
+
return {
|
|
350
|
+
signature: crash.signature,
|
|
351
|
+
kind: crash.kind,
|
|
352
|
+
primitive: crash.primitive ?? null,
|
|
353
|
+
stack: boundedStack(crash.stack),
|
|
354
|
+
log: logFile ? { ...logFile, ref: logRef } : null,
|
|
355
|
+
reproducer
|
|
356
|
+
};
|
|
357
|
+
});
|
|
358
|
+
if (result.crashes.length > MAX_RETAINED_CRASHES) {
|
|
359
|
+
log(`[userspace-fuzz] retained the first ${MAX_RETAINED_CRASHES} of ${result.crashes.length} deduplicated crashes`);
|
|
360
|
+
}
|
|
361
|
+
const manifest = Buffer.from(JSON.stringify({
|
|
362
|
+
schema: MEMSAFETY_ARTIFACT_SCHEMA,
|
|
363
|
+
crashes
|
|
364
|
+
}, null, 2) + "\n", "utf8");
|
|
365
|
+
if (manifest.byteLength > ARTIFACT_MANIFEST_RESERVE_BYTES) {
|
|
366
|
+
rmSync(logsDir, { recursive: true, force: true });
|
|
367
|
+
rmSync(reproducersDir, { recursive: true, force: true });
|
|
368
|
+
log("[userspace-fuzz] evidence manifest exceeded its bounded reserve; discarded this run's retained copies");
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
writeFileSync(join(workDir, "manifest.json"), manifest, {
|
|
372
|
+
flag: "wx",
|
|
373
|
+
mode: 384
|
|
374
|
+
});
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
function resolveRetainedArtifactBytes(value) {
|
|
378
|
+
const bytes = value ?? DEFAULT_RETAINED_ARTIFACT_BYTES;
|
|
379
|
+
if (!Number.isSafeInteger(bytes) || bytes < MIN_RETAINED_ARTIFACT_BYTES) {
|
|
380
|
+
throw new Error(`artifactMaxBytes must be a safe integer of at least ${MIN_RETAINED_ARTIFACT_BYTES}`);
|
|
381
|
+
}
|
|
382
|
+
return bytes;
|
|
383
|
+
}
|
|
384
|
+
function safeArtifactId(signature, index) {
|
|
385
|
+
if (/^[0-9a-f]{16}$/.test(signature))
|
|
386
|
+
return signature;
|
|
387
|
+
return createHash("sha256").update(`${index}\0${signature}`).digest("hex").slice(0, 16);
|
|
388
|
+
}
|
|
389
|
+
function boundedStack(stack) {
|
|
390
|
+
return (stack ?? []).slice(0, 10).map((frame) => frame.slice(0, 1024));
|
|
391
|
+
}
|
|
392
|
+
function writeBoundedUtf8File(path, value, limit) {
|
|
393
|
+
if (limit <= 0)
|
|
394
|
+
return null;
|
|
395
|
+
const input = Buffer.from(value, "utf8");
|
|
396
|
+
const truncated = input.length > limit;
|
|
397
|
+
const bytes = truncated ? input.subarray(input.length - limit) : input;
|
|
398
|
+
writeFileSync(path, bytes, { flag: "wx", mode: 384 });
|
|
399
|
+
return {
|
|
400
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
401
|
+
sizeBytes: bytes.length,
|
|
402
|
+
...truncated ? { truncated: true } : {}
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function copyBoundedReproducer(source, destination, ref, sourceRoot, limit) {
|
|
406
|
+
let fd;
|
|
407
|
+
try {
|
|
408
|
+
if (!isStableSourceDirectory(sourceRoot, dirname(source))) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
const listed = lstatSync(source);
|
|
412
|
+
if (!listed.isFile() || listed.isSymbolicLink() || listed.size > limit) {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
fd = openSync(source, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
416
|
+
const before = fstatSync(fd);
|
|
417
|
+
if (!before.isFile() || before.dev !== listed.dev || before.ino !== listed.ino || before.size !== listed.size || before.mtimeMs !== listed.mtimeMs) {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
const bytes = readFileSync(fd);
|
|
421
|
+
const after = fstatSync(fd);
|
|
422
|
+
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || bytes.length !== before.size) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
if (!isStableSourceDirectory(sourceRoot, dirname(source))) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
writeFileSync(destination, bytes, { flag: "wx", mode: 384 });
|
|
429
|
+
return {
|
|
430
|
+
ref,
|
|
431
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
432
|
+
sizeBytes: bytes.length
|
|
433
|
+
};
|
|
434
|
+
} catch {
|
|
435
|
+
return null;
|
|
436
|
+
} finally {
|
|
437
|
+
if (fd !== void 0)
|
|
438
|
+
closeSync(fd);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function cargoFuzzRunArgs(harnessEntry, fuzzDir, timeoutSec) {
|
|
442
|
+
const fuzzDirArgs = fuzzDir ? ["--fuzz-dir", fuzzDir] : [];
|
|
443
|
+
return [
|
|
444
|
+
"fuzz",
|
|
445
|
+
"run",
|
|
446
|
+
...fuzzDirArgs,
|
|
447
|
+
harnessEntry,
|
|
448
|
+
"--",
|
|
449
|
+
`-max_total_time=${Math.floor(timeoutSec)}`
|
|
450
|
+
];
|
|
451
|
+
}
|
|
452
|
+
function cargoFuzzListArgs(fuzzDir) {
|
|
453
|
+
return ["fuzz", "list", ...fuzzDir ? ["--fuzz-dir", fuzzDir] : []];
|
|
454
|
+
}
|
|
455
|
+
async function discoverCargoFuzzHarness(sourceRoot, fuzzDir, timeoutMs) {
|
|
456
|
+
if (!existsSync(join(sourceRoot, fuzzDir ?? "fuzz", "Cargo.toml"))) {
|
|
457
|
+
return { reason: `no cargo-fuzz manifest at ${fuzzDir ?? "fuzz"}/Cargo.toml` };
|
|
458
|
+
}
|
|
459
|
+
const res = await runChild("cargo", cargoFuzzListArgs(fuzzDir), sourceRoot, timeoutMs);
|
|
460
|
+
if (res.failed)
|
|
461
|
+
return { reason: "cargo fuzz list failed" };
|
|
462
|
+
const harnesses = Array.from(new Set(res.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)));
|
|
463
|
+
if (harnesses.length === 1)
|
|
464
|
+
return { harnessEntry: harnesses[0] };
|
|
465
|
+
if (harnesses.length === 0)
|
|
466
|
+
return { reason: "no cargo-fuzz harnesses found" };
|
|
467
|
+
return {
|
|
468
|
+
reason: `multiple cargo-fuzz harnesses found (${harnesses.join(", ")})`
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
async function runRustLoop(opts, ctx) {
|
|
472
|
+
const { target } = opts;
|
|
473
|
+
const sourceRoot = resolve(target.sourceRoot);
|
|
474
|
+
const fuzzRoot = resolveCargoFuzzDir(sourceRoot, target.fuzzDir);
|
|
475
|
+
const crashes = [];
|
|
476
|
+
let iterations = 0;
|
|
477
|
+
let harnessEntry = target.harnessEntry;
|
|
478
|
+
let executedHarness;
|
|
479
|
+
if (!await cargoAvailable()) {
|
|
480
|
+
ctx.toolingMissing.push("cargo");
|
|
481
|
+
ctx.log(`[userspace-fuzz] ${TOOLING_HINTS.cargo}`);
|
|
482
|
+
return finish(crashes, iterations, sourceRoot, ctx, "fuzz");
|
|
483
|
+
}
|
|
484
|
+
if (!fuzzRoot || harnessEntry && !isCargoFuzzHarnessName(harnessEntry)) {
|
|
485
|
+
ctx.toolingMissing.push("cargo-fuzz-layout");
|
|
486
|
+
ctx.log("[userspace-fuzz] rejected unsafe cargo-fuzz directory or harness name");
|
|
487
|
+
return finish(crashes, iterations, sourceRoot, ctx, "fuzz");
|
|
488
|
+
}
|
|
489
|
+
const fuzzDir = relative(sourceRoot, fuzzRoot);
|
|
490
|
+
if (!await cargoFuzzAvailable()) {
|
|
491
|
+
ctx.toolingMissing.push("cargo-fuzz");
|
|
492
|
+
ctx.log(`[userspace-fuzz] ${TOOLING_HINTS["cargo-fuzz"]}`);
|
|
493
|
+
} else {
|
|
494
|
+
if (!harnessEntry) {
|
|
495
|
+
const discovery = await discoverCargoFuzzHarness(sourceRoot, fuzzDir, ctx.timeoutMs);
|
|
496
|
+
if ("reason" in discovery) {
|
|
497
|
+
ctx.toolingMissing.push("cargo-fuzz-harness");
|
|
498
|
+
ctx.log(`[userspace-fuzz] ${discovery.reason}; ${TOOLING_HINTS["cargo-fuzz-harness"]}`);
|
|
499
|
+
} else {
|
|
500
|
+
harnessEntry = discovery.harnessEntry;
|
|
501
|
+
if (!isCargoFuzzHarnessName(harnessEntry)) {
|
|
502
|
+
ctx.toolingMissing.push("cargo-fuzz-harness");
|
|
503
|
+
ctx.log("[userspace-fuzz] cargo-fuzz reported an unsafe harness name");
|
|
504
|
+
harnessEntry = void 0;
|
|
505
|
+
} else {
|
|
506
|
+
ctx.log(`[userspace-fuzz] auto-selected cargo-fuzz harness ${harnessEntry}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (harnessEntry) {
|
|
511
|
+
ctx.log(`[userspace-fuzz] cargo fuzz run ${harnessEntry} (<=${ctx.timeoutMs / 1e3}s)`);
|
|
512
|
+
const res = await runChild("cargo", cargoFuzzRunArgs(harnessEntry, fuzzDir, ctx.timeoutMs / 1e3), sourceRoot, ctx.timeoutMs + 3e4);
|
|
513
|
+
executedHarness = harnessEntry;
|
|
514
|
+
iterations += 1;
|
|
515
|
+
const crash = parseCrashOutput(res.output, {
|
|
516
|
+
kind: res.timedOut ? "timeout" : void 0,
|
|
517
|
+
inputPath: findLibfuzzerCrashInput(join(fuzzRoot, "artifacts", harnessEntry), sourceRoot)
|
|
518
|
+
});
|
|
519
|
+
if (crash)
|
|
520
|
+
crashes.push(crash);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (opts.runMiri) {
|
|
524
|
+
if (!await miriAvailable()) {
|
|
525
|
+
ctx.toolingMissing.push("miri");
|
|
526
|
+
ctx.log(`[userspace-fuzz] ${TOOLING_HINTS.miri}`);
|
|
527
|
+
} else {
|
|
528
|
+
ctx.log("[userspace-fuzz] cargo +nightly miri test (UB run)");
|
|
529
|
+
const res = await runChild("cargo", ["+nightly", "miri", "test"], sourceRoot, ctx.timeoutMs + 3e4);
|
|
530
|
+
iterations += 1;
|
|
531
|
+
const crash = parseCrashOutput(res.output, {
|
|
532
|
+
kind: res.timedOut ? "timeout" : void 0
|
|
533
|
+
});
|
|
534
|
+
if (crash)
|
|
535
|
+
crashes.push(crash);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const corpusDir = harnessEntry ? join(fuzzRoot, "corpus", harnessEntry) : join(fuzzRoot, "corpus");
|
|
539
|
+
const result = finishWithCorpus(crashes, iterations, corpusDir, ctx);
|
|
540
|
+
if (executedHarness)
|
|
541
|
+
result.executedHarness = executedHarness;
|
|
542
|
+
return result;
|
|
543
|
+
}
|
|
544
|
+
async function runCLoop(opts, ctx) {
|
|
545
|
+
const crashes = [];
|
|
546
|
+
let iterations = 0;
|
|
547
|
+
if (!opts.tier2Artifact) {
|
|
548
|
+
ctx.log("[userspace-fuzz] C/C++ path requires a tier2Artifact (from buildTier2Harness); nothing to run");
|
|
549
|
+
return finish(crashes, iterations, resolve(opts.target.sourceRoot), ctx, "tier2");
|
|
550
|
+
}
|
|
551
|
+
if (!await clangAvailable()) {
|
|
552
|
+
ctx.toolingMissing.push("clang");
|
|
553
|
+
ctx.log(`[userspace-fuzz] ${TOOLING_HINTS.clang}`);
|
|
554
|
+
return finish(crashes, iterations, resolve(opts.target.sourceRoot), ctx, "tier2");
|
|
555
|
+
}
|
|
556
|
+
const { compile_command, run_command } = opts.tier2Artifact;
|
|
557
|
+
const sourceRoot = resolve(opts.target.sourceRoot);
|
|
558
|
+
ctx.log(`[userspace-fuzz] compiling Tier-2 harness: ${compile_command}`);
|
|
559
|
+
const compileRes = await runChild("sh", ["-c", compile_command], sourceRoot, ctx.timeoutMs);
|
|
560
|
+
if (compileRes.signalled || /error:|fatal error:/i.test(compileRes.output)) {
|
|
561
|
+
ctx.log(`[userspace-fuzz] Tier-2 harness failed to compile:
|
|
562
|
+
${compileRes.output.slice(-2e3)}`);
|
|
563
|
+
return finish(crashes, iterations, sourceRoot, ctx, "tier2");
|
|
564
|
+
}
|
|
565
|
+
ctx.log(`[userspace-fuzz] running Tier-2 harness: ${run_command}`);
|
|
566
|
+
const runRes = await runChild("sh", ["-c", run_command], sourceRoot, ctx.timeoutMs + 3e4);
|
|
567
|
+
iterations += 1;
|
|
568
|
+
const crash = parseCrashOutput(runRes.output, {
|
|
569
|
+
kind: runRes.timedOut ? "timeout" : runRes.signalled ? "segfault" : void 0
|
|
570
|
+
});
|
|
571
|
+
if (crash) {
|
|
572
|
+
crash.inputPath = findLibfuzzerCrashInput(sourceRoot, sourceRoot) ?? crash.inputPath;
|
|
573
|
+
crashes.push(crash);
|
|
574
|
+
}
|
|
575
|
+
return finishWithCorpus(crashes, iterations, sourceRoot, ctx);
|
|
576
|
+
}
|
|
577
|
+
function findLibfuzzerCrashInput(dir, sourceRoot) {
|
|
578
|
+
if (!isStableSourceDirectory(sourceRoot, dir))
|
|
579
|
+
return void 0;
|
|
580
|
+
try {
|
|
581
|
+
const hit = readdirSync(dir).find((name) => /^(crash|oom|timeout|leak)-[0-9a-f]+$/i.test(name));
|
|
582
|
+
if (!hit || !isStableSourceDirectory(sourceRoot, dir))
|
|
583
|
+
return void 0;
|
|
584
|
+
const candidate = join(dir, hit);
|
|
585
|
+
const listed = lstatSync(candidate);
|
|
586
|
+
return listed.isFile() && !listed.isSymbolicLink() ? candidate : void 0;
|
|
587
|
+
} catch {
|
|
588
|
+
return void 0;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function finish(crashes, iterations, corpusDir, ctx, _phase) {
|
|
592
|
+
return finishWithCorpus(crashes, iterations, corpusDir, ctx);
|
|
593
|
+
}
|
|
594
|
+
function finishWithCorpus(crashes, iterations, corpusDir, ctx) {
|
|
595
|
+
const deduped = dedupeBySignature(crashes);
|
|
596
|
+
const result = {
|
|
597
|
+
iterations,
|
|
598
|
+
crashes: deduped,
|
|
599
|
+
corpusSize: corpusCount(corpusDir),
|
|
600
|
+
durationMs: Date.now() - ctx.start
|
|
601
|
+
};
|
|
602
|
+
if (ctx.toolingMissing.length > 0) {
|
|
603
|
+
result.toolingMissing = Array.from(new Set(ctx.toolingMissing));
|
|
604
|
+
}
|
|
605
|
+
ctx.log(`[userspace-fuzz] done: iterations=${result.iterations} crashes=${result.crashes.length} corpus=${result.corpusSize}` + (result.toolingMissing ? ` toolingMissing=[${result.toolingMissing.join(",")}]` : ""));
|
|
606
|
+
return result;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// packages/core/dist/triage/userspace-primitive.js
|
|
610
|
+
var SEVERITY_ORDER = ["info", "low", "medium", "high", "critical"];
|
|
611
|
+
function severityRank(sev) {
|
|
612
|
+
return SEVERITY_ORDER.indexOf(sev);
|
|
613
|
+
}
|
|
614
|
+
function maxMemSeverity(a, b) {
|
|
615
|
+
return severityRank(a) >= severityRank(b) ? a : b;
|
|
616
|
+
}
|
|
617
|
+
function sniffMemPrimitive(raw) {
|
|
618
|
+
if (/heap-use-after-free|use-after-free|use of (deallocated|freed)/i.test(raw)) {
|
|
619
|
+
return "use-after-free";
|
|
620
|
+
}
|
|
621
|
+
if (/double-free|attempting double-free|free of (already-freed|untracked)/i.test(raw)) {
|
|
622
|
+
return "double-free";
|
|
623
|
+
}
|
|
624
|
+
if (/stack-buffer-overflow|stack-use-after-(return|scope)|stack-oob/i.test(raw)) {
|
|
625
|
+
return "stack-oob";
|
|
626
|
+
}
|
|
627
|
+
if (/heap-buffer-overflow|global-buffer-overflow|dynamic-stack-buffer-overflow/i.test(raw)) {
|
|
628
|
+
return /\bWRITE of size\b/i.test(raw) ? "heap-oob-write" : "heap-oob-read";
|
|
629
|
+
}
|
|
630
|
+
if (/uninitialized (memory|bytes|value)|use of uninitialized|msan|memorysanitizer|conjuring an instance/i.test(raw)) {
|
|
631
|
+
return "uninit-read";
|
|
632
|
+
}
|
|
633
|
+
if (/dangling (reference|pointer)|pointer to .* was dereferenced after/i.test(raw)) {
|
|
634
|
+
return "use-after-free";
|
|
635
|
+
}
|
|
636
|
+
if (/out-of-bounds|out of bounds|index out of bounds/i.test(raw)) {
|
|
637
|
+
return /\bWRITE of size\b|writing|store/i.test(raw) ? "heap-oob-write" : "heap-oob-read";
|
|
638
|
+
}
|
|
639
|
+
if (/signed integer overflow|unsigned integer overflow|integer overflow|shift .* out of range/i.test(raw)) {
|
|
640
|
+
return "integer-overflow";
|
|
641
|
+
}
|
|
642
|
+
if (/(member|downcast|wrong type|type mismatch|invalid (vtable|downcast)).*type|type confusion|misaligned/i.test(raw)) {
|
|
643
|
+
return "type-confusion";
|
|
644
|
+
}
|
|
645
|
+
if (/null pointer|null-deref|dereferenc(e|ing) (a )?null|SEGV on unknown address 0x0+\b/i.test(raw)) {
|
|
646
|
+
return "null-deref";
|
|
647
|
+
}
|
|
648
|
+
return void 0;
|
|
649
|
+
}
|
|
650
|
+
function primitiveFromKind(kind) {
|
|
651
|
+
switch (kind) {
|
|
652
|
+
case "segfault":
|
|
653
|
+
return "null-deref";
|
|
654
|
+
case "ubsan":
|
|
655
|
+
return "integer-overflow";
|
|
656
|
+
case "msan":
|
|
657
|
+
return "uninit-read";
|
|
658
|
+
default:
|
|
659
|
+
return "unknown";
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function sniffAccess(raw) {
|
|
663
|
+
if (/\bWRITE of size\b|\bwriting\b|\bstore\b|wild pointer write/i.test(raw))
|
|
664
|
+
return "write";
|
|
665
|
+
if (/\bREAD of size\b|\breading\b|\bload\b/i.test(raw))
|
|
666
|
+
return "read";
|
|
667
|
+
if (/\bexecute\b|\bPC\b.*invalid|control flow|return address/i.test(raw))
|
|
668
|
+
return "exec";
|
|
669
|
+
return "none";
|
|
670
|
+
}
|
|
671
|
+
function classifyUserspacePrimitive(crash) {
|
|
672
|
+
const raw = crash.rawOutput ?? "";
|
|
673
|
+
const primitive = crash.primitive ?? sniffMemPrimitive(raw) ?? primitiveFromKind(crash.kind);
|
|
674
|
+
const access = sniffAccess(raw);
|
|
675
|
+
let severity;
|
|
676
|
+
let controllable;
|
|
677
|
+
let readWrite;
|
|
678
|
+
let rationale;
|
|
679
|
+
switch (primitive) {
|
|
680
|
+
case "use-after-free": {
|
|
681
|
+
readWrite = access === "read" ? "read" : "write";
|
|
682
|
+
controllable = true;
|
|
683
|
+
severity = readWrite === "write" ? "critical" : "high";
|
|
684
|
+
rationale = `Use-after-free grants a dangling reference; a ${readWrite} after free is the lever \u2014 reclaim the freed allocation with a controlled object to turn it into ${readWrite === "write" ? "an object overwrite" : "an info-leak"}.`;
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
case "double-free": {
|
|
688
|
+
readWrite = "write";
|
|
689
|
+
controllable = true;
|
|
690
|
+
severity = "high";
|
|
691
|
+
rationale = "Double-free confuses the allocator freelist, enabling overlapping allocations and downstream object overwrite.";
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
case "heap-oob-write": {
|
|
695
|
+
readWrite = "write";
|
|
696
|
+
controllable = true;
|
|
697
|
+
severity = "critical";
|
|
698
|
+
rationale = "Heap out-of-bounds write corrupts an adjacent allocation \u2014 a write-what-where candidate once the neighbouring object is groomed.";
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
case "heap-oob-read": {
|
|
702
|
+
readWrite = "read";
|
|
703
|
+
controllable = /attacker|user-?controlled|tainted|index|offset|length/i.test(raw);
|
|
704
|
+
severity = "medium";
|
|
705
|
+
rationale = "Heap out-of-bounds read leaks adjacent allocation contents \u2014 an info-leak candidate useful for defeating ASLR.";
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
case "stack-oob": {
|
|
709
|
+
readWrite = access === "read" ? "read" : "write";
|
|
710
|
+
controllable = true;
|
|
711
|
+
severity = readWrite === "write" ? "high" : "medium";
|
|
712
|
+
rationale = "Stack out-of-bounds access; write variants can target saved registers / return addresses subject to stack-canary mitigation.";
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
case "type-confusion": {
|
|
716
|
+
readWrite = "write";
|
|
717
|
+
controllable = true;
|
|
718
|
+
severity = "high";
|
|
719
|
+
rationale = "Type confusion treats one object layout as another, enabling controlled reads/writes through mismatched fields (e.g. a fake vtable).";
|
|
720
|
+
break;
|
|
721
|
+
}
|
|
722
|
+
case "uninit-read": {
|
|
723
|
+
readWrite = "read";
|
|
724
|
+
controllable = false;
|
|
725
|
+
severity = "medium";
|
|
726
|
+
rationale = "Uninitialised read returns stale memory \u2014 an info-leak building block; exploitability depends on whether the leaked bytes reach an attacker.";
|
|
727
|
+
break;
|
|
728
|
+
}
|
|
729
|
+
case "integer-overflow": {
|
|
730
|
+
readWrite = "none";
|
|
731
|
+
controllable = false;
|
|
732
|
+
severity = "low";
|
|
733
|
+
rationale = "Integer overflow rarely yields a direct memory primitive on its own; treat as a building block (e.g. an undersized allocation) pending further analysis.";
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
case "null-deref": {
|
|
737
|
+
readWrite = "none";
|
|
738
|
+
controllable = false;
|
|
739
|
+
severity = "low";
|
|
740
|
+
rationale = "Null / faulting dereference is typically a denial-of-service in userspace unless a controlled offset reaches a mapped page.";
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
default: {
|
|
744
|
+
readWrite = access;
|
|
745
|
+
controllable = false;
|
|
746
|
+
severity = "info";
|
|
747
|
+
rationale = `Crash kind ${crash.kind} did not map to a known memory-safety primitive; treat as undetermined pending symbolisation.`;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
if ((crash.kind === "panic" || crash.kind === "timeout" || crash.kind === "oom") && primitive === "unknown") {
|
|
751
|
+
return {
|
|
752
|
+
primitive: "unknown",
|
|
753
|
+
severity: crash.kind === "panic" ? "low" : "info",
|
|
754
|
+
controllable: false,
|
|
755
|
+
readWrite: "none",
|
|
756
|
+
rationale: crash.kind === "panic" ? "Rust panic with no sanitizer signature \u2014 a safe-abort / availability bug, not a memory-corruption primitive." : `${crash.kind} with no corruption signature \u2014 an availability bug.`
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
return { primitive, severity, controllable, readWrite, rationale };
|
|
760
|
+
}
|
|
761
|
+
function describeExploitabilityVerdict(verdict) {
|
|
762
|
+
return [
|
|
763
|
+
`Primitive: ${verdict.primitive} (op=${verdict.readWrite})`,
|
|
764
|
+
`Severity: ${verdict.severity} (controllable=${verdict.controllable})`,
|
|
765
|
+
`- ${verdict.rationale}`
|
|
766
|
+
];
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// packages/core/dist/triage/pov-gate.js
|
|
770
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
771
|
+
import { promisify } from "node:util";
|
|
772
|
+
var execFileAsync = promisify(execFile2);
|
|
773
|
+
function oracleForCategory(category) {
|
|
774
|
+
switch (category) {
|
|
775
|
+
case "xss":
|
|
776
|
+
return "headless-browser";
|
|
777
|
+
case "ssrf":
|
|
778
|
+
case "command-injection":
|
|
779
|
+
case "code-injection":
|
|
780
|
+
return "oast-callback";
|
|
781
|
+
default:
|
|
782
|
+
return "regex-fallback";
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
function oastConfirmedPayload(finding, oracle) {
|
|
786
|
+
if (!oracle.verified)
|
|
787
|
+
return null;
|
|
788
|
+
if (oracleForCategory(finding.category) !== "oast-callback")
|
|
789
|
+
return null;
|
|
790
|
+
return {
|
|
791
|
+
findingId: finding.id,
|
|
792
|
+
category: finding.category,
|
|
793
|
+
oracle: "oast-callback",
|
|
794
|
+
hasPov: true,
|
|
795
|
+
reason: oracle.evidence || void 0
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
var POV_TOOLS = [
|
|
799
|
+
{
|
|
800
|
+
name: "bash",
|
|
801
|
+
description: "Execute a shell command (curl, python, etc.) to run the exploit. Use this to actually invoke your PoC and capture its output.",
|
|
802
|
+
input_schema: {
|
|
803
|
+
type: "object",
|
|
804
|
+
properties: {
|
|
805
|
+
command: { type: "string", description: "Shell command to run" }
|
|
806
|
+
},
|
|
807
|
+
required: ["command"]
|
|
808
|
+
}
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
name: "http_request",
|
|
812
|
+
description: "Send a raw HTTP request. Returns the response body/status. Useful when you want a typed request without shelling out to curl.",
|
|
813
|
+
input_schema: {
|
|
814
|
+
type: "object",
|
|
815
|
+
properties: {
|
|
816
|
+
url: { type: "string", description: "Target URL" },
|
|
817
|
+
method: { type: "string", description: "HTTP method (default GET)" },
|
|
818
|
+
body: { type: "string", description: "Request body" },
|
|
819
|
+
headers: {
|
|
820
|
+
type: "object",
|
|
821
|
+
description: "Additional headers as key-value pairs"
|
|
822
|
+
}
|
|
823
|
+
},
|
|
824
|
+
required: ["url"]
|
|
825
|
+
}
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
name: "submit_pov",
|
|
829
|
+
description: "Submit the final proof-of-vulnerability once you have confirmed the exploit works. Call this EXACTLY ONCE with the working artifact and the execution output that proves exploitation. Only call this after you have actually run the exploit and seen category-specific proof in the response.",
|
|
830
|
+
input_schema: {
|
|
831
|
+
type: "object",
|
|
832
|
+
properties: {
|
|
833
|
+
artifact_type: {
|
|
834
|
+
type: "string",
|
|
835
|
+
description: "Type of artifact",
|
|
836
|
+
enum: ["curl", "python", "javascript", "bash"]
|
|
837
|
+
},
|
|
838
|
+
artifact: {
|
|
839
|
+
type: "string",
|
|
840
|
+
description: "The concrete PoC (e.g. the full curl command or script source)."
|
|
841
|
+
},
|
|
842
|
+
execution_evidence: {
|
|
843
|
+
type: "string",
|
|
844
|
+
description: "The response / output from running the artifact, showing category-specific proof (extracted data, /etc/passwd, whoami, reflected script, etc.)."
|
|
845
|
+
}
|
|
846
|
+
},
|
|
847
|
+
required: ["artifact_type", "artifact", "execution_evidence"]
|
|
848
|
+
}
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
name: "give_up",
|
|
852
|
+
description: "Call this if you CANNOT produce a working exploit. Do NOT speculate or describe what would happen \u2014 if you cannot run it and see real proof, give up honestly.",
|
|
853
|
+
input_schema: {
|
|
854
|
+
type: "object",
|
|
855
|
+
properties: {
|
|
856
|
+
reason: { type: "string", description: "Why no PoV was possible" }
|
|
857
|
+
},
|
|
858
|
+
required: ["reason"]
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
];
|
|
862
|
+
var CATEGORY_JUDGES = {
|
|
863
|
+
"sql-injection": {
|
|
864
|
+
label: "SQLi: extracted DB data / error / version string",
|
|
865
|
+
patterns: [
|
|
866
|
+
/\b(mysql|mariadb|postgres|postgresql|sqlite|oracle)\b.*\bversion\b/i,
|
|
867
|
+
/\b\d+\.\d+\.\d+[-\w]*-?(mariadb|mysql|ubuntu|debian|log)\b/i,
|
|
868
|
+
/you have an error in your sql syntax/i,
|
|
869
|
+
/ORA-\d{5}/,
|
|
870
|
+
/pg_(sleep|version|database)/i,
|
|
871
|
+
/information_schema\.(tables|columns|schemata)/i,
|
|
872
|
+
/union\s+select/i,
|
|
873
|
+
/\bsqlite_version\(\)/i,
|
|
874
|
+
// Extracted user/hash row patterns
|
|
875
|
+
/['"]\s*,\s*['"][0-9a-f]{32,}['"]/
|
|
876
|
+
]
|
|
877
|
+
},
|
|
878
|
+
xss: {
|
|
879
|
+
label: "XSS: script reflected unencoded or dialog fired",
|
|
880
|
+
patterns: [
|
|
881
|
+
/<script[^>]*>[^<]*(alert|confirm|prompt)\s*\(/i,
|
|
882
|
+
/on(error|load|click|mouseover)\s*=\s*["']?\s*(alert|confirm|prompt)\s*\(/i,
|
|
883
|
+
/dialog\s+(fired|opened|appeared)/i,
|
|
884
|
+
/\balert\(1\)/i,
|
|
885
|
+
/javascript:alert/i
|
|
886
|
+
]
|
|
887
|
+
},
|
|
888
|
+
ssrf: {
|
|
889
|
+
label: "SSRF: internal IP/metadata endpoint responded",
|
|
890
|
+
patterns: [
|
|
891
|
+
/169\.254\.169\.254/,
|
|
892
|
+
/metadata\.google\.internal/i,
|
|
893
|
+
/instance-identity/i,
|
|
894
|
+
/iam\/security-credentials/i,
|
|
895
|
+
/127\.0\.0\.1|localhost/i,
|
|
896
|
+
/10\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
|
|
897
|
+
/192\.168\.\d{1,3}\.\d{1,3}/,
|
|
898
|
+
/172\.(1[6-9]|2[0-9]|3[01])\.\d{1,3}\.\d{1,3}/
|
|
899
|
+
]
|
|
900
|
+
},
|
|
901
|
+
"command-injection": {
|
|
902
|
+
label: "RCE: command output in response",
|
|
903
|
+
patterns: [
|
|
904
|
+
/uid=\d+\([^)]+\)\s+gid=\d+/,
|
|
905
|
+
/\broot:x:0:0:/,
|
|
906
|
+
/Linux\s+[\w.-]+\s+\d+\.\d+\.\d+/,
|
|
907
|
+
/^\/bin\/(bash|sh)/m,
|
|
908
|
+
/PATH=[^\n]{5,}/,
|
|
909
|
+
/^(usr|bin|etc|var|tmp|home|root)\b/m,
|
|
910
|
+
/\bwhoami\b\s*[\r\n]+\s*\w+/i
|
|
911
|
+
]
|
|
912
|
+
},
|
|
913
|
+
"code-injection": {
|
|
914
|
+
label: "Code injection: arbitrary code output in response",
|
|
915
|
+
patterns: [
|
|
916
|
+
/uid=\d+\([^)]+\)/,
|
|
917
|
+
/\broot:x:0:0:/,
|
|
918
|
+
/__import__|eval\(|exec\(/,
|
|
919
|
+
/\bpython\d?\b.*traceback/i
|
|
920
|
+
]
|
|
921
|
+
},
|
|
922
|
+
"path-traversal": {
|
|
923
|
+
label: "LFI: sensitive file contents in response",
|
|
924
|
+
patterns: [
|
|
925
|
+
/\broot:x:0:0:/,
|
|
926
|
+
/\bdaemon:x:\d+:\d+:/,
|
|
927
|
+
/\[boot loader\]/i,
|
|
928
|
+
/-----BEGIN (RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----/,
|
|
929
|
+
/\/etc\/shadow/
|
|
930
|
+
]
|
|
931
|
+
},
|
|
932
|
+
"information-disclosure": {
|
|
933
|
+
label: "IDOR / info disclosure: access to restricted data",
|
|
934
|
+
patterns: [
|
|
935
|
+
/"(email|password|ssn|credit_?card|api[_-]?key|token)"\s*:/i,
|
|
936
|
+
/\broot:x:0:0:/,
|
|
937
|
+
/-----BEGIN (RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----/,
|
|
938
|
+
/user\s*id\s*[:=]\s*\d+/i
|
|
939
|
+
]
|
|
940
|
+
},
|
|
941
|
+
"heap-overflow": {
|
|
942
|
+
label: "Heap overflow: KASAN out-of-bounds or crash evidence",
|
|
943
|
+
patterns: [
|
|
944
|
+
/BUG:\s*KASAN:\s*(slab-)?out-of-bounds/i,
|
|
945
|
+
/heap-buffer-overflow/i,
|
|
946
|
+
/\bKASAN\b.*\b(read|write)\b.*\bsize\s+\d+\b/i,
|
|
947
|
+
/allocated by task/i,
|
|
948
|
+
/Oops:.*\[#\d+\]/
|
|
949
|
+
]
|
|
950
|
+
},
|
|
951
|
+
"use-after-free": {
|
|
952
|
+
label: "UAF: KASAN use-after-free or freed-object access",
|
|
953
|
+
patterns: [
|
|
954
|
+
/BUG:\s*KASAN:\s*(slab-)?use-after-free/i,
|
|
955
|
+
/Freed by task/i,
|
|
956
|
+
/\buse-after-free\b/i,
|
|
957
|
+
/\bKASAN\b.*\bfreed\b/i,
|
|
958
|
+
/allocated by task.*\nfreed by task/is
|
|
959
|
+
]
|
|
960
|
+
},
|
|
961
|
+
"stack-buffer-overflow": {
|
|
962
|
+
label: "Stack overflow: KASAN stack-out-of-bounds",
|
|
963
|
+
patterns: [
|
|
964
|
+
/BUG:\s*KASAN:\s*stack-out-of-bounds/i,
|
|
965
|
+
/stack-buffer-overflow/i,
|
|
966
|
+
/\bKASAN\b.*\bstack\b/i
|
|
967
|
+
]
|
|
968
|
+
},
|
|
969
|
+
"null-pointer-deref": {
|
|
970
|
+
label: "Null deref: kernel NULL pointer dereference",
|
|
971
|
+
patterns: [
|
|
972
|
+
/BUG:\s*kernel NULL pointer dereference/i,
|
|
973
|
+
/unable to handle kernel NULL pointer/i,
|
|
974
|
+
/general protection fault.*0000/i,
|
|
975
|
+
/Oops:.*\[#\d+\]/,
|
|
976
|
+
/IP:.*\+0x/
|
|
977
|
+
]
|
|
978
|
+
},
|
|
979
|
+
"integer-overflow": {
|
|
980
|
+
label: "Integer overflow: UBSAN or arithmetic overflow",
|
|
981
|
+
patterns: [
|
|
982
|
+
/UBSAN:\s*(shift|integer|array)/i,
|
|
983
|
+
/signed integer overflow/i,
|
|
984
|
+
/unsigned integer overflow/i,
|
|
985
|
+
/shift.*out of range/i,
|
|
986
|
+
/division by zero/i
|
|
987
|
+
]
|
|
988
|
+
},
|
|
989
|
+
"race-condition": {
|
|
990
|
+
label: "Race: RCU stall or lock dependency violation",
|
|
991
|
+
patterns: [
|
|
992
|
+
/rcu.*stall/i,
|
|
993
|
+
/INFO:\s*possible circular locking/i,
|
|
994
|
+
/WARNING:.*lockdep/i,
|
|
995
|
+
/DEADLOCK/i,
|
|
996
|
+
/inconsistent lock state/i
|
|
997
|
+
]
|
|
998
|
+
},
|
|
999
|
+
"double-free": {
|
|
1000
|
+
label: "Double free: KASAN double-free detection",
|
|
1001
|
+
patterns: [
|
|
1002
|
+
/BUG:\s*KASAN:\s*double-free/i,
|
|
1003
|
+
/KASAN.*double.?free/i,
|
|
1004
|
+
/Object already free/i
|
|
1005
|
+
]
|
|
1006
|
+
},
|
|
1007
|
+
"type-confusion": {
|
|
1008
|
+
label: "Type confusion: invalid cast or type mismatch",
|
|
1009
|
+
patterns: [
|
|
1010
|
+
/UBSAN:\s*member access.*type/i,
|
|
1011
|
+
/type.*confusion/i,
|
|
1012
|
+
/invalid.*downcast/i,
|
|
1013
|
+
/load of misaligned address/i
|
|
1014
|
+
]
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
function judgePovEvidence(finding, executionEvidence) {
|
|
1018
|
+
const judge = CATEGORY_JUDGES[finding.category];
|
|
1019
|
+
if (judge) {
|
|
1020
|
+
for (const pat of judge.patterns) {
|
|
1021
|
+
const m = executionEvidence.match(pat);
|
|
1022
|
+
if (m) {
|
|
1023
|
+
return { passed: true, matchedPattern: m[0], label: judge.label };
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return { passed: false, label: judge.label };
|
|
1027
|
+
}
|
|
1028
|
+
const generic = [
|
|
1029
|
+
/uid=\d+\([^)]+\)/,
|
|
1030
|
+
/\broot:x:0:0:/,
|
|
1031
|
+
/flag\{[^}]+\}/i,
|
|
1032
|
+
/-----BEGIN (RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----/,
|
|
1033
|
+
/HTTP\/\d\.\d\s+200\s+OK[\s\S]{0,2000}password/i
|
|
1034
|
+
];
|
|
1035
|
+
for (const pat of generic) {
|
|
1036
|
+
const m = executionEvidence.match(pat);
|
|
1037
|
+
if (m) {
|
|
1038
|
+
return {
|
|
1039
|
+
passed: true,
|
|
1040
|
+
matchedPattern: m[0],
|
|
1041
|
+
label: "generic exploitation proof"
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
return { passed: false, label: "no category-specific proof" };
|
|
1046
|
+
}
|
|
1047
|
+
function buildPovSystemPrompt(finding, target, maxTurns) {
|
|
1048
|
+
return `You are the PoV (Proof-of-Vulnerability) agent.
|
|
1049
|
+
|
|
1050
|
+
You have ONE JOB: demonstrate that this finding is actually exploitable by
|
|
1051
|
+
RUNNING a working exploit against the target and capturing output that
|
|
1052
|
+
proves the vulnerability is real.
|
|
1053
|
+
|
|
1054
|
+
# Target
|
|
1055
|
+
${target}
|
|
1056
|
+
|
|
1057
|
+
# Finding under test
|
|
1058
|
+
- id: ${finding.id}
|
|
1059
|
+
- title: ${finding.title}
|
|
1060
|
+
- category: ${finding.category}
|
|
1061
|
+
- severity: ${finding.severity}
|
|
1062
|
+
|
|
1063
|
+
# Evidence from the original agent
|
|
1064
|
+
Request:
|
|
1065
|
+
${(finding.evidence.request ?? "").slice(0, 2e3)}
|
|
1066
|
+
|
|
1067
|
+
Response:
|
|
1068
|
+
${(finding.evidence.response ?? "").slice(0, 2e3)}
|
|
1069
|
+
|
|
1070
|
+
Analysis:
|
|
1071
|
+
${(finding.evidence.analysis ?? "").slice(0, 1e3)}
|
|
1072
|
+
|
|
1073
|
+
# Rules (these override any general instincts)
|
|
1074
|
+
1. You MUST produce a CONCRETE, EXECUTABLE exploit (curl/python/bash/js).
|
|
1075
|
+
2. You MUST actually RUN it via the \`bash\` or \`http_request\` tool and
|
|
1076
|
+
capture the response.
|
|
1077
|
+
3. The captured response MUST contain category-specific proof of
|
|
1078
|
+
exploitation (extracted data, /etc/passwd, uid=0, reflected <script>,
|
|
1079
|
+
169.254.169.254 metadata, etc.). A 200 OK alone is NOT proof.
|
|
1080
|
+
4. DO NOT speculate, DO NOT describe what would happen, DO NOT paraphrase.
|
|
1081
|
+
If you cannot run it and see real proof, call \`give_up\`.
|
|
1082
|
+
5. When you have working proof, call \`submit_pov\` ONCE with the exact
|
|
1083
|
+
artifact and the exact output.
|
|
1084
|
+
6. You have at most ${maxTurns} turns. Be decisive.
|
|
1085
|
+
|
|
1086
|
+
# Accepted proof by category
|
|
1087
|
+
- sql-injection: extracted rows, version strings, SQL error, union select data
|
|
1088
|
+
- xss: <script> reflected unencoded, or dialog fired
|
|
1089
|
+
- ssrf: 169.254.169.254 or internal network response
|
|
1090
|
+
- command-injection / code-injection: uid=, /etc/passwd, kernel version
|
|
1091
|
+
- path-traversal: /etc/passwd, shadow, private keys
|
|
1092
|
+
- information-disclosure: sensitive fields (password, api_key, tokens)
|
|
1093
|
+
|
|
1094
|
+
Start by writing the exploit and running it. No preamble.`;
|
|
1095
|
+
}
|
|
1096
|
+
var MAX_OUTPUT_CHARS = 8e3;
|
|
1097
|
+
var BASH_TIMEOUT_MS = 15e3;
|
|
1098
|
+
function clip(s) {
|
|
1099
|
+
if (s.length <= MAX_OUTPUT_CHARS)
|
|
1100
|
+
return s;
|
|
1101
|
+
return `${s.slice(0, MAX_OUTPUT_CHARS)}
|
|
1102
|
+
\u2026[truncated ${s.length - MAX_OUTPUT_CHARS} chars]`;
|
|
1103
|
+
}
|
|
1104
|
+
async function runBash(command) {
|
|
1105
|
+
try {
|
|
1106
|
+
const { stdout, stderr } = await execFileAsync("bash", ["-lc", command], {
|
|
1107
|
+
timeout: BASH_TIMEOUT_MS,
|
|
1108
|
+
maxBuffer: 2 * 1024 * 1024
|
|
1109
|
+
});
|
|
1110
|
+
return clip(`$ ${command}
|
|
1111
|
+
${stdout}${stderr ? `
|
|
1112
|
+
[stderr]
|
|
1113
|
+
${stderr}` : ""}`);
|
|
1114
|
+
} catch (err) {
|
|
1115
|
+
const e = err;
|
|
1116
|
+
return clip(`$ ${command}
|
|
1117
|
+
[error] ${e.message}
|
|
1118
|
+
${e.stdout ?? ""}${e.stderr ?? ""}`);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
async function runHttp(input) {
|
|
1122
|
+
const url = String(input.url ?? "");
|
|
1123
|
+
const method = String(input.method ?? "GET");
|
|
1124
|
+
const body = input.body !== void 0 ? String(input.body) : void 0;
|
|
1125
|
+
const headers = input.headers ?? {};
|
|
1126
|
+
if (!url)
|
|
1127
|
+
return "[error] url is required";
|
|
1128
|
+
try {
|
|
1129
|
+
const res = await fetch(url, {
|
|
1130
|
+
method,
|
|
1131
|
+
headers,
|
|
1132
|
+
body
|
|
1133
|
+
});
|
|
1134
|
+
const text = await res.text();
|
|
1135
|
+
return clip(`HTTP/${res.status} ${method} ${url}
|
|
1136
|
+
${text}`);
|
|
1137
|
+
} catch (err) {
|
|
1138
|
+
return clip(`[error] fetch ${url}: ${err.message}`);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
function isOracleInfraError(reason) {
|
|
1142
|
+
return /\b(failed|error|threw|exception|unavailable|timed? ?out)\b/i.test(reason);
|
|
1143
|
+
}
|
|
1144
|
+
async function generatePov(finding, target, runtime, maxTurns = 5, opts = {}) {
|
|
1145
|
+
const oracleKind = oracleForCategory(finding.category);
|
|
1146
|
+
const system = buildPovSystemPrompt(finding, target, maxTurns);
|
|
1147
|
+
const messages = [
|
|
1148
|
+
{
|
|
1149
|
+
role: "user",
|
|
1150
|
+
content: [
|
|
1151
|
+
{
|
|
1152
|
+
type: "text",
|
|
1153
|
+
text: "Build and run a working PoC for the finding above. Remember: real execution, real output, no speculation."
|
|
1154
|
+
}
|
|
1155
|
+
]
|
|
1156
|
+
}
|
|
1157
|
+
];
|
|
1158
|
+
const judge = opts.judge ?? judgePovEvidence;
|
|
1159
|
+
let submitted = null;
|
|
1160
|
+
let gaveUpReason = null;
|
|
1161
|
+
let turnsUsed = 0;
|
|
1162
|
+
for (let turn = 1; turn <= maxTurns; turn++) {
|
|
1163
|
+
turnsUsed = turn;
|
|
1164
|
+
const result = await runtime.executeNative(system, messages, POV_TOOLS);
|
|
1165
|
+
if (result.error) {
|
|
1166
|
+
return {
|
|
1167
|
+
hasPov: false,
|
|
1168
|
+
povArtifact: null,
|
|
1169
|
+
artifactType: "none",
|
|
1170
|
+
executionEvidence: "",
|
|
1171
|
+
confidence: 0,
|
|
1172
|
+
turnsUsed,
|
|
1173
|
+
reason: `runtime error: ${result.error}`,
|
|
1174
|
+
oracle: oracleKind
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
messages.push({ role: "assistant", content: result.content });
|
|
1178
|
+
const toolUseBlocks = result.content.filter((b) => b.type === "tool_use");
|
|
1179
|
+
if (toolUseBlocks.length === 0) {
|
|
1180
|
+
messages.push({
|
|
1181
|
+
role: "user",
|
|
1182
|
+
content: [
|
|
1183
|
+
{
|
|
1184
|
+
type: "text",
|
|
1185
|
+
text: "No tool call detected. Either RUN the exploit via `bash`/`http_request` and then call `submit_pov`, or call `give_up`. No speculation."
|
|
1186
|
+
}
|
|
1187
|
+
]
|
|
1188
|
+
});
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
const toolResults = [];
|
|
1192
|
+
for (const block of toolUseBlocks) {
|
|
1193
|
+
let output;
|
|
1194
|
+
switch (block.name) {
|
|
1195
|
+
case "bash": {
|
|
1196
|
+
const cmd = String(block.input.command ?? "");
|
|
1197
|
+
output = opts.disableBash ? `[bash disabled in test mode] would run: ${cmd}` : await runBash(cmd);
|
|
1198
|
+
break;
|
|
1199
|
+
}
|
|
1200
|
+
case "http_request": {
|
|
1201
|
+
output = opts.disableHttp ? `[http disabled in test mode] would fetch: ${JSON.stringify(block.input)}` : await runHttp(block.input);
|
|
1202
|
+
break;
|
|
1203
|
+
}
|
|
1204
|
+
case "submit_pov": {
|
|
1205
|
+
const art = String(block.input.artifact ?? "");
|
|
1206
|
+
const ev = String(block.input.execution_evidence ?? "");
|
|
1207
|
+
const at = String(block.input.artifact_type ?? "bash");
|
|
1208
|
+
submitted = {
|
|
1209
|
+
artifact: art,
|
|
1210
|
+
execution_evidence: ev,
|
|
1211
|
+
artifact_type: ["curl", "python", "javascript", "bash"].includes(at) ? at : "bash"
|
|
1212
|
+
};
|
|
1213
|
+
output = "submitted";
|
|
1214
|
+
break;
|
|
1215
|
+
}
|
|
1216
|
+
case "give_up": {
|
|
1217
|
+
gaveUpReason = String(block.input.reason ?? "agent gave up");
|
|
1218
|
+
output = "acknowledged";
|
|
1219
|
+
break;
|
|
1220
|
+
}
|
|
1221
|
+
default:
|
|
1222
|
+
output = `[error] unknown tool ${block.name}`;
|
|
1223
|
+
}
|
|
1224
|
+
toolResults.push({
|
|
1225
|
+
type: "tool_result",
|
|
1226
|
+
tool_use_id: block.id,
|
|
1227
|
+
content: output
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
messages.push({ role: "user", content: toolResults });
|
|
1231
|
+
if (submitted)
|
|
1232
|
+
break;
|
|
1233
|
+
if (gaveUpReason)
|
|
1234
|
+
break;
|
|
1235
|
+
}
|
|
1236
|
+
if (gaveUpReason) {
|
|
1237
|
+
return {
|
|
1238
|
+
hasPov: false,
|
|
1239
|
+
povArtifact: null,
|
|
1240
|
+
artifactType: "none",
|
|
1241
|
+
executionEvidence: "",
|
|
1242
|
+
confidence: 0,
|
|
1243
|
+
turnsUsed,
|
|
1244
|
+
reason: `agent gave up: ${gaveUpReason}`,
|
|
1245
|
+
oracle: oracleKind
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
if (!submitted) {
|
|
1249
|
+
return {
|
|
1250
|
+
hasPov: false,
|
|
1251
|
+
povArtifact: null,
|
|
1252
|
+
artifactType: "none",
|
|
1253
|
+
executionEvidence: "",
|
|
1254
|
+
confidence: 0,
|
|
1255
|
+
turnsUsed,
|
|
1256
|
+
reason: turnsUsed >= maxTurns ? `max turns (${maxTurns}) exceeded without a working PoC` : "agent exited without submitting a PoC",
|
|
1257
|
+
oracle: oracleKind
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
if (oracleKind !== "regex-fallback") {
|
|
1261
|
+
return adjudicateWithOracle(finding, target, submitted, turnsUsed, maxTurns, oracleKind, opts);
|
|
1262
|
+
}
|
|
1263
|
+
const verdict = judge(finding, submitted.execution_evidence);
|
|
1264
|
+
if (!verdict.passed) {
|
|
1265
|
+
return {
|
|
1266
|
+
hasPov: false,
|
|
1267
|
+
povArtifact: submitted.artifact || null,
|
|
1268
|
+
artifactType: submitted.artifact_type,
|
|
1269
|
+
executionEvidence: submitted.execution_evidence,
|
|
1270
|
+
confidence: 0.2,
|
|
1271
|
+
turnsUsed,
|
|
1272
|
+
reason: `submitted PoC did not contain category-specific proof (${verdict.label})`,
|
|
1273
|
+
oracle: "regex-fallback"
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
const confidence = Math.max(0.7, 1 - (turnsUsed - 1) * (0.3 / Math.max(1, maxTurns - 1)));
|
|
1277
|
+
return {
|
|
1278
|
+
hasPov: true,
|
|
1279
|
+
povArtifact: submitted.artifact,
|
|
1280
|
+
artifactType: submitted.artifact_type,
|
|
1281
|
+
executionEvidence: submitted.execution_evidence,
|
|
1282
|
+
confidence,
|
|
1283
|
+
turnsUsed,
|
|
1284
|
+
reason: `PoV confirmed: ${verdict.label}${verdict.matchedPattern ? ` (matched: ${verdict.matchedPattern.slice(0, 80)})` : ""}`,
|
|
1285
|
+
oracle: "regex-fallback"
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
async function adjudicateWithOracle(finding, target, submitted, turnsUsed, maxTurns, oracleKind, opts) {
|
|
1289
|
+
const runOracle = opts.oracle ?? verifyOracleByCategory;
|
|
1290
|
+
let oracleResult;
|
|
1291
|
+
try {
|
|
1292
|
+
oracleResult = opts.precomputedOracle ?? await runOracle(finding, target);
|
|
1293
|
+
} catch (err) {
|
|
1294
|
+
return {
|
|
1295
|
+
hasPov: false,
|
|
1296
|
+
povArtifact: submitted.artifact || null,
|
|
1297
|
+
artifactType: submitted.artifact_type,
|
|
1298
|
+
executionEvidence: submitted.execution_evidence,
|
|
1299
|
+
confidence: 0,
|
|
1300
|
+
turnsUsed,
|
|
1301
|
+
reason: `oracle (${oracleKind}) errored, inconclusive: ${err.message}`,
|
|
1302
|
+
oracle: oracleKind,
|
|
1303
|
+
inconclusive: true
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
if (oracleResult.verified) {
|
|
1307
|
+
const speed = Math.max(0.7, 1 - (turnsUsed - 1) * (0.3 / Math.max(1, maxTurns - 1)));
|
|
1308
|
+
return {
|
|
1309
|
+
hasPov: true,
|
|
1310
|
+
povArtifact: submitted.artifact,
|
|
1311
|
+
artifactType: submitted.artifact_type,
|
|
1312
|
+
// Prefer the oracle's reproduced evidence; keep the agent's as context.
|
|
1313
|
+
executionEvidence: oracleResult.evidence || submitted.execution_evidence,
|
|
1314
|
+
confidence: Math.min(speed, oracleResult.confidence || speed),
|
|
1315
|
+
turnsUsed,
|
|
1316
|
+
reason: `PoV confirmed by ${oracleKind} oracle: ${oracleResult.evidence}`,
|
|
1317
|
+
oracle: oracleKind
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
if (isOracleInfraError(oracleResult.reason)) {
|
|
1321
|
+
return {
|
|
1322
|
+
hasPov: false,
|
|
1323
|
+
povArtifact: submitted.artifact || null,
|
|
1324
|
+
artifactType: submitted.artifact_type,
|
|
1325
|
+
executionEvidence: submitted.execution_evidence,
|
|
1326
|
+
confidence: 0,
|
|
1327
|
+
turnsUsed,
|
|
1328
|
+
reason: `oracle (${oracleKind}) inconclusive: ${oracleResult.reason}`,
|
|
1329
|
+
oracle: oracleKind,
|
|
1330
|
+
inconclusive: true
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
return {
|
|
1334
|
+
hasPov: false,
|
|
1335
|
+
povArtifact: submitted.artifact || null,
|
|
1336
|
+
artifactType: submitted.artifact_type,
|
|
1337
|
+
executionEvidence: submitted.execution_evidence,
|
|
1338
|
+
confidence: 0,
|
|
1339
|
+
turnsUsed,
|
|
1340
|
+
reason: `${oracleKind} oracle did not reproduce the exploit: ${oracleResult.reason}`,
|
|
1341
|
+
oracle: oracleKind
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
var MEMCORRUPTION_CRASH_KINDS = /* @__PURE__ */ new Set([
|
|
1345
|
+
"asan",
|
|
1346
|
+
"ubsan",
|
|
1347
|
+
"msan",
|
|
1348
|
+
"miri",
|
|
1349
|
+
"segfault"
|
|
1350
|
+
]);
|
|
1351
|
+
function isReproducedMemCorruption(crash) {
|
|
1352
|
+
return Boolean(crash.inputPath) && MEMCORRUPTION_CRASH_KINDS.has(crash.kind) && Boolean(crash.rawOutput && crash.rawOutput.trim().length > 0);
|
|
1353
|
+
}
|
|
1354
|
+
function memCorruptionVerdict(crash) {
|
|
1355
|
+
const reproduced = isReproducedMemCorruption(crash);
|
|
1356
|
+
const exploit = classifyUserspacePrimitive(crash);
|
|
1357
|
+
if (!reproduced) {
|
|
1358
|
+
return {
|
|
1359
|
+
verdict: "inconclusive",
|
|
1360
|
+
confidence: 0,
|
|
1361
|
+
reasoning: `Crash artifact (kind=${crash.kind}) is not a reproduced memory-corruption PoC \u2014 ${crash.inputPath ? "no sanitizer/Miri corruption signature" : "no saved reproducing input"}. Inconclusive, not a rejection.`,
|
|
1362
|
+
signals: [
|
|
1363
|
+
{
|
|
1364
|
+
name: "memcorruption_repro",
|
|
1365
|
+
passed: false,
|
|
1366
|
+
confidence: 0,
|
|
1367
|
+
reasoning: `kind=${crash.kind}, primitive=${exploit.primitive}, savedInput=${Boolean(crash.inputPath)}`
|
|
1368
|
+
}
|
|
1369
|
+
]
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
const gate = reproConfidence(crash);
|
|
1373
|
+
return {
|
|
1374
|
+
verdict: "confirmed",
|
|
1375
|
+
confidence: gate.confidence,
|
|
1376
|
+
reasoning: `Reproduced under the sanitizer/Miri build (kind=${crash.kind}): ${exploit.primitive} (${exploit.readWrite}). ${exploit.rationale}${gate.note}`,
|
|
1377
|
+
signals: [
|
|
1378
|
+
{
|
|
1379
|
+
name: "memcorruption_repro",
|
|
1380
|
+
passed: true,
|
|
1381
|
+
confidence: gate.confidence,
|
|
1382
|
+
reasoning: `signature=${crash.signature}, input=${crash.inputPath}` + (crash.reproConfirmations != null ? `, repro=${crash.reproConfirmations}/${crash.reproAttempts ?? crash.reproConfirmations}` : "")
|
|
1383
|
+
}
|
|
1384
|
+
],
|
|
1385
|
+
evidenceKind: "reproduced-memcorruption-poc"
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
function reproConfidence(crash) {
|
|
1389
|
+
const n = crash.reproConfirmations;
|
|
1390
|
+
if (n == null)
|
|
1391
|
+
return { confidence: 0.95, note: "" };
|
|
1392
|
+
if (n >= 2) {
|
|
1393
|
+
const attempts = crash.reproAttempts ?? n;
|
|
1394
|
+
return { confidence: 0.95, note: ` Reproduced ${n}/${attempts}\xD7 (N\xD7 confirmed).` };
|
|
1395
|
+
}
|
|
1396
|
+
if (n === 1) {
|
|
1397
|
+
const attempts = crash.reproAttempts ?? 1;
|
|
1398
|
+
return {
|
|
1399
|
+
confidence: 0.82,
|
|
1400
|
+
note: attempts > 1 ? ` Reproduced only 1/${attempts}\xD7 \u2014 flaky-repro risk; re-run to confirm before relying on it.` : ` Single-shot reproduction (N=1) \u2014 re-run to rule out an environment fluke.`
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
return { confidence: 0.82, note: " Reproduction count is 0 \u2014 treat as unconfirmed flake." };
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// packages/core/dist/stages/memsafety-scan.js
|
|
1407
|
+
function memPrimitiveToCategory(primitive) {
|
|
1408
|
+
switch (primitive) {
|
|
1409
|
+
case "use-after-free":
|
|
1410
|
+
return "use-after-free";
|
|
1411
|
+
case "double-free":
|
|
1412
|
+
return "double-free";
|
|
1413
|
+
case "heap-oob-read":
|
|
1414
|
+
return "out-of-bounds-read";
|
|
1415
|
+
case "heap-oob-write":
|
|
1416
|
+
return "out-of-bounds-write";
|
|
1417
|
+
case "stack-oob":
|
|
1418
|
+
return "stack-buffer-overflow";
|
|
1419
|
+
case "type-confusion":
|
|
1420
|
+
return "type-confusion";
|
|
1421
|
+
case "uninit-read":
|
|
1422
|
+
return "uninitialized-memory";
|
|
1423
|
+
case "null-deref":
|
|
1424
|
+
return "null-deref";
|
|
1425
|
+
case "integer-overflow":
|
|
1426
|
+
return "integer-overflow";
|
|
1427
|
+
case "unknown":
|
|
1428
|
+
return "other";
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
function crashArtifactToFinding(crash, target, exploitability, verdict) {
|
|
1432
|
+
const severity = exploitability.severity;
|
|
1433
|
+
const category = memPrimitiveToCategory(exploitability.primitive);
|
|
1434
|
+
const stackSummary = (crash.stack ?? []).slice(0, 5).join(" \u2192 ");
|
|
1435
|
+
const reproducerRef = crash.artifactRef ?? crash.inputPath;
|
|
1436
|
+
const reproduced = verdict.verdict === "confirmed";
|
|
1437
|
+
const description = [
|
|
1438
|
+
`Userspace ${crash.kind} crash in ${target.language} target (${target.sourceRoot}).`,
|
|
1439
|
+
`Primitive: ${exploitability.primitive} (op=${exploitability.readWrite}, controllable=${exploitability.controllable}).`,
|
|
1440
|
+
stackSummary ? `Stack: ${stackSummary}.` : "",
|
|
1441
|
+
reproducerRef ? `Reproducing input: ${reproducerRef}.` : "",
|
|
1442
|
+
reproduced ? "Reproduced under the sanitizer/Miri build (memory-corruption PoC)." : "Observed crash; not yet a reproduced memory-corruption PoC (inconclusive \u2014 not a rejection)."
|
|
1443
|
+
].filter(Boolean).join("\n");
|
|
1444
|
+
const analysisLines = [
|
|
1445
|
+
`Crash kind: ${crash.kind}`,
|
|
1446
|
+
`Signature: ${crash.signature}`,
|
|
1447
|
+
`Category: ${category}`,
|
|
1448
|
+
`Build system: ${target.buildSystem}`,
|
|
1449
|
+
target.harnessEntry ? `Harness: ${target.harnessEntry}` : "",
|
|
1450
|
+
"",
|
|
1451
|
+
"---exploitability---",
|
|
1452
|
+
...describeExploitabilityVerdict(exploitability),
|
|
1453
|
+
"",
|
|
1454
|
+
"---verify verdict---",
|
|
1455
|
+
`Verdict: ${verdict.verdict} (confidence=${verdict.confidence})`,
|
|
1456
|
+
verdict.evidenceKind ? `Evidence: ${verdict.evidenceKind}` : "",
|
|
1457
|
+
verdict.reasoning
|
|
1458
|
+
].filter(Boolean);
|
|
1459
|
+
const confidence = reproduced ? verdict.confidence : 0.3;
|
|
1460
|
+
const rawOut = crash.rawOutput ?? "";
|
|
1461
|
+
return {
|
|
1462
|
+
id: randomUUID(),
|
|
1463
|
+
templateId: `memsafety-${crash.kind}`,
|
|
1464
|
+
title: `Userspace ${exploitability.primitive} (${crash.kind}) in ${target.sourceRoot}`,
|
|
1465
|
+
description,
|
|
1466
|
+
severity,
|
|
1467
|
+
category,
|
|
1468
|
+
status: "discovered",
|
|
1469
|
+
evidence: {
|
|
1470
|
+
request: reproducerRef ?? "N/A (userspace crash artifact)",
|
|
1471
|
+
response: rawOut.length > 4e3 ? rawOut.slice(0, 4e3) + "\n... [truncated]" : rawOut,
|
|
1472
|
+
analysis: analysisLines.join("\n")
|
|
1473
|
+
},
|
|
1474
|
+
fingerprint: `memsafety:${crash.signature}`,
|
|
1475
|
+
confidence,
|
|
1476
|
+
timestamp: Date.now()
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
async function runMemSafetyScan(opts) {
|
|
1480
|
+
const log = opts.logger ?? ((line) => console.log(line));
|
|
1481
|
+
const warnings = [];
|
|
1482
|
+
const playbookContext = buildPlaybookInjection(["rust_memsafety"]);
|
|
1483
|
+
log(`[memsafety] fuzzing ${opts.target.language} target at ${opts.target.sourceRoot}` + (opts.target.harnessEntry ? ` (harness=${opts.target.harnessEntry})` : ""));
|
|
1484
|
+
const loop = await runUserspaceFuzzLoop({
|
|
1485
|
+
...opts.fuzz,
|
|
1486
|
+
target: opts.target,
|
|
1487
|
+
logger: log
|
|
1488
|
+
});
|
|
1489
|
+
const toolingMissing = loop.toolingMissing ?? [];
|
|
1490
|
+
if (toolingMissing.length > 0) {
|
|
1491
|
+
const note = `[memsafety] fuzz loop could not complete \u2014 unavailable prerequisite: ${toolingMissing.join(", ")}. No findings emitted (no fabricated results).`;
|
|
1492
|
+
log(note);
|
|
1493
|
+
warnings.push(note);
|
|
1494
|
+
}
|
|
1495
|
+
const details = [];
|
|
1496
|
+
for (const crash of loop.crashes) {
|
|
1497
|
+
const exploitability = classifyUserspacePrimitive(crash);
|
|
1498
|
+
const verdict = memCorruptionVerdict(crash);
|
|
1499
|
+
const finding = crashArtifactToFinding(crash, opts.target, exploitability, verdict);
|
|
1500
|
+
details.push({ finding, crash, exploitability, verdict });
|
|
1501
|
+
}
|
|
1502
|
+
if (details.length === 0 && toolingMissing.length === 0) {
|
|
1503
|
+
log(`[memsafety] fuzz loop ran ${loop.iterations} iteration(s) with no crashes captured.`);
|
|
1504
|
+
}
|
|
1505
|
+
return {
|
|
1506
|
+
findings: details.map((d) => d.finding),
|
|
1507
|
+
details,
|
|
1508
|
+
loop,
|
|
1509
|
+
toolingMissing,
|
|
1510
|
+
playbookContext,
|
|
1511
|
+
warnings
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
export {
|
|
1516
|
+
parseCrashOutput,
|
|
1517
|
+
runUserspaceFuzzLoop,
|
|
1518
|
+
maxMemSeverity,
|
|
1519
|
+
sniffMemPrimitive,
|
|
1520
|
+
classifyUserspacePrimitive,
|
|
1521
|
+
describeExploitabilityVerdict,
|
|
1522
|
+
oracleForCategory,
|
|
1523
|
+
oastConfirmedPayload,
|
|
1524
|
+
judgePovEvidence,
|
|
1525
|
+
generatePov,
|
|
1526
|
+
isReproducedMemCorruption,
|
|
1527
|
+
memCorruptionVerdict,
|
|
1528
|
+
memPrimitiveToCategory,
|
|
1529
|
+
crashArtifactToFinding,
|
|
1530
|
+
runMemSafetyScan
|
|
1531
|
+
};
|