tamperward 2.29.13 → 2.29.15
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/dist/cli/index.js +114 -18
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -14640,6 +14640,7 @@ function parseCapturedSupervisorResult(stdoutText) {
|
|
|
14640
14640
|
signal: typeof raw.signal === "string" ? raw.signal : null,
|
|
14641
14641
|
timedOut: Boolean(raw.timedOut),
|
|
14642
14642
|
...typeof raw.error === "string" && raw.error ? { error: raw.error } : {},
|
|
14643
|
+
...raw.pipeHeldOpen === true ? { pipeHeldOpen: true } : {},
|
|
14643
14644
|
diagnostics
|
|
14644
14645
|
};
|
|
14645
14646
|
}
|
|
@@ -14657,6 +14658,7 @@ function runCapturedProcessSync(executable, args, opts) {
|
|
|
14657
14658
|
timeoutMs: opts.timeoutMs,
|
|
14658
14659
|
detached: Boolean(opts.detached),
|
|
14659
14660
|
killGroupOnFinish: Boolean(opts.killGroupOnFinish),
|
|
14661
|
+
drainMs: opts.drainMs ?? 1e3,
|
|
14660
14662
|
captureBytes: DIAGNOSTIC_TAIL_BYTES
|
|
14661
14663
|
}),
|
|
14662
14664
|
{ mode: 384 }
|
|
@@ -14789,6 +14791,7 @@ let done = false;
|
|
|
14789
14791
|
let timedOut = false;
|
|
14790
14792
|
let exitInfo = { exit: null, signal: null };
|
|
14791
14793
|
let descendantTracker = null;
|
|
14794
|
+
let drainTimer = null;
|
|
14792
14795
|
|
|
14793
14796
|
function killOwned() {
|
|
14794
14797
|
if (!child || !child.pid) return;
|
|
@@ -14810,6 +14813,7 @@ function finish(extra) {
|
|
|
14810
14813
|
if (done) return;
|
|
14811
14814
|
done = true;
|
|
14812
14815
|
if (descendantTracker) clearInterval(descendantTracker);
|
|
14816
|
+
if (drainTimer) clearTimeout(drainTimer);
|
|
14813
14817
|
try {
|
|
14814
14818
|
// stdout is reserved for the trusted supervisor result. Candidate suite
|
|
14815
14819
|
// stdout/stderr are separate pipes and are never inherited here.
|
|
@@ -14864,12 +14868,19 @@ if (child) {
|
|
|
14864
14868
|
signal: signal == null ? null : String(signal),
|
|
14865
14869
|
};
|
|
14866
14870
|
if (cfg.killGroupOnFinish) killOwned();
|
|
14871
|
+
// A leaked descendant can hold a pipe open so 'close' never fires; finish off
|
|
14872
|
+
// exit after a bounded drain instead of hanging. 'close' still wins if first.
|
|
14873
|
+
if (!drainTimer) {
|
|
14874
|
+
drainTimer = setTimeout(() => finish({ pipeHeldOpen: true }), Number(cfg.drainMs));
|
|
14875
|
+
drainTimer.unref();
|
|
14876
|
+
}
|
|
14867
14877
|
});
|
|
14868
14878
|
|
|
14869
14879
|
// close fires after the stdio pipes close, so the retained tails include all
|
|
14870
14880
|
// output that was drained before owned descendants were terminated.
|
|
14871
14881
|
child.on('close', (code, signal) => {
|
|
14872
14882
|
clearTimeout(timer);
|
|
14883
|
+
if (drainTimer) clearTimeout(drainTimer);
|
|
14873
14884
|
if (exitInfo.exit === null && code != null) exitInfo.exit = Number(code);
|
|
14874
14885
|
if (exitInfo.signal === null && signal != null) exitInfo.signal = String(signal);
|
|
14875
14886
|
finish({});
|
|
@@ -15714,7 +15725,12 @@ function runLocalSuite(dir, cmd, budgetSecs) {
|
|
|
15714
15725
|
diagnostics: r.diagnostics
|
|
15715
15726
|
};
|
|
15716
15727
|
}
|
|
15717
|
-
return {
|
|
15728
|
+
return {
|
|
15729
|
+
exit: r.exit,
|
|
15730
|
+
secs,
|
|
15731
|
+
...r.pipeHeldOpen ? { pipeHeldOpen: true } : {},
|
|
15732
|
+
diagnostics: r.diagnostics
|
|
15733
|
+
};
|
|
15718
15734
|
} finally {
|
|
15719
15735
|
rmSync7(scratch, { recursive: true, force: true });
|
|
15720
15736
|
}
|
|
@@ -15873,7 +15889,29 @@ function runVerify(opts) {
|
|
|
15873
15889
|
}
|
|
15874
15890
|
const frozenNodeModules = dependencyEnvironment ? dependencyEnvironment.roots.find((root) => root.kind === "node_modules")?.realPath ?? null : null;
|
|
15875
15891
|
const checkDeps = () => dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
|
|
15876
|
-
const runStage = (dir) => isolated ? runContainerStage(verifierBackend, dir, cmd, budget) : runLocalSuite(dir, cmd, budget);
|
|
15892
|
+
const runStage = opts.runStage ?? ((dir) => isolated ? runContainerStage(verifierBackend, dir, cmd, budget) : runLocalSuite(dir, cmd, budget));
|
|
15893
|
+
const notQuiescent = (stage, r) => {
|
|
15894
|
+
const detail = `the ${stage} suite exited (code ${r.exit ?? "unknown"}) but left a process holding stdout open after termination was attempted; the stage is not quiescent, so the run cannot be certified`;
|
|
15895
|
+
opts.onVerdict?.({ verdict: "CANNOT_VERIFY", reason: "VERIFIER_BACKEND_RUNTIME_FAILURE", detail });
|
|
15896
|
+
if (opts.json) {
|
|
15897
|
+
out3(JSON.stringify({
|
|
15898
|
+
schema_version: MACHINE_SCHEMA_VERSION,
|
|
15899
|
+
verdict: "CANNOT_VERIFY",
|
|
15900
|
+
reason: "VERIFIER_BACKEND_RUNTIME_FAILURE",
|
|
15901
|
+
stage,
|
|
15902
|
+
...caseReport(),
|
|
15903
|
+
detail,
|
|
15904
|
+
diagnostics: diagnosticsJson(r.diagnostics, true),
|
|
15905
|
+
verifier_backend: backendReport(),
|
|
15906
|
+
oracle_assurance: oracleAssuranceReport()
|
|
15907
|
+
}));
|
|
15908
|
+
} else {
|
|
15909
|
+
out3(`verify: the ${stage} suite exited but left a process holding stdout open \u2014 the stage is not quiescent, failing closed`);
|
|
15910
|
+
out3("verify: " + detail);
|
|
15911
|
+
renderStageDiagnostics(out3, stage, r);
|
|
15912
|
+
}
|
|
15913
|
+
return 2;
|
|
15914
|
+
};
|
|
15877
15915
|
let base;
|
|
15878
15916
|
try {
|
|
15879
15917
|
base = resolveBase(opts.base ?? "HEAD", cwd);
|
|
@@ -15933,6 +15971,10 @@ function runVerify(opts) {
|
|
|
15933
15971
|
);
|
|
15934
15972
|
}
|
|
15935
15973
|
const visible = runStage(visDir);
|
|
15974
|
+
if (visible.pipeHeldOpen) {
|
|
15975
|
+
cleanup([visRoot]);
|
|
15976
|
+
return notQuiescent("visible", visible);
|
|
15977
|
+
}
|
|
15936
15978
|
if (visible.failure === "backend" || visible.failure === "resource") {
|
|
15937
15979
|
cleanup([visRoot]);
|
|
15938
15980
|
const exhausted = visible.failure === "resource";
|
|
@@ -16002,6 +16044,10 @@ function runVerify(opts) {
|
|
|
16002
16044
|
}
|
|
16003
16045
|
const overlayBefore = overlayDigest(priDir, restored);
|
|
16004
16046
|
const pristine = runStage(priDir);
|
|
16047
|
+
if (pristine.pipeHeldOpen) {
|
|
16048
|
+
cleanup([visRoot, priRoot]);
|
|
16049
|
+
return notQuiescent("pristine", pristine);
|
|
16050
|
+
}
|
|
16005
16051
|
if (pristine.failure === "backend" || pristine.failure === "resource") {
|
|
16006
16052
|
cleanup([visRoot, priRoot]);
|
|
16007
16053
|
const exhausted = pristine.failure === "resource";
|
|
@@ -17883,6 +17929,22 @@ function parseStraceFileAccess(raw) {
|
|
|
17883
17929
|
}
|
|
17884
17930
|
return out3;
|
|
17885
17931
|
}
|
|
17932
|
+
function tracerDenialDiagnostic(stderr) {
|
|
17933
|
+
for (const raw of stderr.split("\n")) {
|
|
17934
|
+
const line = raw.trim();
|
|
17935
|
+
if (line.startsWith("strace:") && TRACER_DENIAL_RE.test(line)) return line;
|
|
17936
|
+
}
|
|
17937
|
+
return null;
|
|
17938
|
+
}
|
|
17939
|
+
function classifyTraceRun(r) {
|
|
17940
|
+
if (r.spawnError) return { kind: "tracer-error", diagnostic: `strace could not be launched: ${r.spawnError}` };
|
|
17941
|
+
const denial = tracerDenialDiagnostic(r.stderr);
|
|
17942
|
+
if (denial) return { kind: "tracer-error", diagnostic: denial };
|
|
17943
|
+
if (r.signal) return { kind: "tracer-error", diagnostic: `strace was killed by ${r.signal} before the trace completed` };
|
|
17944
|
+
if (r.traceFileCount === 0) return { kind: "tracer-error", diagnostic: "strace produced no trace files; the tracer did not attach (ptrace may be denied)" };
|
|
17945
|
+
if (r.accessCount === 0) return { kind: "tracer-error", diagnostic: "strace recorded no file syscalls; the tracer attached to nothing (ptrace may be denied)" };
|
|
17946
|
+
return { kind: "ok", exit: r.status ?? 1 };
|
|
17947
|
+
}
|
|
17886
17948
|
function inside3(root, path) {
|
|
17887
17949
|
const rel = relative6(resolve11(root), resolve11(path));
|
|
17888
17950
|
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep4}`) && !isAbsolute12(rel);
|
|
@@ -18009,11 +18071,9 @@ function rewriteTraceRoot(accesses, actualRoot) {
|
|
|
18009
18071
|
return { ...item, path: join22(TRACE_ROOT, ...rel.split("/")) };
|
|
18010
18072
|
});
|
|
18011
18073
|
}
|
|
18012
|
-
function
|
|
18013
|
-
const root = mkdtempSync5(join22(tmpdir6(), "tw-trace-base-"));
|
|
18074
|
+
function straceOnce(root, command, budget) {
|
|
18014
18075
|
const traceDir = mkdtempSync5(join22(tmpdir6(), "tw-trace-log-"));
|
|
18015
18076
|
try {
|
|
18016
|
-
materializeBase(base, cwd, root);
|
|
18017
18077
|
const prefix = join22(traceDir, "trace");
|
|
18018
18078
|
const traced = spawnSync5(
|
|
18019
18079
|
"strace",
|
|
@@ -18042,21 +18102,40 @@ function runOneTrace(cwd, base, command, budget) {
|
|
|
18042
18102
|
maxBuffer: 64 * 1024 * 1024
|
|
18043
18103
|
}
|
|
18044
18104
|
);
|
|
18045
|
-
|
|
18046
|
-
const logs = traceFiles(prefix);
|
|
18047
|
-
if (logs.length === 0) {
|
|
18048
|
-
throw new Error("strace produced no trace files");
|
|
18049
|
-
}
|
|
18105
|
+
const logs = traced.error ? [] : traceFiles(prefix);
|
|
18050
18106
|
const accesses = logs.flatMap((path) => parseStraceFileAccess(readFileSync20(path, "utf8")));
|
|
18051
|
-
|
|
18052
|
-
|
|
18053
|
-
|
|
18107
|
+
const raw = {
|
|
18108
|
+
spawnError: traced.error ? traced.error.message : null,
|
|
18109
|
+
status: traced.status,
|
|
18110
|
+
signal: traced.signal ?? null,
|
|
18111
|
+
stderr: String(traced.stderr ?? "").slice(0, 8192),
|
|
18112
|
+
traceFileCount: logs.length,
|
|
18113
|
+
accessCount: accesses.length
|
|
18054
18114
|
};
|
|
18115
|
+
return { raw, accesses };
|
|
18055
18116
|
} finally {
|
|
18056
|
-
rmSync9(root, { recursive: true, force: true });
|
|
18057
18117
|
rmSync9(traceDir, { recursive: true, force: true });
|
|
18058
18118
|
}
|
|
18059
18119
|
}
|
|
18120
|
+
function runOneTrace(cwd, base, command, budget) {
|
|
18121
|
+
const root = mkdtempSync5(join22(tmpdir6(), "tw-trace-base-"));
|
|
18122
|
+
try {
|
|
18123
|
+
materializeBase(base, cwd, root);
|
|
18124
|
+
const { raw, accesses } = straceOnce(root, command, budget);
|
|
18125
|
+
return { outcome: classifyTraceRun(raw), accesses: rewriteTraceRoot(accesses, root) };
|
|
18126
|
+
} finally {
|
|
18127
|
+
rmSync9(root, { recursive: true, force: true });
|
|
18128
|
+
}
|
|
18129
|
+
}
|
|
18130
|
+
function tracerPreflight(budget = 10) {
|
|
18131
|
+
const probeRoot = mkdtempSync5(join22(tmpdir6(), "tw-trace-probe-"));
|
|
18132
|
+
try {
|
|
18133
|
+
const { raw } = straceOnce(probeRoot, "true", Math.max(1, Math.min(budget, 10)));
|
|
18134
|
+
return classifyTraceRun(raw);
|
|
18135
|
+
} finally {
|
|
18136
|
+
rmSync9(probeRoot, { recursive: true, force: true });
|
|
18137
|
+
}
|
|
18138
|
+
}
|
|
18060
18139
|
function renderText2(report2) {
|
|
18061
18140
|
const out3 = (s = "") => void process.stdout.write(s + "\n");
|
|
18062
18141
|
out3("tamperward trace-verify \u2014 ADVISORY observation, not an enforcement verdict");
|
|
@@ -18143,6 +18222,15 @@ function runTraceVerify(opts = {}) {
|
|
|
18143
18222
|
`);
|
|
18144
18223
|
return 2;
|
|
18145
18224
|
}
|
|
18225
|
+
const preflight = tracerPreflight(budget);
|
|
18226
|
+
if (preflight.kind === "tracer-error") {
|
|
18227
|
+
process.stderr.write(
|
|
18228
|
+
`tamperward trace-verify: the tracer cannot observe in this environment, so no verifier input can be discovered \u2014 ${preflight.diagnostic}.
|
|
18229
|
+
This is a tracing/tooling limitation (e.g. ptrace denied by seccomp or Yama), not a verifier result.
|
|
18230
|
+
`
|
|
18231
|
+
);
|
|
18232
|
+
return 2;
|
|
18233
|
+
}
|
|
18146
18234
|
const atBase = trackedAt(base, cwd);
|
|
18147
18235
|
const tracked = new Set(atBase);
|
|
18148
18236
|
const covered = verifierCoveredInputs(command, atBase, policy);
|
|
@@ -18150,9 +18238,16 @@ function runTraceVerify(opts = {}) {
|
|
|
18150
18238
|
const runExits = [];
|
|
18151
18239
|
try {
|
|
18152
18240
|
for (let i = 0; i < runs; i++) {
|
|
18153
|
-
const
|
|
18154
|
-
|
|
18155
|
-
|
|
18241
|
+
const { outcome, accesses } = runOneTrace(cwd, base, command, budget);
|
|
18242
|
+
if (outcome.kind === "tracer-error") {
|
|
18243
|
+
process.stderr.write(
|
|
18244
|
+
`tamperward trace-verify: tracing failed during run ${i + 1}/${runs} \u2014 ${outcome.diagnostic}. No advisory report emitted.
|
|
18245
|
+
`
|
|
18246
|
+
);
|
|
18247
|
+
return 2;
|
|
18248
|
+
}
|
|
18249
|
+
observed.push(accesses);
|
|
18250
|
+
runExits.push(outcome.exit);
|
|
18156
18251
|
}
|
|
18157
18252
|
} catch (e) {
|
|
18158
18253
|
process.stderr.write(`tamperward trace-verify: tracing failed (${e instanceof Error ? e.message : String(e)}).
|
|
@@ -18199,7 +18294,7 @@ function parseTraceVerify(args) {
|
|
|
18199
18294
|
}
|
|
18200
18295
|
return out3;
|
|
18201
18296
|
}
|
|
18202
|
-
var TRACE_ROOT, CONFIG_BASENAMES;
|
|
18297
|
+
var TRACE_ROOT, TRACER_DENIAL_RE, CONFIG_BASENAMES;
|
|
18203
18298
|
var init_trace_verify = __esm({
|
|
18204
18299
|
"src/cli/trace-verify.ts"() {
|
|
18205
18300
|
"use strict";
|
|
@@ -18209,6 +18304,7 @@ var init_trace_verify = __esm({
|
|
|
18209
18304
|
init_policy_load();
|
|
18210
18305
|
init_verify();
|
|
18211
18306
|
TRACE_ROOT = "/__tamperward_trusted_base__";
|
|
18307
|
+
TRACER_DENIAL_RE = /\b(?:PTRACE_[A-Z_]+|ptrace|seccomp|no[-_ ]new[-_ ]privs|yama)\b|Operation not permitted|Permission denied/i;
|
|
18212
18308
|
CONFIG_BASENAMES = /* @__PURE__ */ new Set([
|
|
18213
18309
|
"package.json",
|
|
18214
18310
|
"pytest.ini",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.29.
|
|
3
|
+
"version": "2.29.15",
|
|
4
4
|
"description": "The deterministic agent-integrity gate. One ruleset, evaluated on the actual diff/commands as a verdict, enforced everywhere a change can be made.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "hexrift",
|