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.
Files changed (2) hide show
  1. package/dist/cli/index.js +114 -18
  2. 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 { exit: r.exit, secs, diagnostics: r.diagnostics };
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 runOneTrace(cwd, base, command, budget) {
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
- if (traced.error) throw traced.error;
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
- return {
18052
- exit: traced.status ?? 1,
18053
- accesses: rewriteTraceRoot(accesses, root)
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 result = runOneTrace(cwd, base, command, budget);
18154
- observed.push(result.accesses);
18155
- runExits.push(result.exit);
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.13",
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",