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,1422 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __0secCreateRequire } from "node:module";
|
|
3
|
+
const require = __0secCreateRequire(import.meta.url);
|
|
4
|
+
import {
|
|
5
|
+
homeStateDir
|
|
6
|
+
} from "./chunk-KKGE5RQE.js";
|
|
7
|
+
|
|
8
|
+
// packages/core/dist/triage/kernel-vm-runner.js
|
|
9
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
10
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { dirname, join, resolve } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { constants as fsConstants, copyFileSync, mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync, existsSync, realpathSync, statSync, chmodSync } from "node:fs";
|
|
15
|
+
|
|
16
|
+
// packages/core/dist/triage/bug-attribution.js
|
|
17
|
+
function normalizeModuleName(ref) {
|
|
18
|
+
const base = ref.replace(/^["']|["']$/g, "").split("/").pop() ?? ref;
|
|
19
|
+
return base.replace(/\.ko(\.(gz|xz|zst))?$/i, "").replace(/-/g, "_");
|
|
20
|
+
}
|
|
21
|
+
function detectOutOfBandModuleLoad(scanText, targetModules) {
|
|
22
|
+
const allowed = new Set(targetModules.map(normalizeModuleName));
|
|
23
|
+
const allowedList = [...allowed];
|
|
24
|
+
const re = /\binsmod\b\s+["']?([\w./+-]+)["']?|(?:^|[;\n]|&&|\|\||\||[$#]\s+|\b(?:system|popen)\s*\(\s*["'])\s*modprobe\b\s+["']?([\w./+-]+)["']?|\b(?:__NR_|SYS_|sys_)?(finit_module|init_module)\b/g;
|
|
25
|
+
for (let m = re.exec(scanText); m; m = re.exec(scanText)) {
|
|
26
|
+
if (m[3]) {
|
|
27
|
+
return { denied: true, method: m[3], offendingModule: m[3], allowed: allowedList };
|
|
28
|
+
}
|
|
29
|
+
const cmd = m[1] ? "insmod" : "modprobe";
|
|
30
|
+
const token = m[1] ?? m[2];
|
|
31
|
+
if (cmd === "insmod" && !/\.ko(\.(gz|xz|zst))?$/i.test(token) && !token.includes("/"))
|
|
32
|
+
continue;
|
|
33
|
+
if (!allowed.has(normalizeModuleName(token))) {
|
|
34
|
+
return { denied: true, method: cmd, offendingModule: token, allowed: allowedList };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { denied: false, allowed: allowedList };
|
|
38
|
+
}
|
|
39
|
+
function scanHardcodedKernelAddresses(exploitSource, runOutput, opts) {
|
|
40
|
+
const addresses = [
|
|
41
|
+
...new Set((exploitSource.match(/0x[0-9a-fA-F]{8,16}/g) ?? []).filter((lit) => {
|
|
42
|
+
const hex = lit.slice(2);
|
|
43
|
+
return hex.length >= 12 && /^ffff/i.test(hex);
|
|
44
|
+
}))
|
|
45
|
+
];
|
|
46
|
+
const flagged = addresses.length > 0;
|
|
47
|
+
const leakProvenance = /\bARB-READ\b|\bLEAK[:=]/i.test(runOutput) || /leak(?:ed|ing)?\b[^\n]*0x[0-9a-fA-F]{8,}/i.test(runOutput);
|
|
48
|
+
const refuse = flagged && !leakProvenance && opts.kaslrOn;
|
|
49
|
+
return {
|
|
50
|
+
flagged,
|
|
51
|
+
addresses,
|
|
52
|
+
leakProvenance,
|
|
53
|
+
verdict: refuse ? "refuse" : "allow",
|
|
54
|
+
reason: !flagged ? "no hardcoded kernel-address literal in exploit source" : leakProvenance ? "hardcoded kernel address is backed by a runtime leak marker" : refuse ? "KASLR-on claim with a hardcoded kernel address and no leak provenance \u2014 root not credited" : "hardcoded kernel address present but nokaslr climb \u2014 flagged, allowed"
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function assertBugAttribution(input) {
|
|
58
|
+
const scanText = `${input.exploitSource}
|
|
59
|
+
${input.runOutput}`;
|
|
60
|
+
const modLoad = detectOutOfBandModuleLoad(scanText, input.targetModules);
|
|
61
|
+
if (modLoad.denied) {
|
|
62
|
+
throw new Error(`bug-attribution guard: out-of-band kernel module load (${modLoad.method} ${modLoad.offendingModule ?? ""}) is not the sanctioned target module [${modLoad.allowed.join(", ") || "none"}] \u2014 a privilege credited off an attacker-supplied module is rejected`);
|
|
63
|
+
}
|
|
64
|
+
const addr = scanHardcodedKernelAddresses(input.exploitSource, input.runOutput, { kaslrOn: input.kaslrOn });
|
|
65
|
+
if (addr.verdict === "refuse") {
|
|
66
|
+
throw new Error(`bug-attribution guard: hardcoded kernel address without leak provenance under KASLR-on (${addr.addresses.join(", ")}) \u2014 root not credited`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// packages/core/dist/triage/kernel-vm-runner.js
|
|
71
|
+
var ATTESTATION_KEYS = ["schema", "nonce", "reproducer_sha256", "expected_kernel_release", "observed_kernel_release", "boot_id", "kernel_image_sha256", "kernel_config_sha256", "ruid", "euid", "suid", "rgid", "egid", "sgid", "groups", "cap_inh", "cap_prm", "cap_eff", "cap_amb", "securebits", "userns_max", "initial_userns", "no_new_privs"];
|
|
72
|
+
var RELEASE_RE = /^[A-Za-z0-9._+~-]{1,128}$/;
|
|
73
|
+
function parseKernelExecutionAttestation(raw) {
|
|
74
|
+
const fields = /* @__PURE__ */ new Map();
|
|
75
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
76
|
+
if (!line)
|
|
77
|
+
continue;
|
|
78
|
+
const match = /^([a-z0-9_]+)=([^\r\n]*)$/.exec(line);
|
|
79
|
+
if (!match || fields.has(match[1]))
|
|
80
|
+
throw new Error("malformed or duplicate kernel execution attestation field");
|
|
81
|
+
fields.set(match[1], match[2]);
|
|
82
|
+
}
|
|
83
|
+
if (fields.size !== ATTESTATION_KEYS.length || ATTESTATION_KEYS.some((key) => !fields.has(key)))
|
|
84
|
+
throw new Error("kernel execution attestation has missing or unknown fields");
|
|
85
|
+
const dec = (key) => {
|
|
86
|
+
const value = fields.get(key);
|
|
87
|
+
if (!/^(0|[1-9][0-9]{0,9})$/.test(value))
|
|
88
|
+
throw new Error(`invalid attestation ${key}`);
|
|
89
|
+
const parsed = Number(value);
|
|
90
|
+
if (!Number.isSafeInteger(parsed) || parsed > 4294967295)
|
|
91
|
+
throw new Error(`invalid attestation ${key}`);
|
|
92
|
+
return parsed;
|
|
93
|
+
};
|
|
94
|
+
const nonce = fields.get("nonce"), reproducerSha256 = fields.get("reproducer_sha256");
|
|
95
|
+
const expectedKernelRelease2 = fields.get("expected_kernel_release"), observedKernelRelease = fields.get("observed_kernel_release");
|
|
96
|
+
const bootId = fields.get("boot_id"), kernelImageSha256 = fields.get("kernel_image_sha256"), kernelConfigSha256 = fields.get("kernel_config_sha256");
|
|
97
|
+
const caps = ["cap_inh", "cap_prm", "cap_eff", "cap_amb"].map((key) => fields.get(key));
|
|
98
|
+
if (fields.get("schema") !== "2" || !/^[a-f0-9]{32}$/.test(nonce) || !/^[a-f0-9]{64}$/.test(reproducerSha256) || !RELEASE_RE.test(expectedKernelRelease2) || !RELEASE_RE.test(observedKernelRelease) || !/^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/.test(bootId) || !/^[a-f0-9]{64}$/.test(kernelImageSha256) || !/^[a-f0-9]{64}$/.test(kernelConfigSha256) || caps.some((cap) => !/^[a-f0-9]{16}$/.test(cap)))
|
|
99
|
+
throw new Error("invalid kernel execution attestation binding, provenance, or capability field");
|
|
100
|
+
const groups = fields.get("groups");
|
|
101
|
+
if (groups !== "" && !/^(0|[1-9][0-9]{0,9})(,(0|[1-9][0-9]{0,9}))*$/.test(groups))
|
|
102
|
+
throw new Error("invalid attestation groups");
|
|
103
|
+
const nnp = dec("no_new_privs");
|
|
104
|
+
if (nnp !== 0 && nnp !== 1)
|
|
105
|
+
throw new Error("invalid attestation no_new_privs");
|
|
106
|
+
const initial = dec("initial_userns");
|
|
107
|
+
if (initial !== 0 && initial !== 1)
|
|
108
|
+
throw new Error("invalid attestation initial_userns");
|
|
109
|
+
const receipt = { schemaVersion: 2, nonce, reproducerSha256, expectedKernelRelease: expectedKernelRelease2, observedKernelRelease, bootId, kernelImageSha256, kernelConfigSha256, realUid: dec("ruid"), effectiveUid: dec("euid"), savedUid: dec("suid"), realGid: dec("rgid"), effectiveGid: dec("egid"), savedGid: dec("sgid"), supplementaryGroups: groups ? groups.split(",").map(Number) : [], inheritableCapabilities: caps[0], permittedCapabilities: caps[1], effectiveCapabilities: caps[2], ambientCapabilities: caps[3], secureBits: dec("securebits"), userNamespaceMax: dec("userns_max"), initialUserNamespace: initial === 1, noNewPrivileges: nnp === 1 };
|
|
110
|
+
if (raw !== serializeKernelExecutionAttestation(receipt))
|
|
111
|
+
throw new Error("kernel execution attestation is not canonical");
|
|
112
|
+
return receipt;
|
|
113
|
+
}
|
|
114
|
+
function bindKernelExecutionAttestation(receipt, expected) {
|
|
115
|
+
if (expected.dropUid === void 0 !== (expected.dropGid === void 0))
|
|
116
|
+
throw new Error("kernel execution identity requires UID and GID together");
|
|
117
|
+
if (receipt.nonce !== expected.nonce || receipt.reproducerSha256 !== expected.reproducerSha256 || receipt.expectedKernelRelease !== expected.expectedKernelRelease || receipt.observedKernelRelease !== expected.expectedKernelRelease || receipt.kernelImageSha256 !== expected.kernelImageSha256 || receipt.kernelConfigSha256 !== expected.kernelConfigSha256)
|
|
118
|
+
throw new Error("kernel execution attestation binding or runtime kernel identity mismatch");
|
|
119
|
+
if (expected.dropUid !== void 0 && (receipt.realUid !== expected.dropUid || receipt.effectiveUid !== expected.dropUid || receipt.savedUid !== expected.dropUid || receipt.supplementaryGroups.length !== 0 || [receipt.inheritableCapabilities, receipt.permittedCapabilities, receipt.effectiveCapabilities, receipt.ambientCapabilities].some((cap) => cap !== "0000000000000000") || !receipt.noNewPrivileges || receipt.userNamespaceMax !== 0 || !receipt.initialUserNamespace))
|
|
120
|
+
throw new Error("kernel execution attestation did not prove requested UID/capability boundary");
|
|
121
|
+
if (expected.dropGid !== void 0 && (receipt.realGid !== expected.dropGid || receipt.effectiveGid !== expected.dropGid || receipt.savedGid !== expected.dropGid))
|
|
122
|
+
throw new Error("kernel execution attestation did not prove requested GID boundary");
|
|
123
|
+
}
|
|
124
|
+
function renderKernelExecutionLauncherSource() {
|
|
125
|
+
return String.raw`#define _GNU_SOURCE
|
|
126
|
+
#include <errno.h>
|
|
127
|
+
#include <fcntl.h>
|
|
128
|
+
#include <grp.h>
|
|
129
|
+
#include <sched.h>
|
|
130
|
+
#include <stdio.h>
|
|
131
|
+
#include <stdlib.h>
|
|
132
|
+
#include <string.h>
|
|
133
|
+
#include <sys/prctl.h>
|
|
134
|
+
#include <sys/stat.h>
|
|
135
|
+
#include <sys/types.h>
|
|
136
|
+
#include <sys/wait.h>
|
|
137
|
+
#include <unistd.h>
|
|
138
|
+
static int num(const char *s,unsigned *v){char *e=0;unsigned long n=strtoul(s,&e,10);if(!s[0]||*e||n>0xffffffffUL)return -1;*v=(unsigned)n;return 0;}
|
|
139
|
+
static int writezero(void){int f=open("/proc/sys/user/max_user_namespaces",O_WRONLY);if(f<0)return -1;if(write(f,"0\n",2)!=2){close(f);return -1;}return close(f);}
|
|
140
|
+
int main(int argc,char **argv){
|
|
141
|
+
if(argc<11)return 125; unsigned uid=0,gid=0; int du=strcmp(argv[7],"-")!=0,dg=strcmp(argv[8],"-")!=0; if(du!=dg)return 125;
|
|
142
|
+
if((du&&num(argv[7],&uid))||(dg&&num(argv[8],&gid)))return 125;
|
|
143
|
+
int receipt=open(argv[1],O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW,0400); if(receipt<0)return 126;int initns=open("/proc/1/ns/user",O_RDONLY|O_CLOEXEC);
|
|
144
|
+
if(du&&writezero())return 126;
|
|
145
|
+
int p[2]; if(pipe2(p,O_CLOEXEC))return 126; pid_t child=fork(); if(child<0)return 126;
|
|
146
|
+
if(child){close(receipt);if(initns>=0)close(initns);close(p[1]);char x;ssize_t n=read(p[0],&x,1);close(p[0]);if(n!=0){waitpid(child,0,0);return 127;}int m=open(argv[9],O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW,0400);if(m<0){kill(child,SIGKILL);waitpid(child,0,0);return 126;}write(m,"1\n",2);close(m);int st;if(waitpid(child,&st,0)<0)return 126;if(WIFEXITED(st))return WEXITSTATUS(st);return 128+WTERMSIG(st);}
|
|
147
|
+
close(p[0]); if(dg&&(setgroups(0,NULL)||setresgid(gid,gid,gid)))return 126;if(du&&setresuid(uid,uid,uid))return 126;if(du&&prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0))return 126;
|
|
148
|
+
uid_t r,e,s;gid_t gr,ge,gs;if(getresuid(&r,&e,&s)||getresgid(&gr,&ge,&gs))return 126;gid_t gl[64];int ng=getgroups(64,gl);if(ng<0)return 126;
|
|
149
|
+
FILE *st=fopen("/proc/self/status","r");char line[256],ci[17]="",cp[17]="",ce[17]="",ca[17]="";int nnp=-1;if(!st)return 126;while(fgets(line,sizeof line,st)){sscanf(line,"CapInh: %16[0-9a-fA-F]",ci);sscanf(line,"CapPrm: %16[0-9a-fA-F]",cp);sscanf(line,"CapEff: %16[0-9a-fA-F]",ce);sscanf(line,"CapAmb: %16[0-9a-fA-F]",ca);sscanf(line,"NoNewPrivs: %d",&nnp);}fclose(st);
|
|
150
|
+
char *caps[]={ci,cp,ce,ca};for(int i=0;i<4;i++){if(strlen(caps[i])!=16)return 126;for(char *q=caps[i];*q;q++)if(*q>='A'&&*q<='F')*q+=32;}
|
|
151
|
+
struct stat ns,ns1;int initial=initns>=0&&!stat("/proc/self/ns/user",&ns)&&!fstat(initns,&ns1)&&ns.st_dev==ns1.st_dev&&ns.st_ino==ns1.st_ino;if(initns>=0)close(initns);FILE *um=fopen("/proc/sys/user/max_user_namespaces","r");unsigned umax;if(!um||fscanf(um,"%u",&umax)!=1)return 126;fclose(um);int sb=prctl(PR_GET_SECUREBITS);if(sb<0||nnp<0)return 126;
|
|
152
|
+
char rel[129],boot[37];FILE *ur=fopen("/proc/sys/kernel/osrelease","r"),*bi=fopen("/proc/sys/kernel/random/boot_id","r");if(!ur||!bi||!fgets(rel,sizeof rel,ur)||!fgets(boot,sizeof boot,bi))return 126;fclose(ur);fclose(bi);rel[strcspn(rel,"\r\n")]=0;boot[strcspn(boot,"\r\n")]=0;
|
|
153
|
+
FILE *o=fdopen(receipt,"w");if(!o)return 126;fprintf(o,"schema=2\nnonce=%s\nreproducer_sha256=%s\nexpected_kernel_release=%s\nobserved_kernel_release=%s\nboot_id=%s\nkernel_image_sha256=%s\nkernel_config_sha256=%s\nruid=%u\neuid=%u\nsuid=%u\nrgid=%u\negid=%u\nsgid=%u\ngroups=",argv[2],argv[3],argv[4],rel,boot,argv[5],argv[6],r,e,s,gr,ge,gs);for(int i=0;i<ng;i++)fprintf(o,"%s%u",i?",":"",gl[i]);fprintf(o,"\ncap_inh=%s\ncap_prm=%s\ncap_eff=%s\ncap_amb=%s\nsecurebits=%d\nuserns_max=%u\ninitial_userns=%d\nno_new_privs=%d\n",ci,cp,ce,ca,sb,umax,initial,nnp);if(fclose(o))return 126;
|
|
154
|
+
execvp(argv[10],&argv[10]);char bad='x';write(p[1],&bad,1);return 127;
|
|
155
|
+
}`;
|
|
156
|
+
}
|
|
157
|
+
var RECOGNIZED_CONFIG_PROFILES = ["kasan", "kcsan", "plain"];
|
|
158
|
+
function buildFlagsForProfile(profile) {
|
|
159
|
+
if (profile === "kasan") {
|
|
160
|
+
return [
|
|
161
|
+
"--enable CONFIG_KASAN",
|
|
162
|
+
"--set-str CONFIG_KASAN_MODE generic",
|
|
163
|
+
"--enable CONFIG_KASAN_GENERIC",
|
|
164
|
+
"--enable CONFIG_KASAN_INLINE",
|
|
165
|
+
"--enable CONFIG_KASAN_STACK",
|
|
166
|
+
"--enable CONFIG_KASAN_VMALLOC",
|
|
167
|
+
"--enable CONFIG_UBSAN",
|
|
168
|
+
"--enable CONFIG_UBSAN_BOUNDS",
|
|
169
|
+
"--enable CONFIG_UBSAN_SHIFT"
|
|
170
|
+
];
|
|
171
|
+
}
|
|
172
|
+
if (profile === "plain") {
|
|
173
|
+
return ["--disable CONFIG_KASAN", "--disable CONFIG_KCSAN", "--enable CONFIG_DEBUG_INFO"];
|
|
174
|
+
}
|
|
175
|
+
if (profile === "kcsan") {
|
|
176
|
+
return [
|
|
177
|
+
"--enable CONFIG_KCSAN",
|
|
178
|
+
"--enable CONFIG_KCSAN_EARLY_ENABLE",
|
|
179
|
+
"--set-val CONFIG_KCSAN_REPORT_ONCE_IN_MS 0",
|
|
180
|
+
"--enable CONFIG_KCSAN_INTERRUPT_WATCHER",
|
|
181
|
+
"--enable CONFIG_PREEMPT",
|
|
182
|
+
"--disable CONFIG_KASAN",
|
|
183
|
+
"--enable CONFIG_DEBUG_INFO",
|
|
184
|
+
"--enable CONFIG_DEBUG_KERNEL"
|
|
185
|
+
];
|
|
186
|
+
}
|
|
187
|
+
throw new Error(`unrecognized kernel config profile: ${profile} (known: ${RECOGNIZED_CONFIG_PROFILES.join(", ")})`);
|
|
188
|
+
}
|
|
189
|
+
function kcsanConfigSupported(kernelConfigText) {
|
|
190
|
+
return /^CONFIG_KCSAN=y\s*$/m.test(kernelConfigText);
|
|
191
|
+
}
|
|
192
|
+
function shellQuote(value) {
|
|
193
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
194
|
+
}
|
|
195
|
+
function inferDiskFormat(diskImage) {
|
|
196
|
+
return diskImage.endsWith(".qcow2") || diskImage.endsWith(".qcow") ? "qcow2" : "raw";
|
|
197
|
+
}
|
|
198
|
+
function defaultKernelCacheDir() {
|
|
199
|
+
return process.env["0SEC_KERNEL_BUILD_CACHE"]?.trim() || join(homeStateDir(), "kernel-cache");
|
|
200
|
+
}
|
|
201
|
+
function kernelTreeFingerprint(kernelTree) {
|
|
202
|
+
try {
|
|
203
|
+
const described = execFileSync("git", ["describe", "--tags", "--always", "--dirty"], {
|
|
204
|
+
cwd: kernelTree,
|
|
205
|
+
encoding: "utf8",
|
|
206
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
207
|
+
}).trim();
|
|
208
|
+
if (described)
|
|
209
|
+
return described;
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
return execFileSync("git", ["rev-parse", "HEAD"], {
|
|
214
|
+
cwd: kernelTree,
|
|
215
|
+
encoding: "utf8",
|
|
216
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
217
|
+
}).trim();
|
|
218
|
+
} catch {
|
|
219
|
+
const stat = statSync(kernelTree);
|
|
220
|
+
return `${realpathSync(kernelTree)}:${Math.trunc(stat.mtimeMs)}`;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function sanitizeForPath(value) {
|
|
224
|
+
return value.replace(/[^A-Za-z0-9._-]+/g, "_").slice(0, 64) || "rev";
|
|
225
|
+
}
|
|
226
|
+
function configNameHash(configProfile) {
|
|
227
|
+
return createHash("sha256").update(configProfile).digest("hex").slice(0, 12);
|
|
228
|
+
}
|
|
229
|
+
function sha256File(path) {
|
|
230
|
+
if (!path || !existsSync(path))
|
|
231
|
+
throw new Error(`kernel provenance artifact not found: ${path || "<unset>"}`);
|
|
232
|
+
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
233
|
+
}
|
|
234
|
+
function expectedKernelRelease(kernelTree, explicit, requireExplicit = false) {
|
|
235
|
+
const fromEnv = explicit?.trim() || process.env["0SEC_KERNEL_QEMU_EXPECTED_RELEASE"]?.trim();
|
|
236
|
+
if (fromEnv) {
|
|
237
|
+
if (!RELEASE_RE.test(fromEnv))
|
|
238
|
+
throw new Error("invalid expected kernel release");
|
|
239
|
+
return fromEnv;
|
|
240
|
+
}
|
|
241
|
+
if (requireExplicit)
|
|
242
|
+
throw new Error("env-provided kernel artifacts require an explicit expected kernel release");
|
|
243
|
+
try {
|
|
244
|
+
const release2 = execFileSync("make", ["-s", "kernelrelease"], { cwd: kernelTree, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
245
|
+
if (RELEASE_RE.test(release2))
|
|
246
|
+
return release2;
|
|
247
|
+
} catch {
|
|
248
|
+
}
|
|
249
|
+
const makefile = readFileSync(join(kernelTree, "Makefile"), "utf8");
|
|
250
|
+
const part = (name) => new RegExp(`^${name}\\s*=\\s*([^\\s#]+)`, "m").exec(makefile)?.[1];
|
|
251
|
+
const version = part("VERSION"), patch = part("PATCHLEVEL"), sub = part("SUBLEVEL") ?? "0", extra = part("EXTRAVERSION") ?? "";
|
|
252
|
+
const release = version && patch ? `${version}.${patch}.${sub}${extra}` : "";
|
|
253
|
+
if (!RELEASE_RE.test(release))
|
|
254
|
+
throw new Error("cannot determine expected kernel release; set 0SEC_KERNEL_QEMU_EXPECTED_RELEASE");
|
|
255
|
+
return release;
|
|
256
|
+
}
|
|
257
|
+
function kernelBuildCacheKey(kernelTree, configProfile) {
|
|
258
|
+
const fingerprint = kernelTreeFingerprint(kernelTree);
|
|
259
|
+
const revPart = fingerprint.includes(":") ? createHash("sha256").update(fingerprint).digest("hex").slice(0, 16) : sanitizeForPath(fingerprint);
|
|
260
|
+
return `${revPart}-${configNameHash(configProfile)}`;
|
|
261
|
+
}
|
|
262
|
+
function artifactsExist(outDir) {
|
|
263
|
+
return existsSync(join(outDir, "bzImage")) && existsSync(join(outDir, "rootfs.img")) && existsSync(join(outDir, "kernel.config"));
|
|
264
|
+
}
|
|
265
|
+
function defaultBuildRunner(input) {
|
|
266
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
267
|
+
const script = [
|
|
268
|
+
join(here, "kernel-vm", "build-from-tree.sh"),
|
|
269
|
+
join(process.cwd(), "packages/core/src/triage/kernel-vm/build-from-tree.sh"),
|
|
270
|
+
join(process.cwd(), "src/triage/kernel-vm/build-from-tree.sh")
|
|
271
|
+
].find((candidate) => existsSync(candidate));
|
|
272
|
+
if (!script) {
|
|
273
|
+
throw new Error("kernel build script not found; set 0SEC_KERNEL_QEMU_KERNEL/0SEC_KERNEL_QEMU_DISK to prebuilt artifacts or run from a source checkout");
|
|
274
|
+
}
|
|
275
|
+
execFileSync("bash", [script, input.kernelTree, input.outDir, input.configProfile], {
|
|
276
|
+
stdio: "inherit"
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
function prepareKernelVmArtifacts(opts) {
|
|
280
|
+
const configProfile = opts.configProfile ?? "kasan";
|
|
281
|
+
const log = opts.logger ?? ((line) => console.log(line));
|
|
282
|
+
const envKernel = process.env["0SEC_KERNEL_QEMU_KERNEL"]?.trim();
|
|
283
|
+
const envDisk = process.env["0SEC_KERNEL_QEMU_DISK"]?.trim();
|
|
284
|
+
if (!opts.force && envKernel && envDisk && existsSync(envKernel) && existsSync(envDisk)) {
|
|
285
|
+
log(`[kernel-cache] env-override: using 0SEC_KERNEL_QEMU_KERNEL/DISK (skipping build)`);
|
|
286
|
+
return {
|
|
287
|
+
kernelImage: envKernel,
|
|
288
|
+
diskImage: envDisk,
|
|
289
|
+
kernelConfig: process.env["0SEC_KERNEL_QEMU_CONFIG"]?.trim() || "",
|
|
290
|
+
cacheKey: "env",
|
|
291
|
+
cacheDir: "",
|
|
292
|
+
cacheStatus: "env",
|
|
293
|
+
configProfile
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const kernelTree = realpathSync(resolve(opts.kernelTree));
|
|
297
|
+
const cacheRoot = resolve(opts.cacheDir ?? defaultKernelCacheDir());
|
|
298
|
+
const cacheKey = kernelBuildCacheKey(kernelTree, configProfile);
|
|
299
|
+
const outDir = join(cacheRoot, cacheKey);
|
|
300
|
+
mkdirSync(outDir, { recursive: true });
|
|
301
|
+
if (!opts.force && artifactsExist(outDir)) {
|
|
302
|
+
log(`[kernel-cache] hit: ${outDir} (config=${configProfile})`);
|
|
303
|
+
warnIfKcsanUnsupported(configProfile, join(outDir, "kernel.config"), log);
|
|
304
|
+
return {
|
|
305
|
+
kernelImage: join(outDir, "bzImage"),
|
|
306
|
+
diskImage: join(outDir, "rootfs.img"),
|
|
307
|
+
kernelConfig: join(outDir, "kernel.config"),
|
|
308
|
+
cacheKey,
|
|
309
|
+
cacheDir: outDir,
|
|
310
|
+
cacheStatus: "hit",
|
|
311
|
+
configProfile
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
log(`[kernel-cache] miss: building into ${outDir} (config=${configProfile})`);
|
|
315
|
+
const runner = opts.buildRunner ?? defaultBuildRunner;
|
|
316
|
+
runner({ kernelTree, outDir, configProfile });
|
|
317
|
+
if (!artifactsExist(outDir)) {
|
|
318
|
+
throw new Error(`kernel build did not produce bzImage/rootfs.img/kernel.config in ${outDir}`);
|
|
319
|
+
}
|
|
320
|
+
warnIfKcsanUnsupported(configProfile, join(outDir, "kernel.config"), log);
|
|
321
|
+
return {
|
|
322
|
+
kernelImage: join(outDir, "bzImage"),
|
|
323
|
+
diskImage: join(outDir, "rootfs.img"),
|
|
324
|
+
kernelConfig: join(outDir, "kernel.config"),
|
|
325
|
+
cacheKey,
|
|
326
|
+
cacheDir: outDir,
|
|
327
|
+
cacheStatus: "miss",
|
|
328
|
+
configProfile
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function warnIfKcsanUnsupported(configProfile, kernelConfigPath, log) {
|
|
332
|
+
if (configProfile !== "kcsan")
|
|
333
|
+
return;
|
|
334
|
+
let text = "";
|
|
335
|
+
try {
|
|
336
|
+
text = readFileSync(kernelConfigPath, "utf-8");
|
|
337
|
+
} catch {
|
|
338
|
+
log(`[kcsan-gate] WARN: could not read ${kernelConfigPath} to verify CONFIG_KCSAN \u2014 race lane may be blind`);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (!kcsanConfigSupported(text)) {
|
|
342
|
+
log(`[kcsan-gate] WARN: CONFIG_KCSAN not enabled in ${kernelConfigPath} \u2014 data races will NOT be observed (fail-soft)`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function loadKernelVmConfigFromEnv() {
|
|
346
|
+
const kernelImage = process.env["0SEC_KERNEL_QEMU_KERNEL"]?.trim();
|
|
347
|
+
const diskImage = process.env["0SEC_KERNEL_QEMU_DISK"]?.trim();
|
|
348
|
+
const missing = [
|
|
349
|
+
!kernelImage ? "0SEC_KERNEL_QEMU_KERNEL" : "",
|
|
350
|
+
!diskImage ? "0SEC_KERNEL_QEMU_DISK" : ""
|
|
351
|
+
].filter(Boolean);
|
|
352
|
+
if (missing.length > 0) {
|
|
353
|
+
throw new Error(`kernel VM runner is enabled but missing required env vars: ${missing.join(", ")}`);
|
|
354
|
+
}
|
|
355
|
+
const resolvedKernelImage = kernelImage;
|
|
356
|
+
const resolvedDiskImage = diskImage;
|
|
357
|
+
const kaslr = /^(1|true|on|yes)$/i.test(process.env["0SEC_KERNEL_QEMU_KASLR"]?.trim() ?? "");
|
|
358
|
+
const explicitAppend = process.env["0SEC_KERNEL_QEMU_APPEND"]?.trim();
|
|
359
|
+
const kernelAppend = explicitAppend || buildKernelAppend(kaslr);
|
|
360
|
+
const widenSymbol = process.env["0SEC_KERNEL_QEMU_WIDEN_SYMBOL"]?.trim() || void 0;
|
|
361
|
+
const widenOffsetRaw = process.env["0SEC_KERNEL_QEMU_WIDEN_OFFSET"]?.trim();
|
|
362
|
+
const widenDelayRaw = process.env["0SEC_KERNEL_QEMU_WIDEN_DELAY_MS"]?.trim();
|
|
363
|
+
const weaponizeInitramfs = /^(1|true|on|yes)$/i.test(process.env["0SEC_KERNEL_QEMU_INITRAMFS"]?.trim() ?? "") || /^(1|true|on|yes)$/i.test(process.env.USE_KERNEL_WEAPONIZE?.trim() ?? "");
|
|
364
|
+
const initramfsModules = (process.env["0SEC_KERNEL_QEMU_INITRAMFS_MODULES"]?.trim() || "").split(/[:,\s]+/).map((m) => m.trim()).filter(Boolean);
|
|
365
|
+
const busyboxPath = process.env["0SEC_KERNEL_QEMU_BUSYBOX"]?.trim() || void 0;
|
|
366
|
+
return {
|
|
367
|
+
qemuBinary: process.env["0SEC_KERNEL_QEMU_BINARY"]?.trim() || "qemu-system-x86_64",
|
|
368
|
+
kernelImage: resolvedKernelImage,
|
|
369
|
+
diskImage: resolvedDiskImage,
|
|
370
|
+
diskFormat: process.env["0SEC_KERNEL_QEMU_DISK_FORMAT"]?.trim() || inferDiskFormat(resolvedDiskImage),
|
|
371
|
+
bootTimeoutSec: parseInt(process.env["0SEC_KERNEL_QEMU_BOOT_TIMEOUT_SEC"]?.trim() || "120", 10),
|
|
372
|
+
memoryMb: parseInt(process.env["0SEC_KERNEL_QEMU_MEMORY_MB"]?.trim() || "2048", 10),
|
|
373
|
+
smp: parseInt(process.env["0SEC_KERNEL_QEMU_SMP"]?.trim() || "2", 10),
|
|
374
|
+
kernelAppend,
|
|
375
|
+
qemuAccel: process.env["0SEC_KERNEL_QEMU_ACCEL"]?.trim() || void 0,
|
|
376
|
+
initrdPath: process.env["0SEC_KERNEL_QEMU_INITRD"]?.trim() || void 0,
|
|
377
|
+
timeoutSec: parseInt(process.env["0SEC_KERNEL_QEMU_TIMEOUT_SEC"]?.trim() || "60", 10),
|
|
378
|
+
shareTag: process.env["0SEC_KERNEL_QEMU_SHARE_TAG"]?.trim() || "osecshare",
|
|
379
|
+
artifactDir: process.env["0SEC_KERNEL_QEMU_ARTIFACT_DIR"]?.trim() || void 0,
|
|
380
|
+
kaslr,
|
|
381
|
+
...widenSymbol ? { widenSymbol } : {},
|
|
382
|
+
...widenOffsetRaw && Number.isFinite(parseInt(widenOffsetRaw, 16)) ? { widenOffset: parseInt(widenOffsetRaw, 16) } : {},
|
|
383
|
+
...widenDelayRaw && Number.isFinite(parseInt(widenDelayRaw, 10)) ? { widenDelayMs: parseInt(widenDelayRaw, 10) } : {},
|
|
384
|
+
...process.env["0SEC_KERNEL_QEMU_GUEST_BUILD_DIR"]?.trim() ? { guestKernelBuildDir: process.env["0SEC_KERNEL_QEMU_GUEST_BUILD_DIR"].trim() } : {},
|
|
385
|
+
weaponizeInitramfs,
|
|
386
|
+
...initramfsModules.length > 0 ? { initramfsModules } : {},
|
|
387
|
+
...busyboxPath ? { busyboxPath } : {}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function buildKernelAppend(kaslr) {
|
|
391
|
+
const base = "console=ttyS0 root=/dev/vda rw";
|
|
392
|
+
const tail = "panic=-1 init=/sbin/0sec-init";
|
|
393
|
+
return `${base} ${kaslr ? "kaslr" : "nokaslr"} ${tail}`;
|
|
394
|
+
}
|
|
395
|
+
function renderRaceWidenModuleSource(symbol, offset, delayMs) {
|
|
396
|
+
const off = `0x${offset.toString(16)}`;
|
|
397
|
+
return [
|
|
398
|
+
"// 0sec race-widening kprobe: inject mdelay() at the faulting PC to widen",
|
|
399
|
+
"// the UAF/race window. Best-effort; harmless if the probe fails to register.",
|
|
400
|
+
"#include <linux/module.h>",
|
|
401
|
+
"#include <linux/kernel.h>",
|
|
402
|
+
"#include <linux/kprobes.h>",
|
|
403
|
+
"#include <linux/delay.h>",
|
|
404
|
+
"",
|
|
405
|
+
'MODULE_LICENSE("GPL");',
|
|
406
|
+
"",
|
|
407
|
+
`static unsigned long widen_delay_ms = ${delayMs};`,
|
|
408
|
+
"module_param(widen_delay_ms, ulong, 0644);",
|
|
409
|
+
"",
|
|
410
|
+
"static struct kprobe kp = {",
|
|
411
|
+
` .symbol_name = "${symbol}",`,
|
|
412
|
+
` .offset = ${off},`,
|
|
413
|
+
"};",
|
|
414
|
+
"",
|
|
415
|
+
"static int handler_pre(struct kprobe *p, struct pt_regs *regs) {",
|
|
416
|
+
" mdelay(widen_delay_ms);",
|
|
417
|
+
" return 0;",
|
|
418
|
+
"}",
|
|
419
|
+
"",
|
|
420
|
+
"static int __init widen_init(void) {",
|
|
421
|
+
" kp.pre_handler = handler_pre;",
|
|
422
|
+
` pr_info("0sec-widen: probing ${symbol}+${off} delay=%lums\\n", widen_delay_ms);`,
|
|
423
|
+
" return register_kprobe(&kp);",
|
|
424
|
+
"}",
|
|
425
|
+
"",
|
|
426
|
+
"static void __exit widen_exit(void) { unregister_kprobe(&kp); }",
|
|
427
|
+
"",
|
|
428
|
+
"module_init(widen_init);",
|
|
429
|
+
"module_exit(widen_exit);"
|
|
430
|
+
].join("\n");
|
|
431
|
+
}
|
|
432
|
+
var RACE_HARNESS_BASE_HEADERS = [
|
|
433
|
+
"#include <stdio.h>",
|
|
434
|
+
"#include <stdlib.h>",
|
|
435
|
+
"#include <string.h>",
|
|
436
|
+
"#include <unistd.h>",
|
|
437
|
+
"#include <pthread.h>",
|
|
438
|
+
"#include <sched.h>",
|
|
439
|
+
"#include <time.h>",
|
|
440
|
+
"#include <fcntl.h>",
|
|
441
|
+
"#include <sys/types.h>"
|
|
442
|
+
];
|
|
443
|
+
function renderRealIpiRaceHarness(spec) {
|
|
444
|
+
const maxIters = Number.isFinite(spec.maxIters) ? Math.max(1, Math.trunc(spec.maxIters)) : 2e5;
|
|
445
|
+
const seconds = Number.isFinite(spec.seconds) ? Math.max(1, Math.trunc(spec.seconds)) : 40;
|
|
446
|
+
const cpuA = 0;
|
|
447
|
+
const cpuB = spec.sameCpu ? 0 : 1;
|
|
448
|
+
const seen = /* @__PURE__ */ new Set();
|
|
449
|
+
const headerLines = [];
|
|
450
|
+
const push = (h) => {
|
|
451
|
+
const t = h.trim();
|
|
452
|
+
if (!t || seen.has(t))
|
|
453
|
+
return;
|
|
454
|
+
seen.add(t);
|
|
455
|
+
headerLines.push(t);
|
|
456
|
+
};
|
|
457
|
+
push("#define _GNU_SOURCE");
|
|
458
|
+
for (const h of RACE_HARNESS_BASE_HEADERS)
|
|
459
|
+
push(h);
|
|
460
|
+
for (const h of spec.headers ?? []) {
|
|
461
|
+
push(h);
|
|
462
|
+
}
|
|
463
|
+
const setupC = spec.setupC?.trim() ? spec.setupC : "/* no widening tactics composed */";
|
|
464
|
+
return [
|
|
465
|
+
...headerLines,
|
|
466
|
+
"",
|
|
467
|
+
"static volatile int g_stop = 0;",
|
|
468
|
+
"",
|
|
469
|
+
"static void osec_pin_cpu(int cpu) {",
|
|
470
|
+
" cpu_set_t set;",
|
|
471
|
+
" CPU_ZERO(&set);",
|
|
472
|
+
" CPU_SET(cpu, &set);",
|
|
473
|
+
" sched_setaffinity(0, sizeof(set), &set);",
|
|
474
|
+
"}",
|
|
475
|
+
"",
|
|
476
|
+
"static long osec_env_long(const char *name, long dflt) {",
|
|
477
|
+
" const char *v = getenv(name);",
|
|
478
|
+
" if (!v || !*v) return dflt;",
|
|
479
|
+
" char *end = NULL;",
|
|
480
|
+
" long n = strtol(v, &end, 10);",
|
|
481
|
+
" return (end && *end == '\\0' && n > 0) ? n : dflt;",
|
|
482
|
+
"}",
|
|
483
|
+
"",
|
|
484
|
+
`static void *osec_racer_a(void *arg) {`,
|
|
485
|
+
" (void)arg;",
|
|
486
|
+
` osec_pin_cpu(${cpuA});`,
|
|
487
|
+
" while (!g_stop) {",
|
|
488
|
+
` ${spec.raceOpA}`,
|
|
489
|
+
" }",
|
|
490
|
+
" return NULL;",
|
|
491
|
+
"}",
|
|
492
|
+
"",
|
|
493
|
+
`static void *osec_racer_b(void *arg) {`,
|
|
494
|
+
" (void)arg;",
|
|
495
|
+
` osec_pin_cpu(${cpuB});`,
|
|
496
|
+
" while (!g_stop) {",
|
|
497
|
+
` ${spec.raceOpB}`,
|
|
498
|
+
" }",
|
|
499
|
+
" return NULL;",
|
|
500
|
+
"}",
|
|
501
|
+
"",
|
|
502
|
+
"int main(void) {",
|
|
503
|
+
` long retries = osec_env_long("0SEC_RACE_RETRIES", ${maxIters});`,
|
|
504
|
+
` long seconds = osec_env_long("0SEC_RACE_SECONDS", ${seconds});`,
|
|
505
|
+
" time_t deadline = time(NULL) + seconds;",
|
|
506
|
+
"",
|
|
507
|
+
" /* Arm the widening tactics ONCE (freeze/register + IPI bursts). */",
|
|
508
|
+
setupC,
|
|
509
|
+
"",
|
|
510
|
+
" /* Bad Epoll non-crashing retry loop: re-race until the sanitizer fires.",
|
|
511
|
+
" The harness itself never touches freed memory \u2014 the kernel splat on the",
|
|
512
|
+
" serial console is the only terminator; here we just exhaust the budget. */",
|
|
513
|
+
" for (long iter = 0; iter < retries && time(NULL) < deadline; iter++) {",
|
|
514
|
+
" g_stop = 0;",
|
|
515
|
+
" pthread_t ta, tb;",
|
|
516
|
+
" if (pthread_create(&ta, NULL, osec_racer_a, NULL) != 0) break;",
|
|
517
|
+
" if (pthread_create(&tb, NULL, osec_racer_b, NULL) != 0) { g_stop = 1; pthread_join(ta, NULL); break; }",
|
|
518
|
+
" usleep(200);",
|
|
519
|
+
" g_stop = 1;",
|
|
520
|
+
" pthread_join(ta, NULL);",
|
|
521
|
+
" pthread_join(tb, NULL);",
|
|
522
|
+
' if ((iter & 0x3ff) == 0) { printf("0SEC-RACE iter=%ld\\n", iter); fflush(stdout); }',
|
|
523
|
+
" }",
|
|
524
|
+
' printf("0SEC-RACE done (budget exhausted, no splat)\\n");',
|
|
525
|
+
" fflush(stdout);",
|
|
526
|
+
" return 0;",
|
|
527
|
+
"}"
|
|
528
|
+
].join("\n");
|
|
529
|
+
}
|
|
530
|
+
function buildQemuCommand(config, serialLogPath, sharedDir) {
|
|
531
|
+
const args = [
|
|
532
|
+
"-m",
|
|
533
|
+
String(config.memoryMb),
|
|
534
|
+
"-smp",
|
|
535
|
+
String(config.smp),
|
|
536
|
+
"-kernel",
|
|
537
|
+
config.kernelImage,
|
|
538
|
+
"-drive",
|
|
539
|
+
`file=${config.diskImage},format=${config.diskFormat},if=virtio`,
|
|
540
|
+
"-append",
|
|
541
|
+
config.kernelAppend,
|
|
542
|
+
"-virtfs",
|
|
543
|
+
`local,path=${sharedDir},mount_tag=${config.shareTag},security_model=none,id=hostshare`,
|
|
544
|
+
"-nographic",
|
|
545
|
+
"-monitor",
|
|
546
|
+
"none",
|
|
547
|
+
"-serial",
|
|
548
|
+
`file:${serialLogPath}`,
|
|
549
|
+
"-no-reboot",
|
|
550
|
+
// Every verification boot must start from the same immutable rootfs state.
|
|
551
|
+
// QEMU writes guest disk changes to a temporary overlay and discards them
|
|
552
|
+
// when the VM exits; this is the disk-state half of the N-boot contract.
|
|
553
|
+
"-snapshot"
|
|
554
|
+
];
|
|
555
|
+
if (config.qemuAccel) {
|
|
556
|
+
args.push("-accel", config.qemuAccel);
|
|
557
|
+
}
|
|
558
|
+
if (config.initrdPath) {
|
|
559
|
+
args.push("-initrd", config.initrdPath);
|
|
560
|
+
}
|
|
561
|
+
return { command: config.qemuBinary, args };
|
|
562
|
+
}
|
|
563
|
+
function buildInitramfsKernelAppend(kaslr) {
|
|
564
|
+
return [
|
|
565
|
+
"console=ttyS0",
|
|
566
|
+
"earlyprintk=serial",
|
|
567
|
+
"rdinit=/init",
|
|
568
|
+
"kasan_multi_shot=1",
|
|
569
|
+
kaslr ? "kaslr" : "nokaslr",
|
|
570
|
+
"panic=-1"
|
|
571
|
+
].join(" ");
|
|
572
|
+
}
|
|
573
|
+
function renderInitramfsInitScript(moduleNames, raceEnv, timeoutSec) {
|
|
574
|
+
const raceEnvEntries = Object.entries(raceEnv).filter(([key]) => /^0SEC_RACE_[A-Z0-9_]+$/.test(key));
|
|
575
|
+
const envAssignments = raceEnvEntries.map(([key, value]) => `${key}=${shellQuote(value)}`).join(" ");
|
|
576
|
+
const exploitCommand = [
|
|
577
|
+
envAssignments ? `/bin/busybox env ${envAssignments}` : "",
|
|
578
|
+
`/bin/busybox timeout ${timeoutSec} /exploit > /tmp/run.log 2>&1`
|
|
579
|
+
].filter(Boolean).join(" ");
|
|
580
|
+
const insmods = moduleNames.map((m) => `insmod /lib/modules/${m} 2>&1 && echo "insmod ${m} ok" || echo "insmod ${m} rc=$?"`).join("\n");
|
|
581
|
+
return [
|
|
582
|
+
"#!/bin/busybox sh",
|
|
583
|
+
"/bin/busybox mkdir -p /proc /sys /dev /tmp /lib/modules",
|
|
584
|
+
"/bin/busybox mount -t proc none /proc",
|
|
585
|
+
"/bin/busybox mount -t sysfs none /sys",
|
|
586
|
+
"/bin/busybox mount -t devtmpfs none /dev 2>/dev/null",
|
|
587
|
+
"/bin/busybox --install -s /bin 2>/dev/null",
|
|
588
|
+
'echo "=== 0SEC-INITRAMFS weaponize lane up ==="',
|
|
589
|
+
"cat /proc/version",
|
|
590
|
+
insmods,
|
|
591
|
+
'echo "=== 0SEC-INITRAMFS run (env: ' + raceEnvEntries.map(([key]) => key).join(",") + ') ==="',
|
|
592
|
+
// CRITICAL: the engine's emitted exploit prints a RECLAIM marker on EVERY
|
|
593
|
+
// spray-loop iteration (thousands/sec). Streaming that to the slow 8250 UART
|
|
594
|
+
// wedges the CPU in io_serial_in and STARVES the race (NMI watchdog fires,
|
|
595
|
+
// KASAN never gets a window). So redirect the exploit's stdout/stderr to a
|
|
596
|
+
// tmpfs file (fast, no UART blocking) during the race, then `cat` it to the
|
|
597
|
+
// serial console AFTER the race completes — the oracle reads the full serial
|
|
598
|
+
// stream either way, and the KASAN splats (kernel printk, a separate path)
|
|
599
|
+
// still interleave live. `timeout` caps a hung flood; busybox `timeout` takes
|
|
600
|
+
// the seconds as a POSITIONAL arg (`timeout SECS PROG`), NOT GNU `-t SECS`.
|
|
601
|
+
`${exploitCommand} || echo "0SEC-INITRAMFS exploit exit=$?" >> /tmp/run.log`,
|
|
602
|
+
'echo "=== 0SEC-INITRAMFS exploit output (batched off the UART hot path) ==="',
|
|
603
|
+
"cat /tmp/run.log",
|
|
604
|
+
'echo "=== 0SEC-INITRAMFS post-run ==="',
|
|
605
|
+
"sync",
|
|
606
|
+
"cat /tmp/pwned 2>/dev/null && echo 0SEC-INITRAMFS-PWNED-FILE-PRESENT",
|
|
607
|
+
'echo "=== 0SEC-INITRAMFS done; powering off ==="',
|
|
608
|
+
"/bin/busybox poweroff -f"
|
|
609
|
+
].join("\n");
|
|
610
|
+
}
|
|
611
|
+
function buildWeaponizeInitramfs(config, exploitC, workDir, raceEnv) {
|
|
612
|
+
const rootDir = join(workDir, "initramfs-root");
|
|
613
|
+
mkdirSync(join(rootDir, "bin"), { recursive: true });
|
|
614
|
+
mkdirSync(join(rootDir, "lib", "modules"), { recursive: true });
|
|
615
|
+
for (const sub of ["proc", "sys", "dev", "tmp"]) {
|
|
616
|
+
mkdirSync(join(rootDir, sub), { recursive: true });
|
|
617
|
+
}
|
|
618
|
+
const busybox = config.busyboxPath || (() => {
|
|
619
|
+
try {
|
|
620
|
+
return execFileSync("sh", ["-c", "command -v busybox"], { encoding: "utf8" }).trim();
|
|
621
|
+
} catch {
|
|
622
|
+
return "";
|
|
623
|
+
}
|
|
624
|
+
})();
|
|
625
|
+
if (!busybox || !existsSync(busybox)) {
|
|
626
|
+
throw new Error("weaponize-initramfs lane: no busybox found (set 0SEC_KERNEL_QEMU_BUSYBOX to a STATIC busybox)");
|
|
627
|
+
}
|
|
628
|
+
execFileSync("cp", [busybox, join(rootDir, "bin", "busybox")]);
|
|
629
|
+
chmodSync(join(rootDir, "bin", "busybox"), 493);
|
|
630
|
+
const srcPath = join(workDir, "exploit.c");
|
|
631
|
+
writeFileSync(srcPath, exploitC, "utf-8");
|
|
632
|
+
const binPath = join(rootDir, "exploit");
|
|
633
|
+
const compileLog = join(workDir, "compile.log");
|
|
634
|
+
try {
|
|
635
|
+
execFileSync("gcc", ["-O0", "-g", "-static", "-o", binPath, srcPath, "-lpthread"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
636
|
+
} catch (err) {
|
|
637
|
+
const msg = err instanceof Error && "stderr" in err ? String(err.stderr) : String(err);
|
|
638
|
+
writeFileSync(compileLog, msg, "utf-8");
|
|
639
|
+
throw new Error(`weaponize-initramfs lane: static exploit compile failed:
|
|
640
|
+
${msg.slice(0, 1500)}`);
|
|
641
|
+
}
|
|
642
|
+
chmodSync(binPath, 493);
|
|
643
|
+
const moduleNames = [];
|
|
644
|
+
for (const koPath of config.initramfsModules ?? []) {
|
|
645
|
+
if (!existsSync(koPath)) {
|
|
646
|
+
throw new Error(`weaponize-initramfs lane: module not found: ${koPath}`);
|
|
647
|
+
}
|
|
648
|
+
const name = koPath.split("/").pop();
|
|
649
|
+
execFileSync("cp", [koPath, join(rootDir, "lib", "modules", name)]);
|
|
650
|
+
moduleNames.push(name);
|
|
651
|
+
}
|
|
652
|
+
const initPath = join(rootDir, "init");
|
|
653
|
+
writeFileSync(initPath, renderInitramfsInitScript(moduleNames, raceEnv, config.timeoutSec), "utf-8");
|
|
654
|
+
chmodSync(initPath, 493);
|
|
655
|
+
const cpioPath = join(workDir, "initramfs.cpio.gz");
|
|
656
|
+
execFileSync("sh", ["-c", `cd ${shellQuote(rootDir)} && find . -print0 | cpio --null -o --format=newc 2>/dev/null | gzip -9 > ${shellQuote(cpioPath)}`]);
|
|
657
|
+
if (!existsSync(cpioPath) || statSync(cpioPath).size === 0) {
|
|
658
|
+
throw new Error("weaponize-initramfs lane: cpio packing produced an empty initramfs");
|
|
659
|
+
}
|
|
660
|
+
return cpioPath;
|
|
661
|
+
}
|
|
662
|
+
function buildInitramfsQemuCommand(config, serialLogPath, initramfsPath, kernelAppend) {
|
|
663
|
+
const args = [
|
|
664
|
+
"-m",
|
|
665
|
+
String(config.memoryMb),
|
|
666
|
+
"-smp",
|
|
667
|
+
String(config.smp),
|
|
668
|
+
"-kernel",
|
|
669
|
+
config.kernelImage,
|
|
670
|
+
"-initrd",
|
|
671
|
+
initramfsPath,
|
|
672
|
+
"-append",
|
|
673
|
+
kernelAppend,
|
|
674
|
+
"-nographic",
|
|
675
|
+
"-monitor",
|
|
676
|
+
"none",
|
|
677
|
+
"-serial",
|
|
678
|
+
`file:${serialLogPath}`,
|
|
679
|
+
"-no-reboot"
|
|
680
|
+
];
|
|
681
|
+
if (config.qemuAccel) {
|
|
682
|
+
args.push("-accel", config.qemuAccel);
|
|
683
|
+
}
|
|
684
|
+
return { command: config.qemuBinary, args };
|
|
685
|
+
}
|
|
686
|
+
function collectRaceEnv() {
|
|
687
|
+
const out = {};
|
|
688
|
+
for (const key of [
|
|
689
|
+
"0SEC_RACE_FLOOD_THREADS",
|
|
690
|
+
"0SEC_RACE_SPRAY_THREADS",
|
|
691
|
+
"0SEC_RACE_PARK_US",
|
|
692
|
+
"0SEC_RACE_SECONDS",
|
|
693
|
+
"0SEC_RACE_SAME_CPU"
|
|
694
|
+
]) {
|
|
695
|
+
const v = process.env[key]?.trim();
|
|
696
|
+
if (v)
|
|
697
|
+
out[key] = v;
|
|
698
|
+
}
|
|
699
|
+
return out;
|
|
700
|
+
}
|
|
701
|
+
async function sleep(ms) {
|
|
702
|
+
await new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
703
|
+
}
|
|
704
|
+
async function stopVm(proc) {
|
|
705
|
+
if (proc.exitCode !== null || proc.killed)
|
|
706
|
+
return;
|
|
707
|
+
proc.kill("SIGTERM");
|
|
708
|
+
const deadline = Date.now() + 1e4;
|
|
709
|
+
while (proc.exitCode === null && Date.now() < deadline) {
|
|
710
|
+
await sleep(250);
|
|
711
|
+
}
|
|
712
|
+
if (proc.exitCode === null) {
|
|
713
|
+
proc.kill("SIGKILL");
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
function renderGuestRunnerScript(config, language, request) {
|
|
717
|
+
const uid = request.dropUid === void 0 ? "-" : String(request.dropUid);
|
|
718
|
+
const gid = request.dropGid === void 0 ? "-" : String(request.dropGid);
|
|
719
|
+
const launcherArgs = `"$WORK_DIR/attest-launcher" "$SHARE_DIR/execution-attestation.txt" ${shellQuote(request.nonce)} ${shellQuote(request.reproducerSha256)} ${shellQuote(request.expectedKernelRelease)} ${shellQuote(request.kernelImageSha256)} ${shellQuote(request.kernelConfigSha256)} ${shellQuote(uid)} ${shellQuote(gid)} "$SHARE_DIR/exec-started.ok"`;
|
|
720
|
+
if (language === "syz") {
|
|
721
|
+
return [
|
|
722
|
+
"#!/bin/sh",
|
|
723
|
+
"set -eu",
|
|
724
|
+
"SHARE_DIR=/mnt/0sec",
|
|
725
|
+
"WORK_DIR=/tmp/0sec-run",
|
|
726
|
+
'mkdir -p "$WORK_DIR"',
|
|
727
|
+
"compiled=0",
|
|
728
|
+
"executed=0",
|
|
729
|
+
"exit_code=0",
|
|
730
|
+
"timed_out=0",
|
|
731
|
+
'cp "$SHARE_DIR/repro.syz" "$WORK_DIR/repro.syz"',
|
|
732
|
+
'if command -v syz-execprog >/dev/null 2>&1 && /usr/bin/gcc -O2 -o "$WORK_DIR/attest-launcher" "$SHARE_DIR/attest-launcher.c" >>"$SHARE_DIR/compile.log" 2>&1; then',
|
|
733
|
+
" compiled=1",
|
|
734
|
+
' : > "$SHARE_DIR/compile.log"',
|
|
735
|
+
"else",
|
|
736
|
+
` printf '%s\\n' 'syz-execprog not found in guest' > "$SHARE_DIR/compile.log"`,
|
|
737
|
+
" exit_code=127",
|
|
738
|
+
"fi",
|
|
739
|
+
'if [ "$compiled" = "1" ]; then',
|
|
740
|
+
" dmesg -C 2>/dev/null || true",
|
|
741
|
+
// KCOV coverage collection (AIxCC T1): ask syz-execprog to collect coverage
|
|
742
|
+
// and dump the per-call PC set to coverage_prog* shards on the share.
|
|
743
|
+
// `-cover=1` enables KCOV; `-coverfile` is the shard prefix. Both flags are no-ops
|
|
744
|
+
// (or rejected) on a non-KCOV kernel / older syz-execprog — fail-soft so a
|
|
745
|
+
// run without coverage still records its crash result.
|
|
746
|
+
` if timeout ${shellQuote(String(config.timeoutSec))}s ${launcherArgs} syz-execprog -cover=1 -coverfile="$SHARE_DIR/coverage" "$WORK_DIR/repro.syz" >"$SHARE_DIR/run.log" 2>&1; then`,
|
|
747
|
+
" executed=1",
|
|
748
|
+
" exit_code=0",
|
|
749
|
+
" else",
|
|
750
|
+
" exit_code=$?",
|
|
751
|
+
' if [ "$exit_code" = "124" ]; then',
|
|
752
|
+
" timed_out=1",
|
|
753
|
+
" else",
|
|
754
|
+
" executed=1",
|
|
755
|
+
" fi",
|
|
756
|
+
" fi",
|
|
757
|
+
' [ -f "$SHARE_DIR/exec-started.ok" ] || executed=0',
|
|
758
|
+
" # Consolidate coverage: syz-execprog -coverfile writes the PC set to",
|
|
759
|
+
" # per-program/per-call shards named `coverage_prog<N>.<call>` (e.g.",
|
|
760
|
+
" # `coverage_prog0.0`), and may also leave a bare `coverage` / `coverage.N`.",
|
|
761
|
+
" # Concatenate every shard into coverage.log for the host to parse; PCs are",
|
|
762
|
+
" # deduped downstream by parseCoveragePcs (fail-soft if none landed).",
|
|
763
|
+
' cat "$SHARE_DIR/coverage" "$SHARE_DIR/coverage."* "$SHARE_DIR/coverage_prog"* > "$SHARE_DIR/coverage.log" 2>/dev/null || true',
|
|
764
|
+
"else",
|
|
765
|
+
' : > "$SHARE_DIR/run.log"',
|
|
766
|
+
"fi",
|
|
767
|
+
'dmesg 2>/dev/null > "$SHARE_DIR/dmesg.log" || true',
|
|
768
|
+
`printf '%s\\n' "$compiled" > "$SHARE_DIR/compiled.ok"`,
|
|
769
|
+
`printf '%s\\n' "$executed" > "$SHARE_DIR/executed.ok"`,
|
|
770
|
+
`printf '%s\\n' "$exit_code" > "$SHARE_DIR/exit_code"`,
|
|
771
|
+
`printf '%s\\n' "$timed_out" > "$SHARE_DIR/timed_out"`,
|
|
772
|
+
"sync"
|
|
773
|
+
].join("\n");
|
|
774
|
+
}
|
|
775
|
+
const widenLines = config.widenSymbol !== void 0 && config.widenOffset !== void 0 && config.widenDelayMs !== void 0 && config.guestKernelBuildDir ? [
|
|
776
|
+
"# \u2500\u2500 Race-widening (best-effort, fail-soft) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
777
|
+
`KBUILD_DIR=${shellQuote(config.guestKernelBuildDir)}`,
|
|
778
|
+
"widened=0",
|
|
779
|
+
'if [ -d "$KBUILD_DIR" ] && command -v make >/dev/null 2>&1; then',
|
|
780
|
+
' cp "$SHARE_DIR/osec_widen.c" "$WORK_DIR/osec_widen.c" 2>/dev/null || true',
|
|
781
|
+
' printf "obj-m += osec_widen.o\\n" > "$WORK_DIR/Makefile"',
|
|
782
|
+
' if make -C "$KBUILD_DIR" M="$WORK_DIR" modules >"$SHARE_DIR/widen.log" 2>&1 \\',
|
|
783
|
+
' && insmod "$WORK_DIR/osec_widen.ko" >>"$SHARE_DIR/widen.log" 2>&1; then',
|
|
784
|
+
" widened=1",
|
|
785
|
+
' printf "%s\\n" "0sec-widen: insmod ok" >> "$SHARE_DIR/widen.log"',
|
|
786
|
+
" else",
|
|
787
|
+
' printf "%s\\n" "0sec-widen: build/insmod failed \u2014 running WITHOUT widening" >> "$SHARE_DIR/widen.log"',
|
|
788
|
+
" fi",
|
|
789
|
+
"else",
|
|
790
|
+
' printf "%s\\n" "0sec-widen: no kernel build tree in guest \u2014 running WITHOUT widening" > "$SHARE_DIR/widen.log"',
|
|
791
|
+
"fi",
|
|
792
|
+
'printf "%s\\n" "$widened" > "$SHARE_DIR/widened.ok"'
|
|
793
|
+
] : [];
|
|
794
|
+
return [
|
|
795
|
+
"#!/bin/sh",
|
|
796
|
+
"set -eu",
|
|
797
|
+
"# uid-drop exec contract: this runner executes the reproducer as the guest",
|
|
798
|
+
"# init (uid 0). A weaponization exploit drops to an unprivileged uid itself",
|
|
799
|
+
"# (setuid(65534)) BEFORE firing its root tail, then re-checks getuid()==0",
|
|
800
|
+
"# after \u2014 so the captured output carries an ordered DROP(uid!=0)\u2192ROOT(uid=0)",
|
|
801
|
+
"# witness the oracle uses to confirm a genuine escalation. We must therefore",
|
|
802
|
+
"# run it directly (as root), NOT via su/sudo to a lower uid.",
|
|
803
|
+
"SHARE_DIR=/mnt/0sec",
|
|
804
|
+
"WORK_DIR=/tmp/0sec-run",
|
|
805
|
+
'mkdir -p "$WORK_DIR"',
|
|
806
|
+
"compiled=0",
|
|
807
|
+
"executed=0",
|
|
808
|
+
"exit_code=0",
|
|
809
|
+
"timed_out=0",
|
|
810
|
+
'cp "$SHARE_DIR/repro.c" "$WORK_DIR/repro.c"',
|
|
811
|
+
...widenLines,
|
|
812
|
+
`if /usr/bin/gcc -B/usr/bin/ -O0 -g -o "$WORK_DIR/repro" "$WORK_DIR/repro.c" -lpthread >"$SHARE_DIR/compile.log" 2>&1 && /usr/bin/gcc -O2 -o "$WORK_DIR/attest-launcher" "$SHARE_DIR/attest-launcher.c" >>"$SHARE_DIR/compile.log" 2>&1; then`,
|
|
813
|
+
" compiled=1",
|
|
814
|
+
"else",
|
|
815
|
+
" exit_code=$?",
|
|
816
|
+
"fi",
|
|
817
|
+
'if [ "$compiled" = "1" ]; then',
|
|
818
|
+
" dmesg -C 2>/dev/null || true",
|
|
819
|
+
` if timeout ${shellQuote(String(config.timeoutSec))}s ${launcherArgs} "$WORK_DIR/repro" >"$SHARE_DIR/run.log" 2>&1; then`,
|
|
820
|
+
" executed=1",
|
|
821
|
+
" exit_code=0",
|
|
822
|
+
" else",
|
|
823
|
+
" exit_code=$?",
|
|
824
|
+
' if [ "$exit_code" = "124" ]; then',
|
|
825
|
+
" timed_out=1",
|
|
826
|
+
" else",
|
|
827
|
+
" executed=1",
|
|
828
|
+
" fi",
|
|
829
|
+
" fi",
|
|
830
|
+
' [ -f "$SHARE_DIR/exec-started.ok" ] || executed=0',
|
|
831
|
+
"else",
|
|
832
|
+
' : > "$SHARE_DIR/run.log"',
|
|
833
|
+
"fi",
|
|
834
|
+
'dmesg 2>/dev/null > "$SHARE_DIR/dmesg.log" || true',
|
|
835
|
+
`printf '%s\\n' "$compiled" > "$SHARE_DIR/compiled.ok"`,
|
|
836
|
+
`printf '%s\\n' "$executed" > "$SHARE_DIR/executed.ok"`,
|
|
837
|
+
`printf '%s\\n' "$exit_code" > "$SHARE_DIR/exit_code"`,
|
|
838
|
+
`printf '%s\\n' "$timed_out" > "$SHARE_DIR/timed_out"`,
|
|
839
|
+
"sync"
|
|
840
|
+
].join("\n");
|
|
841
|
+
}
|
|
842
|
+
async function waitForVmResult(config, proc, hostTmpDir, bootLogPath) {
|
|
843
|
+
const totalBudgetSec = config.bootTimeoutSec + config.timeoutSec + 60;
|
|
844
|
+
const deadline = Date.now() + totalBudgetSec * 1e3;
|
|
845
|
+
const compiledMarker = join(hostTmpDir, "compiled.ok");
|
|
846
|
+
while (Date.now() < deadline) {
|
|
847
|
+
if (existsSync(compiledMarker)) {
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (proc.exitCode !== null) {
|
|
851
|
+
const bootLog2 = existsSync(bootLogPath) ? readFileSync(bootLogPath, "utf-8").slice(-4e3) : "";
|
|
852
|
+
throw new Error(`kernel VM exited before producing results (exit=${proc.exitCode}).
|
|
853
|
+
${bootLog2}`);
|
|
854
|
+
}
|
|
855
|
+
await sleep(2e3);
|
|
856
|
+
}
|
|
857
|
+
const bootLog = existsSync(bootLogPath) ? readFileSync(bootLogPath, "utf-8").slice(-4e3) : "";
|
|
858
|
+
throw new Error(`timed out waiting for kernel VM results in shared dir ${hostTmpDir} after ${totalBudgetSec}s.
|
|
859
|
+
${bootLog}`);
|
|
860
|
+
}
|
|
861
|
+
function parseCoveragePcs(raw) {
|
|
862
|
+
const pcs = /* @__PURE__ */ new Set();
|
|
863
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
864
|
+
const token = line.trim().split(/\s+/)[0];
|
|
865
|
+
if (!token)
|
|
866
|
+
continue;
|
|
867
|
+
let value;
|
|
868
|
+
try {
|
|
869
|
+
value = token.startsWith("0x") || token.startsWith("0X") ? BigInt(token) : /^[0-9]+$/.test(token) ? BigInt(token) : -1n;
|
|
870
|
+
} catch {
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
if (value > 0n)
|
|
874
|
+
pcs.add(`0x${value.toString(16)}`);
|
|
875
|
+
}
|
|
876
|
+
return [...pcs].sort((a, b) => {
|
|
877
|
+
const av = BigInt(a);
|
|
878
|
+
const bv = BigInt(b);
|
|
879
|
+
return av < bv ? -1 : av > bv ? 1 : 0;
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
async function waitForInitramfsVm(config, proc, serialLogPath) {
|
|
883
|
+
const totalBudgetSec = config.bootTimeoutSec + config.timeoutSec + 60;
|
|
884
|
+
const deadline = Date.now() + totalBudgetSec * 1e3;
|
|
885
|
+
while (Date.now() < deadline) {
|
|
886
|
+
if (proc.exitCode !== null)
|
|
887
|
+
return { poweredOff: true };
|
|
888
|
+
if (existsSync(serialLogPath)) {
|
|
889
|
+
const tail = readFileSync(serialLogPath, "utf-8").slice(-2e3);
|
|
890
|
+
if (tail.includes("0SEC-INITRAMFS done"))
|
|
891
|
+
return { poweredOff: true };
|
|
892
|
+
}
|
|
893
|
+
await sleep(1e3);
|
|
894
|
+
}
|
|
895
|
+
return { poweredOff: false };
|
|
896
|
+
}
|
|
897
|
+
async function runWeaponizeInitramfs(config, exploitC) {
|
|
898
|
+
const hostTmpDir = config.artifactDir ? (() => {
|
|
899
|
+
mkdirSync(config.artifactDir, { recursive: true });
|
|
900
|
+
return mkdtempSync(join(config.artifactDir, "0sec-initramfs-"));
|
|
901
|
+
})() : mkdtempSync(join(tmpdir(), "0sec-initramfs-"));
|
|
902
|
+
const serialLogPath = join(hostTmpDir, "serial.log");
|
|
903
|
+
const raceEnv = collectRaceEnv();
|
|
904
|
+
let initramfsPath;
|
|
905
|
+
try {
|
|
906
|
+
initramfsPath = buildWeaponizeInitramfs(config, exploitC, hostTmpDir, raceEnv);
|
|
907
|
+
} catch (err) {
|
|
908
|
+
if (!config.artifactDir)
|
|
909
|
+
rmSync(hostTmpDir, { recursive: true, force: true });
|
|
910
|
+
return {
|
|
911
|
+
compiled: false,
|
|
912
|
+
executed: false,
|
|
913
|
+
output: err instanceof Error ? err.message : String(err),
|
|
914
|
+
dmesg: "",
|
|
915
|
+
exitCode: -1,
|
|
916
|
+
timedOut: false
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
const kernelAppend = buildInitramfsKernelAppend(config.kaslr ?? false);
|
|
920
|
+
const { command, args } = buildInitramfsQemuCommand(config, serialLogPath, initramfsPath, kernelAppend);
|
|
921
|
+
const vmProc = spawn(command, args, { stdio: "ignore" });
|
|
922
|
+
try {
|
|
923
|
+
const { poweredOff } = await waitForInitramfsVm(config, vmProc, serialLogPath);
|
|
924
|
+
const serial = existsSync(serialLogPath) ? readFileSync(serialLogPath, "utf-8") : "";
|
|
925
|
+
const executed = serial.includes("0SEC-INITRAMFS run");
|
|
926
|
+
const timedOut = !poweredOff;
|
|
927
|
+
if (executed) {
|
|
928
|
+
assertBugAttribution({
|
|
929
|
+
exploitSource: exploitC,
|
|
930
|
+
runOutput: serial,
|
|
931
|
+
targetModules: (config.initramfsModules ?? []).map((p) => p.split("/").pop() ?? p),
|
|
932
|
+
kaslrOn: config.kaslr ?? false
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
compiled: true,
|
|
937
|
+
executed,
|
|
938
|
+
output: serial,
|
|
939
|
+
dmesg: serial,
|
|
940
|
+
exitCode: executed ? 0 : 1,
|
|
941
|
+
timedOut
|
|
942
|
+
};
|
|
943
|
+
} finally {
|
|
944
|
+
await stopVm(vmProc);
|
|
945
|
+
if (!config.artifactDir) {
|
|
946
|
+
rmSync(hostTmpDir, { recursive: true, force: true });
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
async function runReproducerInKernelVm(report) {
|
|
951
|
+
if (!report.reproducer) {
|
|
952
|
+
return {
|
|
953
|
+
compiled: false,
|
|
954
|
+
executed: false,
|
|
955
|
+
output: "",
|
|
956
|
+
dmesg: "",
|
|
957
|
+
exitCode: -1,
|
|
958
|
+
timedOut: false
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
const config = loadKernelVmConfigFromEnv();
|
|
962
|
+
if (config.weaponizeInitramfs && (report.reproducerLanguage ?? "c") === "c") {
|
|
963
|
+
return runWeaponizeInitramfs(config, report.reproducer);
|
|
964
|
+
}
|
|
965
|
+
const hostTmpDir = config.artifactDir ? (() => {
|
|
966
|
+
mkdirSync(config.artifactDir, { recursive: true });
|
|
967
|
+
return mkdtempSync(join(config.artifactDir, "0sec-kvm-"));
|
|
968
|
+
})() : mkdtempSync(join(tmpdir(), "0sec-kvm-"));
|
|
969
|
+
const language = report.reproducerLanguage ?? "c";
|
|
970
|
+
const request = report.executionAttestationRequest ?? (() => {
|
|
971
|
+
const release = process.env["0SEC_KERNEL_QEMU_EXPECTED_RELEASE"]?.trim();
|
|
972
|
+
if (!release || !RELEASE_RE.test(release))
|
|
973
|
+
throw new Error("direct kernel VM execution requires 0SEC_KERNEL_QEMU_EXPECTED_RELEASE");
|
|
974
|
+
return {
|
|
975
|
+
nonce: randomBytes(16).toString("hex"),
|
|
976
|
+
reproducerSha256: createHash("sha256").update(report.reproducer).digest("hex"),
|
|
977
|
+
expectedKernelRelease: release,
|
|
978
|
+
kernelImageSha256: sha256File(config.kernelImage),
|
|
979
|
+
kernelConfigSha256: sha256File(process.env["0SEC_KERNEL_QEMU_CONFIG"]?.trim() || "")
|
|
980
|
+
};
|
|
981
|
+
})();
|
|
982
|
+
const sourcePath = join(hostTmpDir, language === "syz" ? "repro.syz" : "repro.c");
|
|
983
|
+
const runnerScriptPath = join(hostTmpDir, "runner.sh");
|
|
984
|
+
const serialLogPath = join(hostTmpDir, "serial.log");
|
|
985
|
+
writeFileSync(sourcePath, report.reproducer, "utf-8");
|
|
986
|
+
writeFileSync(runnerScriptPath, renderGuestRunnerScript(config, language, request), "utf-8");
|
|
987
|
+
writeFileSync(join(hostTmpDir, "attest-launcher.c"), renderKernelExecutionLauncherSource(), "utf-8");
|
|
988
|
+
if (config.widenSymbol !== void 0 && config.widenOffset !== void 0 && config.widenDelayMs !== void 0) {
|
|
989
|
+
writeFileSync(join(hostTmpDir, "osec_widen.c"), renderRaceWidenModuleSource(config.widenSymbol, config.widenOffset, config.widenDelayMs), "utf-8");
|
|
990
|
+
}
|
|
991
|
+
const { command, args } = buildQemuCommand(config, serialLogPath, hostTmpDir);
|
|
992
|
+
const vmProc = spawn(command, args, {
|
|
993
|
+
stdio: "ignore"
|
|
994
|
+
});
|
|
995
|
+
try {
|
|
996
|
+
await waitForVmResult(config, vmProc, hostTmpDir, serialLogPath);
|
|
997
|
+
const compiled = readFileSync(join(hostTmpDir, "compiled.ok"), "utf-8").trim() === "1";
|
|
998
|
+
const executed = existsSync(join(hostTmpDir, "executed.ok")) ? readFileSync(join(hostTmpDir, "executed.ok"), "utf-8").trim() === "1" : false;
|
|
999
|
+
const exitCode = existsSync(join(hostTmpDir, "exit_code")) ? parseInt(readFileSync(join(hostTmpDir, "exit_code"), "utf-8").trim(), 10) : 1;
|
|
1000
|
+
const timedOut = existsSync(join(hostTmpDir, "timed_out")) ? readFileSync(join(hostTmpDir, "timed_out"), "utf-8").trim() === "1" : false;
|
|
1001
|
+
const compileLog = existsSync(join(hostTmpDir, "compile.log")) ? readFileSync(join(hostTmpDir, "compile.log"), "utf-8").trim() : "";
|
|
1002
|
+
const runLog = existsSync(join(hostTmpDir, "run.log")) ? readFileSync(join(hostTmpDir, "run.log"), "utf-8").trim() : "";
|
|
1003
|
+
const dmesg = existsSync(join(hostTmpDir, "dmesg.log")) ? readFileSync(join(hostTmpDir, "dmesg.log"), "utf-8").trim() : "";
|
|
1004
|
+
const coveragePcs = existsSync(join(hostTmpDir, "coverage.log")) ? parseCoveragePcs(readFileSync(join(hostTmpDir, "coverage.log"), "utf-8")) : void 0;
|
|
1005
|
+
const executionAttestationPath = join(hostTmpDir, "execution-attestation.txt");
|
|
1006
|
+
let executionAttestation;
|
|
1007
|
+
if (existsSync(executionAttestationPath)) {
|
|
1008
|
+
executionAttestation = parseKernelExecutionAttestation(readFileSync(executionAttestationPath, "utf-8"));
|
|
1009
|
+
bindKernelExecutionAttestation(executionAttestation, request);
|
|
1010
|
+
}
|
|
1011
|
+
if (executed) {
|
|
1012
|
+
assertBugAttribution({
|
|
1013
|
+
exploitSource: report.reproducer,
|
|
1014
|
+
runOutput: runLog,
|
|
1015
|
+
targetModules: (config.initramfsModules ?? []).map((p) => p.split("/").pop() ?? p),
|
|
1016
|
+
kaslrOn: config.kaslr ?? false
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
return {
|
|
1020
|
+
compiled,
|
|
1021
|
+
executed,
|
|
1022
|
+
output: compiled ? runLog : compileLog,
|
|
1023
|
+
dmesg,
|
|
1024
|
+
exitCode: Number.isFinite(exitCode) ? exitCode : 1,
|
|
1025
|
+
timedOut,
|
|
1026
|
+
...coveragePcs && coveragePcs.length > 0 ? { coveragePcs } : {},
|
|
1027
|
+
...executionAttestation ? { executionAttestation, executionAttestationPath } : {}
|
|
1028
|
+
};
|
|
1029
|
+
} finally {
|
|
1030
|
+
await stopVm(vmProc);
|
|
1031
|
+
if (!config.artifactDir) {
|
|
1032
|
+
rmSync(hostTmpDir, { recursive: true, force: true });
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
var KERNEL_CRASH_SIGNATURES = [
|
|
1037
|
+
// KCSAN data-race — the race class KASAN is structurally blind to. Matched
|
|
1038
|
+
// first so a widened data-race under the `kcsan` build profile is recognized
|
|
1039
|
+
// (closes the loop with kcsan-race.ts's parser + patch-to-poc.ts's
|
|
1040
|
+
// "KCSAN: data-race" expected signature).
|
|
1041
|
+
{ pattern: /BUG:\s*KCSAN:\s*data-race|KCSAN:\s*data-race/i, signature: "kcsan-data-race" },
|
|
1042
|
+
{ pattern: /KASAN:\s+slab-use-after-free|KASAN.*use-after-free/i, signature: "kasan-uaf" },
|
|
1043
|
+
{ pattern: /KASAN:\s+slab-out-of-bounds|KASAN.*out-of-bounds/i, signature: "kasan-oob" },
|
|
1044
|
+
{ pattern: /KASAN.*double-free/i, signature: "kasan-double-free" },
|
|
1045
|
+
{ pattern: /KASAN.*invalid-free/i, signature: "kasan-invalid-free" },
|
|
1046
|
+
{ pattern: /KASAN.*stack-out-of-bounds/i, signature: "kasan-stack-oob" },
|
|
1047
|
+
{ pattern: /UBSAN.*shift/i, signature: "ubsan-shift" },
|
|
1048
|
+
{ pattern: /UBSAN.*overflow/i, signature: "ubsan-overflow" },
|
|
1049
|
+
{ pattern: /UBSAN.*(out-of-bounds|index)/i, signature: "ubsan-bounds" },
|
|
1050
|
+
{ pattern: /UBSAN/i, signature: "ubsan" },
|
|
1051
|
+
{ pattern: /NULL pointer dereference|kernel NULL pointer/i, signature: "null-deref" },
|
|
1052
|
+
{ pattern: /general protection fault/i, signature: "general-protection" }
|
|
1053
|
+
];
|
|
1054
|
+
function detectKernelSignature(dmesg) {
|
|
1055
|
+
for (const { pattern, signature } of KERNEL_CRASH_SIGNATURES) {
|
|
1056
|
+
if (pattern.test(dmesg))
|
|
1057
|
+
return signature;
|
|
1058
|
+
}
|
|
1059
|
+
return void 0;
|
|
1060
|
+
}
|
|
1061
|
+
function defaultDmesgOutPath() {
|
|
1062
|
+
const ns = process.hrtime.bigint().toString();
|
|
1063
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
1064
|
+
return join(tmpdir(), `0sec-verify-${ns}-${random}.dmesg`);
|
|
1065
|
+
}
|
|
1066
|
+
function writeProofFileReadOnly(path, content) {
|
|
1067
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1068
|
+
writeFileSync(path, content, "utf-8");
|
|
1069
|
+
try {
|
|
1070
|
+
chmodSync(path, 292);
|
|
1071
|
+
} catch {
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
async function verifyKernelFinding(opts) {
|
|
1075
|
+
const log = opts.logger ?? ((line) => console.log(line));
|
|
1076
|
+
const dmesgOutPath = opts.dmesgOutPath ?? defaultDmesgOutPath();
|
|
1077
|
+
if (!opts.syzProgramPath && !opts.reproducerPath) {
|
|
1078
|
+
throw new Error("verifyKernelFinding requires either syzProgramPath or reproducerPath");
|
|
1079
|
+
}
|
|
1080
|
+
if (opts.syzProgramPath && opts.reproducerPath) {
|
|
1081
|
+
throw new Error("verifyKernelFinding: pass only one of syzProgramPath or reproducerPath");
|
|
1082
|
+
}
|
|
1083
|
+
const reproPath = opts.syzProgramPath ?? opts.reproducerPath;
|
|
1084
|
+
const reproducerLanguage = opts.syzProgramPath ? "syz" : "c";
|
|
1085
|
+
if (!existsSync(reproPath)) {
|
|
1086
|
+
throw new Error(`reproducer not found: ${reproPath}`);
|
|
1087
|
+
}
|
|
1088
|
+
if (opts.executionIdentity && (!Number.isSafeInteger(opts.executionIdentity.uid) || opts.executionIdentity.uid <= 0 || opts.executionIdentity.uid > 4294967295 || !Number.isSafeInteger(opts.executionIdentity.gid) || opts.executionIdentity.gid <= 0 || opts.executionIdentity.gid > 4294967295)) {
|
|
1089
|
+
throw new Error("executionIdentity requires positive uint32 uid/gid values");
|
|
1090
|
+
}
|
|
1091
|
+
const reproducerBytes = readFileSync(reproPath);
|
|
1092
|
+
const nonce = randomBytes(16).toString("hex");
|
|
1093
|
+
const reproducerSha256 = createHash("sha256").update(reproducerBytes).digest("hex");
|
|
1094
|
+
let artifacts;
|
|
1095
|
+
try {
|
|
1096
|
+
artifacts = prepareKernelVmArtifacts({
|
|
1097
|
+
kernelTree: opts.kernelTree,
|
|
1098
|
+
configProfile: opts.kernelConfig ?? "kasan",
|
|
1099
|
+
cacheDir: opts.cacheDir,
|
|
1100
|
+
force: opts.forceBuild,
|
|
1101
|
+
logger: log,
|
|
1102
|
+
buildRunner: opts.buildRunner
|
|
1103
|
+
});
|
|
1104
|
+
} catch (err) {
|
|
1105
|
+
writeProofFileReadOnly(dmesgOutPath, `[build_failed] ${err instanceof Error ? err.message : String(err)}
|
|
1106
|
+
`);
|
|
1107
|
+
return {
|
|
1108
|
+
status: "build_failed",
|
|
1109
|
+
dmesg_path: dmesgOutPath,
|
|
1110
|
+
build_cache_hit: false
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
const build_cache_hit = artifacts.cacheStatus === "hit" || artifacts.cacheStatus === "env";
|
|
1114
|
+
mkdirSync(dirname(dmesgOutPath), { recursive: true });
|
|
1115
|
+
const launchDir = mkdtempSync(join(dirname(dmesgOutPath), ".0sec-kernel-launch-"));
|
|
1116
|
+
const stagedKernelImage = join(launchDir, "kernel.image");
|
|
1117
|
+
const stagedKernelConfig = join(launchDir, "kernel.config");
|
|
1118
|
+
let attestationRequest;
|
|
1119
|
+
try {
|
|
1120
|
+
copyFileSync(artifacts.kernelImage, stagedKernelImage, fsConstants.COPYFILE_FICLONE);
|
|
1121
|
+
copyFileSync(artifacts.kernelConfig, stagedKernelConfig, fsConstants.COPYFILE_FICLONE);
|
|
1122
|
+
chmodSync(stagedKernelImage, 292);
|
|
1123
|
+
chmodSync(stagedKernelConfig, 292);
|
|
1124
|
+
attestationRequest = {
|
|
1125
|
+
nonce,
|
|
1126
|
+
reproducerSha256,
|
|
1127
|
+
expectedKernelRelease: expectedKernelRelease(opts.kernelTree, opts.expectedKernelRelease, artifacts.cacheStatus === "env"),
|
|
1128
|
+
kernelImageSha256: sha256File(stagedKernelImage),
|
|
1129
|
+
kernelConfigSha256: sha256File(stagedKernelConfig),
|
|
1130
|
+
...opts.executionIdentity ? { dropUid: opts.executionIdentity.uid, dropGid: opts.executionIdentity.gid } : {}
|
|
1131
|
+
};
|
|
1132
|
+
} catch (err) {
|
|
1133
|
+
rmSync(launchDir, { recursive: true, force: true });
|
|
1134
|
+
writeProofFileReadOnly(dmesgOutPath, `[build_failed] ${err instanceof Error ? err.message : String(err)}
|
|
1135
|
+
`);
|
|
1136
|
+
return { status: "build_failed", dmesg_path: dmesgOutPath, build_cache_hit };
|
|
1137
|
+
}
|
|
1138
|
+
const previousEnv = {
|
|
1139
|
+
qemu: process.env["0SEC_KERNEL_QEMU"],
|
|
1140
|
+
kernel: process.env["0SEC_KERNEL_QEMU_KERNEL"],
|
|
1141
|
+
disk: process.env["0SEC_KERNEL_QEMU_DISK"],
|
|
1142
|
+
cfg: process.env["0SEC_KERNEL_QEMU_CONFIG"],
|
|
1143
|
+
cacheKey: process.env["0SEC_KERNEL_QEMU_CACHEKEY"]
|
|
1144
|
+
};
|
|
1145
|
+
process.env["0SEC_KERNEL_QEMU"] = "1";
|
|
1146
|
+
process.env["0SEC_KERNEL_QEMU_KERNEL"] = stagedKernelImage;
|
|
1147
|
+
process.env["0SEC_KERNEL_QEMU_DISK"] = artifacts.diskImage;
|
|
1148
|
+
if (artifacts.kernelConfig) {
|
|
1149
|
+
process.env["0SEC_KERNEL_QEMU_CONFIG"] = stagedKernelConfig;
|
|
1150
|
+
}
|
|
1151
|
+
if (artifacts.cacheKey) {
|
|
1152
|
+
process.env["0SEC_KERNEL_QEMU_CACHEKEY"] = artifacts.cacheKey;
|
|
1153
|
+
}
|
|
1154
|
+
let runResult;
|
|
1155
|
+
try {
|
|
1156
|
+
const report = {
|
|
1157
|
+
raw: "",
|
|
1158
|
+
crashType: "unknown",
|
|
1159
|
+
faultingFunction: "unknown",
|
|
1160
|
+
stackFrames: [],
|
|
1161
|
+
reproducer: reproducerBytes.toString("utf-8"),
|
|
1162
|
+
reproducerLanguage,
|
|
1163
|
+
executionAttestationRequest: attestationRequest
|
|
1164
|
+
};
|
|
1165
|
+
const runner = opts.vmRunner ?? runReproducerInKernelVm;
|
|
1166
|
+
runResult = await runner(report);
|
|
1167
|
+
if (sha256File(stagedKernelImage) !== attestationRequest.kernelImageSha256 || sha256File(stagedKernelConfig) !== attestationRequest.kernelConfigSha256) {
|
|
1168
|
+
throw new Error("staged kernel image or config changed during execution");
|
|
1169
|
+
}
|
|
1170
|
+
} catch (err) {
|
|
1171
|
+
writeProofFileReadOnly(dmesgOutPath, `[run_failed] ${err instanceof Error ? err.message : String(err)}
|
|
1172
|
+
`);
|
|
1173
|
+
return {
|
|
1174
|
+
status: "run_failed",
|
|
1175
|
+
dmesg_path: dmesgOutPath,
|
|
1176
|
+
build_cache_hit
|
|
1177
|
+
};
|
|
1178
|
+
} finally {
|
|
1179
|
+
process.env["0SEC_KERNEL_QEMU"] = previousEnv.qemu;
|
|
1180
|
+
process.env["0SEC_KERNEL_QEMU_KERNEL"] = previousEnv.kernel;
|
|
1181
|
+
process.env["0SEC_KERNEL_QEMU_DISK"] = previousEnv.disk;
|
|
1182
|
+
process.env["0SEC_KERNEL_QEMU_CONFIG"] = previousEnv.cfg;
|
|
1183
|
+
process.env["0SEC_KERNEL_QEMU_CACHEKEY"] = previousEnv.cacheKey;
|
|
1184
|
+
rmSync(launchDir, { recursive: true, force: true });
|
|
1185
|
+
}
|
|
1186
|
+
const dmesgContent = runResult.dmesg || runResult.output || "";
|
|
1187
|
+
writeProofFileReadOnly(dmesgOutPath, dmesgContent);
|
|
1188
|
+
const dmesgSha256 = createHash("sha256").update(dmesgContent).digest("hex");
|
|
1189
|
+
const cov = runResult.coveragePcs && runResult.coveragePcs.length > 0 ? { coveragePcs: runResult.coveragePcs } : {};
|
|
1190
|
+
let attestationFields = {};
|
|
1191
|
+
if (runResult.executionAttestation) {
|
|
1192
|
+
try {
|
|
1193
|
+
bindKernelExecutionAttestation(runResult.executionAttestation, attestationRequest);
|
|
1194
|
+
const attestationPath = `${dmesgOutPath}.execution-attestation`;
|
|
1195
|
+
const raw = runResult.executionAttestationPath && existsSync(runResult.executionAttestationPath) ? readFileSync(runResult.executionAttestationPath, "utf-8") : serializeKernelExecutionAttestation(runResult.executionAttestation);
|
|
1196
|
+
const parsedRaw = parseKernelExecutionAttestation(raw);
|
|
1197
|
+
bindKernelExecutionAttestation(parsedRaw, attestationRequest);
|
|
1198
|
+
if (serializeKernelExecutionAttestation(parsedRaw) !== serializeKernelExecutionAttestation(runResult.executionAttestation))
|
|
1199
|
+
throw new Error("kernel execution attestation object/raw mismatch");
|
|
1200
|
+
writeProofFileReadOnly(attestationPath, raw);
|
|
1201
|
+
attestationFields = { executionAttestation: parsedRaw, executionAttestationPath: attestationPath, executionAttestationSha256: createHash("sha256").update(raw).digest("hex") };
|
|
1202
|
+
} catch {
|
|
1203
|
+
return { status: "run_failed", dmesg_path: dmesgOutPath, build_cache_hit, ...cov, dmesgSha256 };
|
|
1204
|
+
}
|
|
1205
|
+
} else if (opts.executionIdentity || !opts.vmRunner) {
|
|
1206
|
+
return { status: "run_failed", dmesg_path: dmesgOutPath, build_cache_hit, ...cov, dmesgSha256 };
|
|
1207
|
+
}
|
|
1208
|
+
if (!runResult.compiled || !runResult.executed) {
|
|
1209
|
+
return {
|
|
1210
|
+
status: "run_failed",
|
|
1211
|
+
dmesg_path: dmesgOutPath,
|
|
1212
|
+
build_cache_hit,
|
|
1213
|
+
...cov,
|
|
1214
|
+
dmesgSha256,
|
|
1215
|
+
...attestationFields
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
if (opts.expectedSignature) {
|
|
1219
|
+
const haystack = dmesgContent.toLowerCase();
|
|
1220
|
+
const needle = opts.expectedSignature.toLowerCase();
|
|
1221
|
+
if (haystack.includes(needle)) {
|
|
1222
|
+
return {
|
|
1223
|
+
status: "reproduced",
|
|
1224
|
+
signature: opts.expectedSignature,
|
|
1225
|
+
dmesg_path: dmesgOutPath,
|
|
1226
|
+
build_cache_hit,
|
|
1227
|
+
...cov,
|
|
1228
|
+
dmesgSha256,
|
|
1229
|
+
...attestationFields,
|
|
1230
|
+
...opts.executionIdentity ? { executionIdentity: opts.executionIdentity } : {}
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
const detected2 = detectKernelSignature(dmesgContent);
|
|
1234
|
+
if (detected2) {
|
|
1235
|
+
return {
|
|
1236
|
+
status: "run_failed",
|
|
1237
|
+
signature: detected2,
|
|
1238
|
+
dmesg_path: dmesgOutPath,
|
|
1239
|
+
build_cache_hit,
|
|
1240
|
+
...cov,
|
|
1241
|
+
dmesgSha256,
|
|
1242
|
+
...attestationFields
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
return {
|
|
1246
|
+
status: "no_signal",
|
|
1247
|
+
dmesg_path: dmesgOutPath,
|
|
1248
|
+
build_cache_hit,
|
|
1249
|
+
...cov,
|
|
1250
|
+
dmesgSha256,
|
|
1251
|
+
...attestationFields
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
const detected = detectKernelSignature(dmesgContent);
|
|
1255
|
+
if (detected) {
|
|
1256
|
+
return {
|
|
1257
|
+
status: "reproduced",
|
|
1258
|
+
signature: detected,
|
|
1259
|
+
dmesg_path: dmesgOutPath,
|
|
1260
|
+
build_cache_hit,
|
|
1261
|
+
...cov,
|
|
1262
|
+
dmesgSha256,
|
|
1263
|
+
...attestationFields
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
return {
|
|
1267
|
+
status: "no_signal",
|
|
1268
|
+
dmesg_path: dmesgOutPath,
|
|
1269
|
+
build_cache_hit,
|
|
1270
|
+
...cov,
|
|
1271
|
+
dmesgSha256,
|
|
1272
|
+
...attestationFields
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
function serializeKernelExecutionAttestation(r) {
|
|
1276
|
+
return `schema=2
|
|
1277
|
+
nonce=${r.nonce}
|
|
1278
|
+
reproducer_sha256=${r.reproducerSha256}
|
|
1279
|
+
expected_kernel_release=${r.expectedKernelRelease}
|
|
1280
|
+
observed_kernel_release=${r.observedKernelRelease}
|
|
1281
|
+
boot_id=${r.bootId}
|
|
1282
|
+
kernel_image_sha256=${r.kernelImageSha256}
|
|
1283
|
+
kernel_config_sha256=${r.kernelConfigSha256}
|
|
1284
|
+
ruid=${r.realUid}
|
|
1285
|
+
euid=${r.effectiveUid}
|
|
1286
|
+
suid=${r.savedUid}
|
|
1287
|
+
rgid=${r.realGid}
|
|
1288
|
+
egid=${r.effectiveGid}
|
|
1289
|
+
sgid=${r.savedGid}
|
|
1290
|
+
groups=${r.supplementaryGroups.join(",")}
|
|
1291
|
+
cap_inh=${r.inheritableCapabilities}
|
|
1292
|
+
cap_prm=${r.permittedCapabilities}
|
|
1293
|
+
cap_eff=${r.effectiveCapabilities}
|
|
1294
|
+
cap_amb=${r.ambientCapabilities}
|
|
1295
|
+
securebits=${r.secureBits}
|
|
1296
|
+
userns_max=${r.userNamespaceMax}
|
|
1297
|
+
initial_userns=${r.initialUserNamespace ? 1 : 0}
|
|
1298
|
+
no_new_privs=${r.noNewPrivileges ? 1 : 0}
|
|
1299
|
+
`;
|
|
1300
|
+
}
|
|
1301
|
+
async function verifyAcrossBoots(opts) {
|
|
1302
|
+
const log = opts.logger ?? ((line) => console.log(line));
|
|
1303
|
+
const bootTotal = Math.max(1, opts.boots ?? 3);
|
|
1304
|
+
const minHits = Math.max(1, Math.min(opts.minHits ?? 2, bootTotal));
|
|
1305
|
+
const { boots: _boots, minHits: _minHits, dmesgOutPath, ...baseOpts } = opts;
|
|
1306
|
+
const bootStatuses = [];
|
|
1307
|
+
const bootResults = [];
|
|
1308
|
+
let bootHits = 0;
|
|
1309
|
+
let firstReproduced;
|
|
1310
|
+
let lastResult;
|
|
1311
|
+
for (let i = 0; i < bootTotal; i++) {
|
|
1312
|
+
const perBootDmesg = dmesgOutPath ? i === 0 ? dmesgOutPath : `${dmesgOutPath}.boot-${i + 1}` : void 0;
|
|
1313
|
+
const result = await verifyKernelFinding({
|
|
1314
|
+
...baseOpts,
|
|
1315
|
+
...perBootDmesg ? { dmesgOutPath: perBootDmesg } : {}
|
|
1316
|
+
});
|
|
1317
|
+
lastResult = result;
|
|
1318
|
+
bootResults.push(result);
|
|
1319
|
+
bootStatuses.push(result.status);
|
|
1320
|
+
if (result.status === "reproduced") {
|
|
1321
|
+
bootHits++;
|
|
1322
|
+
if (!firstReproduced)
|
|
1323
|
+
firstReproduced = result;
|
|
1324
|
+
}
|
|
1325
|
+
log(`[nboot] boot ${i + 1}/${bootTotal}: ${result.status} (hits=${bootHits}/${minHits} required)`);
|
|
1326
|
+
if (bootHits + (bootTotal - i - 1) < minHits && bootHits < minHits) {
|
|
1327
|
+
log(`[nboot] threshold ${minHits} unreachable \u2014 stopping early`);
|
|
1328
|
+
break;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
let nbootStable = bootHits >= minHits;
|
|
1332
|
+
const winning = firstReproduced ?? lastResult;
|
|
1333
|
+
let aggregateStatus = nbootStable ? "reproduced" : pickWorstStatus(bootStatuses);
|
|
1334
|
+
let manifest = {};
|
|
1335
|
+
const reproduced = bootResults.filter((boot) => boot.status === "reproduced");
|
|
1336
|
+
const provenanceRequired = opts.executionIdentity !== void 0 || opts.vmRunner === void 0;
|
|
1337
|
+
if (nbootStable && provenanceRequired) {
|
|
1338
|
+
const attestations = reproduced.map((boot) => boot.executionAttestation);
|
|
1339
|
+
const first = attestations[0];
|
|
1340
|
+
const complete = reproduced.length >= minHits && first !== void 0 && reproduced.every((boot) => {
|
|
1341
|
+
const receipt = boot.executionAttestation;
|
|
1342
|
+
const identityMatches = !opts.executionIdentity || boot.executionIdentity?.uid === opts.executionIdentity.uid && boot.executionIdentity?.gid === opts.executionIdentity.gid;
|
|
1343
|
+
return receipt?.schemaVersion === 2 && boot.executionAttestationPath && boot.executionAttestationSha256 && boot.dmesgSha256 && identityMatches && receipt.expectedKernelRelease === first.expectedKernelRelease && receipt.observedKernelRelease === first.observedKernelRelease && receipt.kernelImageSha256 === first.kernelImageSha256 && receipt.kernelConfigSha256 === first.kernelConfigSha256;
|
|
1344
|
+
});
|
|
1345
|
+
const bootIds = new Set(attestations.flatMap((receipt) => receipt ? [receipt.bootId] : []));
|
|
1346
|
+
if (!complete || bootIds.size !== reproduced.length) {
|
|
1347
|
+
nbootStable = false;
|
|
1348
|
+
aggregateStatus = "run_failed";
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
if (nbootStable && provenanceRequired) {
|
|
1352
|
+
const path = `${dmesgOutPath ?? winning.dmesg_path}.execution-attestations.manifest`;
|
|
1353
|
+
const first = reproduced[0].executionAttestation;
|
|
1354
|
+
const content = JSON.stringify({
|
|
1355
|
+
schemaVersion: 2,
|
|
1356
|
+
expectedKernelRelease: first.expectedKernelRelease,
|
|
1357
|
+
observedKernelRelease: first.observedKernelRelease,
|
|
1358
|
+
kernelImageSha256: first.kernelImageSha256,
|
|
1359
|
+
kernelConfigSha256: first.kernelConfigSha256,
|
|
1360
|
+
...opts.executionIdentity ? { executionIdentity: opts.executionIdentity } : {},
|
|
1361
|
+
boots: reproduced.map((boot, index) => ({ index: index + 1, bootId: boot.executionAttestation.bootId, receiptSha256: boot.executionAttestationSha256, receiptPath: boot.executionAttestationPath, dmesgSha256: boot.dmesgSha256, dmesgPath: boot.dmesg_path }))
|
|
1362
|
+
}, null, 2) + "\n";
|
|
1363
|
+
writeProofFileReadOnly(path, content);
|
|
1364
|
+
manifest = { executionAttestationManifestPath: path, executionAttestationManifestSha256: createHash("sha256").update(content).digest("hex") };
|
|
1365
|
+
}
|
|
1366
|
+
return {
|
|
1367
|
+
status: aggregateStatus,
|
|
1368
|
+
...winning.signature ? { signature: winning.signature } : {},
|
|
1369
|
+
dmesg_path: winning.dmesg_path,
|
|
1370
|
+
build_cache_hit: winning.build_cache_hit,
|
|
1371
|
+
bootHits,
|
|
1372
|
+
bootTotal,
|
|
1373
|
+
nbootStable,
|
|
1374
|
+
bootStatuses,
|
|
1375
|
+
bootResults,
|
|
1376
|
+
...manifest,
|
|
1377
|
+
...winning.executionIdentity ? { executionIdentity: winning.executionIdentity } : {},
|
|
1378
|
+
...winning.executionAttestation ? { executionAttestation: winning.executionAttestation, executionAttestationPath: winning.executionAttestationPath, executionAttestationSha256: winning.executionAttestationSha256 } : {}
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
var STATUS_PRECEDENCE = [
|
|
1382
|
+
"build_failed",
|
|
1383
|
+
"run_failed",
|
|
1384
|
+
"no_signal",
|
|
1385
|
+
"reproduced"
|
|
1386
|
+
];
|
|
1387
|
+
function pickWorstStatus(statuses) {
|
|
1388
|
+
for (const candidate of STATUS_PRECEDENCE) {
|
|
1389
|
+
if (statuses.includes(candidate))
|
|
1390
|
+
return candidate;
|
|
1391
|
+
}
|
|
1392
|
+
return "no_signal";
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
export {
|
|
1396
|
+
detectOutOfBandModuleLoad,
|
|
1397
|
+
scanHardcodedKernelAddresses,
|
|
1398
|
+
assertBugAttribution,
|
|
1399
|
+
parseKernelExecutionAttestation,
|
|
1400
|
+
bindKernelExecutionAttestation,
|
|
1401
|
+
renderKernelExecutionLauncherSource,
|
|
1402
|
+
RECOGNIZED_CONFIG_PROFILES,
|
|
1403
|
+
buildFlagsForProfile,
|
|
1404
|
+
kcsanConfigSupported,
|
|
1405
|
+
prepareKernelVmArtifacts,
|
|
1406
|
+
loadKernelVmConfigFromEnv,
|
|
1407
|
+
buildKernelAppend,
|
|
1408
|
+
renderRaceWidenModuleSource,
|
|
1409
|
+
renderRealIpiRaceHarness,
|
|
1410
|
+
buildQemuCommand,
|
|
1411
|
+
buildInitramfsKernelAppend,
|
|
1412
|
+
renderInitramfsInitScript,
|
|
1413
|
+
buildWeaponizeInitramfs,
|
|
1414
|
+
buildInitramfsQemuCommand,
|
|
1415
|
+
collectRaceEnv,
|
|
1416
|
+
parseCoveragePcs,
|
|
1417
|
+
runReproducerInKernelVm,
|
|
1418
|
+
defaultDmesgOutPath,
|
|
1419
|
+
writeProofFileReadOnly,
|
|
1420
|
+
verifyKernelFinding,
|
|
1421
|
+
verifyAcrossBoots
|
|
1422
|
+
};
|