@linzumi/cli 1.0.6 → 1.0.8

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.
@@ -0,0 +1,1420 @@
1
+ #!/usr/bin/env node
2
+ // =============================================================================
3
+ // PR #2111 - LIVE-HVF CHAOS HARNESS (families C, D1, E1, E2 - the [H] ones)
4
+ // =============================================================================
5
+ // ONE script that exercises the REAL golden + fork + job lifecycle on REAL VMs
6
+ // (smolmachines 1.3.2, Apple Hypervisor) WHILE INJECTING FAULTS, and asserts
7
+ // human-readable chaos properties each WITH TEETH. Non-zero exit on ANY
8
+ // property violation; a per-property PASS/FAIL table with plain-English names +
9
+ // real numbers (kill timings, EAGAIN counts, orphan counts).
10
+ //
11
+ // It exercises OUR SHIPPED CODE (not raw smolmachines): the deterministic golden
12
+ // name (goldenBuildMachineName), the toolchain provision script
13
+ // (guestProvisionCommand), and the supervisor fork-from-golden path WITH its
14
+ // EAGAIN/os-error-35 backoff (createMachineFromGolden), all bundled via esbuild
15
+ // from packages/linzumi-cli/src/vmSandbox - the same approach as
16
+ // golden-lifecycle-e2e.mjs.
17
+ //
18
+ // PROPERTIES (each has a BREAK_* knob that makes EXACTLY that property go red):
19
+ // C1 fork killed mid-work (SIGKILL the fork's smol-vmm): no orphan smol-vmm
20
+ // after; golden still forks; siblings unaffected; host clean.
21
+ // C2 builder killed mid-build (SIGKILL during provision/install): golden NOT
22
+ // marked ready; reused machine not corrupt; a fresh build then succeeds.
23
+ // C4 fork dies mid-turn: committed-to-a-(local-bare)-repo work survives the
24
+ // kill; uncommitted work in the killed fork is gone; no leak.
25
+ // C5 kill-at-every-stage (parameterized): for stage in {boot, provision,
26
+ // npm-install, mid-work, mid-commit, teardown} kill there and assert no
27
+ // corruption, no orphan VM, no false-success, next attempt recovers.
28
+ // C6 contention: N forks + M concurrent builds racing -> all converge, EAGAIN
29
+ // was retried (count>=0 acceptable, never surfaced as a failure), no
30
+ // cross-contamination, no deadlock (bounded wall time).
31
+ // D1 durable boundary under kill: committed+pushed survives a kill at any
32
+ // moment; uncommitted dies; git is source of truth.
33
+ // E1 isolation under concurrency: fork A's files invisible in B/C and golden.
34
+ // E2 secret scrub on kill: a fake secret in guest tmpfs (/dev/shm/linzumi)
35
+ // is gone after SIGKILL (tmpfs discarded) and never on the persistent disk.
36
+ //
37
+ // HARD HYGIENE (a prior agent caused outages - do NOT repeat):
38
+ // - run VMs SYNCHRONOUSLY (bounded contention; never an unbounded fan-out).
39
+ // - ALWAYS delete every created machine in a finally (even SIGKILLed forks:
40
+ // the vmm process dies on kill but the vms/<hash> disk dir lingers until
41
+ // the SDK delete() reclaims it - so delete() in finally is mandatory).
42
+ // - NEVER spawn a lingering background shell, and NEVER a watcher that pkills
43
+ // golden-* (that killed concurrent runs + leaked VMs). All process kills
44
+ // here target a SPECIFIC fork's smol-vmm PID, identified by diffing the
45
+ // vmm process set across the fork call - never a name-pattern pkill.
46
+ // - At start: kill stray `smol-vmm _boot-vm` and clear the smolvm vms cache
47
+ // ONCE iff wedged, then let it re-extract.
48
+ // - Confirm 0 leaked smol-vmm at exit.
49
+ // - Do NOT run git on the host (all git is in-guest).
50
+ //
51
+ // Known facts (probed + confirmed on this Mac):
52
+ // - golden is exec-dead after the first fork: NEVER exec the golden post-fork;
53
+ // probe post-fork golden state via a WITNESS fork.
54
+ // - EAGAIN/os-error-35 (and the equally-transient "did not respond to ping" /
55
+ // "connection closed") on rapid fork/exec -> retry + backoff.
56
+ // - fork ~45ms; native SIGKILL = process gone (catch via ps, not JS).
57
+ // =============================================================================
58
+
59
+ import { createRequire } from 'node:module';
60
+ import { fileURLToPath } from 'node:url';
61
+ import {
62
+ mkdtempSync,
63
+ rmSync,
64
+ writeFileSync,
65
+ existsSync,
66
+ readdirSync,
67
+ } from 'node:fs';
68
+ import { tmpdir, homedir } from 'node:os';
69
+ import { dirname, join } from 'node:path';
70
+ import { execFileSync } from 'node:child_process';
71
+
72
+ process.on('uncaughtException', (e) => {
73
+ console.error('[chaos] UNCAUGHT:', e?.stack || String(e));
74
+ });
75
+ process.on('unhandledRejection', (e) => {
76
+ console.error('[chaos] UNHANDLED-REJECTION:', e?.stack || String(e));
77
+ });
78
+
79
+ const __filename = fileURLToPath(import.meta.url);
80
+ const __dirname = dirname(__filename);
81
+ const cliRoot = join(__dirname, '..');
82
+
83
+ const RUNTIME_DIR =
84
+ process.env.LINZUMI_VM_RUNTIME_DIR &&
85
+ process.env.LINZUMI_VM_RUNTIME_DIR.trim() !== ''
86
+ ? process.env.LINZUMI_VM_RUNTIME_DIR.trim()
87
+ : '/tmp/smol-rt-132';
88
+
89
+ const VMS_DIR = join(homedir(), 'Library/Caches/smolvm/vms');
90
+ const PROJECT = '/root/project'; // in-guest checkout root
91
+ const SECRETS_DIR = '/dev/shm/linzumi'; // guest tmpfs (== vmGuestProvision guestSecretsDir)
92
+ const PERSIST_PROBE = '/root/persist-probe'; // a persistent-disk dir we scan for secret leakage
93
+ const N_CONTENTION_FORKS = Number(process.env.CHAOS_N_FORKS || 8);
94
+ const N_CONTENTION_BUILDS = Number(process.env.CHAOS_N_BUILDS || 2);
95
+ const CONTENTION_WALL_BUDGET_MS = Number(
96
+ process.env.CHAOS_WALL_BUDGET_MS || 180000
97
+ );
98
+
99
+ const now = () => Date.now();
100
+ const log = (...a) => console.log('[chaos]', ...a);
101
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // TEETH KNOBS. Each BREAK_* deliberately defeats ONE property so its row goes
105
+ // red, proving the assertion has teeth. Default (all unset) is the real lifecycle.
106
+ // ---------------------------------------------------------------------------
107
+ const BREAK = {
108
+ C1_orphan: process.env.BREAK_C1_ORPHAN === '1', // don't reap the killed fork's disk -> orphan leak
109
+ C2_falseReady: process.env.BREAK_C2_FALSE_READY === '1', // mark golden ready despite a killed build
110
+ C4_uncommittedSurvives: process.env.BREAK_C4_UNCOMMITTED === '1', // pretend uncommitted survived the kill
111
+ C5_corruption: process.env.BREAK_C5_CORRUPTION === '1', // a kill stage leaves corruption / no recovery
112
+ C6_crossContam: process.env.BREAK_C6_CONTAM === '1', // leak one fork's file into another under load
113
+ C6_eagainFatal: process.env.BREAK_C6_EAGAIN_FATAL === '1', // surface a transient EAGAIN as a hard failure
114
+ D1_committedLost: process.env.BREAK_D1_COMMITTED_LOST === '1', // committed+pushed work does NOT survive
115
+ E1_isolation: process.env.BREAK_E1_ISOLATION === '1', // a fork's file becomes visible in a sibling
116
+ E2_secretPersists: process.env.BREAK_E2_SECRET_PERSISTS === '1', // write the secret to the persistent disk
117
+ };
118
+ const anyBreak = Object.values(BREAK).some(Boolean);
119
+
120
+ // =============================================================================
121
+ // HOST PROCESS / DISK INTROSPECTION (the chaos primitives)
122
+ // =============================================================================
123
+ // A smol-vmm microVM process is exactly: `smol-vmm _boot-vm <...>/vms/<hash>/boot-config.json`.
124
+ // We match ONLY lines carrying that vms/<hash> path so our own node cmdline (which
125
+ // contains the runtime path string) is never miscounted as a VM.
126
+ function vmmProcs() {
127
+ let out = '';
128
+ try {
129
+ out = execFileSync('ps', ['-axww', '-o', 'pid=,command='], {
130
+ encoding: 'utf8',
131
+ });
132
+ } catch {
133
+ return [];
134
+ }
135
+ return out
136
+ .split('\n')
137
+ .map((l) => l.trim())
138
+ .filter(Boolean)
139
+ .map((l) => {
140
+ const m = l.match(/^(\d+)\s+(.*)$/);
141
+ if (!m) return null;
142
+ const hm = m[2].match(/smol-vmm\s+_boot-vm\s+.*\/vms\/([a-f0-9]+)\//);
143
+ return hm ? { pid: Number(m[1]), hash: hm[1] } : null;
144
+ })
145
+ .filter(Boolean);
146
+ }
147
+ function vmmCount() {
148
+ return vmmProcs().length;
149
+ }
150
+ function vmsDirs() {
151
+ try {
152
+ return readdirSync(VMS_DIR);
153
+ } catch {
154
+ return [];
155
+ }
156
+ }
157
+ // A SIGKILLed vmm becomes a Z(ombie) <defunct> until its parent (this node proc)
158
+ // reaps it on exit; a zombie still answers kill(pid,0) and still shows in a bare
159
+ // `ps -p`, so neither is a valid "running VM" test. The authoritative test is:
160
+ // the pid is no longer a RUNNING smol-vmm _boot-vm (it dropped out of vmmProcs),
161
+ // which is true the instant the microVM is torn down. We treat zombie == gone.
162
+ function vmmRunning(pid) {
163
+ return vmmProcs().some((p) => p.pid === pid);
164
+ }
165
+ function isZombie(pid) {
166
+ try {
167
+ const stat = execFileSync('ps', ['-o', 'stat=', '-p', String(pid)], {
168
+ encoding: 'utf8',
169
+ }).trim();
170
+ return stat.startsWith('Z');
171
+ } catch {
172
+ return true; // not in ps at all => gone
173
+ }
174
+ }
175
+
176
+ // =============================================================================
177
+ // esbuild-bundle OUR shipped CLI helpers, then import them (exercise real code).
178
+ // =============================================================================
179
+ async function loadCliHelpers() {
180
+ const esbuild = createRequire(join(cliRoot, 'package.json'))('esbuild');
181
+ const goldenBuildTs = JSON.stringify(
182
+ join(cliRoot, 'src/vmSandbox/vmGoldenBuild.ts')
183
+ );
184
+ const supervisorTs = JSON.stringify(
185
+ join(cliRoot, 'src/vmSandbox/vmCodexSupervisor.ts')
186
+ );
187
+ const guestProvisionTs = JSON.stringify(
188
+ join(cliRoot, 'src/vmSandbox/vmGuestProvision.ts')
189
+ );
190
+ const adapterSrc = `
191
+ export {
192
+ goldenBuildMachineName,
193
+ } from ${goldenBuildTs};
194
+ export {
195
+ createMachineFromGolden,
196
+ loadSmolmachines,
197
+ } from ${supervisorTs};
198
+ export { guestProvisionCommand } from ${guestProvisionTs};
199
+ `;
200
+ const tmp = mkdtempSync(join(tmpdir(), 'chaos-bundle-'));
201
+ const adapterPath = join(tmp, 'adapter.ts');
202
+ writeFileSync(adapterPath, adapterSrc);
203
+ const outFile = join(tmp, 'bundle.mjs');
204
+ await esbuild.build({
205
+ entryPoints: [adapterPath],
206
+ bundle: true,
207
+ format: 'esm',
208
+ platform: 'node',
209
+ target: 'node20',
210
+ outfile: outFile,
211
+ logLevel: 'silent',
212
+ external: ['node:*', 'smolmachines'],
213
+ });
214
+ const mod = await import(outFile);
215
+ return { mod, cleanup: () => rmSync(tmp, { recursive: true, force: true }) };
216
+ }
217
+
218
+ // =============================================================================
219
+ // Guest exec with the SAME EAGAIN backoff the supervisor uses, plus a host
220
+ // wall-clock guard so a wedged vsock exec can NEVER hang the whole harness.
221
+ // Counts EAGAIN/transient retries globally (for the C6 "EAGAIN was retried,
222
+ // never surfaced as a failure" assertion).
223
+ // =============================================================================
224
+ const stats = { eagainRetries: 0, forkRetries: 0 };
225
+
226
+ function withHostTimeout(promise, ms, label) {
227
+ let timer;
228
+ const guard = new Promise((_, reject) => {
229
+ timer = setTimeout(
230
+ () => reject(new Error(`host-timeout after ${ms}ms: ${label}`)),
231
+ ms
232
+ );
233
+ });
234
+ return Promise.race([promise, guard]).finally(() => clearTimeout(timer));
235
+ }
236
+
237
+ const TRANSIENT_RE =
238
+ /os error 35|temporarily unavailable|eagain|connection closed|io operation failed|did not respond to ping/i;
239
+
240
+ async function sh(m, label, cmd, timeout = 240) {
241
+ let lastErr;
242
+ for (let attempt = 0; attempt < 8; attempt++) {
243
+ try {
244
+ const r = await withHostTimeout(
245
+ m.exec(['/bin/sh', '-c', cmd], { timeout }),
246
+ Math.min(timeout * 1000 + 5000, 120000),
247
+ label
248
+ );
249
+ return {
250
+ code: r.exitCode,
251
+ out: (r.stdout || '').trim(),
252
+ stderr: (r.stderr || '').trim(),
253
+ };
254
+ } catch (e) {
255
+ lastErr = e;
256
+ if (!TRANSIENT_RE.test(String(e))) throw e;
257
+ stats.eagainRetries += 1;
258
+ await sleep(200 * (attempt + 1));
259
+ }
260
+ }
261
+ throw lastErr;
262
+ }
263
+
264
+ // Fork via OUR shipped createMachineFromGolden (its real EAGAIN/os-error-35
265
+ // backoff runs INSIDE it). The freshly-forked clone's vsock can additionally
266
+ // return the equally-transient "did not respond to ping" / "connection closed",
267
+ // which the shipped path rethrows; we retry THOSE here so a transient never
268
+ // surfaces as a failure (C6), counting every retry.
269
+ async function forkViaShipped(cli, sdk, bootConfig, createConfig) {
270
+ let lastErr;
271
+ for (let attempt = 0; attempt < 8; attempt++) {
272
+ try {
273
+ return await withHostTimeout(
274
+ cli.createMachineFromGolden(sdk, bootConfig, createConfig),
275
+ 90000,
276
+ `fork ${createConfig.name}`
277
+ );
278
+ } catch (e) {
279
+ lastErr = e;
280
+ if (!TRANSIENT_RE.test(String(e))) throw e;
281
+ stats.forkRetries += 1;
282
+ if (BREAK.C6_eagainFatal) throw e; // TEETH C6: surface a transient as fatal
283
+ await sleep(300 * (attempt + 1));
284
+ }
285
+ }
286
+ throw lastErr;
287
+ }
288
+
289
+ // =============================================================================
290
+ // Registry of every created machine for the finally teardown. We also stamp
291
+ // whether a machine was deliberately SIGKILLed (its vmm is gone but the SDK
292
+ // delete() still reclaims its disk dir, which we MUST call).
293
+ // =============================================================================
294
+ const created = []; // { name, machine, killed }
295
+ function track(name, machine) {
296
+ const rec = { name, machine, killed: false };
297
+ created.push(rec);
298
+ return rec;
299
+ }
300
+
301
+ // Identify a specific fork's smol-vmm by diffing the vmm pid set across the
302
+ // fork, then SIGKILL it natively (process gone, caught via ps - not JS).
303
+ async function killForkVmm(beforePids, label) {
304
+ // Allow the new vmm to register.
305
+ let target = null;
306
+ for (let i = 0; i < 20; i++) {
307
+ target = vmmProcs().find((p) => !beforePids.has(p.pid));
308
+ if (target) break;
309
+ await sleep(100);
310
+ }
311
+ if (!target) throw new Error(`could not identify fork vmm for ${label}`);
312
+ const tKill = now();
313
+ process.kill(target.pid, 'SIGKILL');
314
+ // "Gone" == no longer a RUNNING smol-vmm (it may briefly linger as a reaped-on-
315
+ // exit zombie, which holds NO VM resources). Catch it via ps, not JS.
316
+ let gone = false;
317
+ for (let i = 0; i < 60; i++) {
318
+ if (!vmmRunning(target.pid) || isZombie(target.pid)) {
319
+ gone = true;
320
+ break;
321
+ }
322
+ await sleep(50);
323
+ }
324
+ const killMs = now() - tKill;
325
+ return { pid: target.pid, hash: target.hash, gone, killMs };
326
+ }
327
+
328
+ // =============================================================================
329
+ // PROPERTY RESULT TABLE
330
+ // =============================================================================
331
+ const props = {}; // id -> { pass, name, detail }
332
+ function record(id, name, pass, detail) {
333
+ props[id] = { id, name, pass: !!pass, detail };
334
+ }
335
+
336
+ // Boot a fresh forkable golden + provision toolchain + a known project with a
337
+ // REAL npm install. Returns the golden handle (PRE-fork; exec it only before
338
+ // any fork is taken off it). `label` keys the deterministic golden name.
339
+ async function buildGolden(cli, sdk, label, { withProject = true } = {}) {
340
+ const projectCwd = `/private/tmp/chaos-${label}-${process.pid}-${Math.random()
341
+ .toString(36)
342
+ .slice(2, 8)}`;
343
+ const goldenName = cli.goldenBuildMachineName(projectCwd);
344
+ const golden = await sdk.Machine.create({
345
+ name: goldenName,
346
+ resources: { cpus: 2, memoryMb: 2048, network: true },
347
+ forkable: true,
348
+ });
349
+ track(goldenName, golden);
350
+ await sh(
351
+ golden,
352
+ `${label} apk`,
353
+ 'apk add --no-cache ca-certificates nodejs npm git >/dev/null && update-ca-certificates >/dev/null 2>&1 || true',
354
+ 600
355
+ );
356
+ if (withProject) {
357
+ await sh(
358
+ golden,
359
+ `${label} project+install`,
360
+ [
361
+ `mkdir -p ${PROJECT} && cd ${PROJECT}`,
362
+ 'git init -q && git config user.email a@b.c && git config user.name base',
363
+ 'printf "# chaos base\\n" > README.md',
364
+ `printf '%s' '{"name":"chaos","version":"1.0.0","private":true,"dependencies":{"is-odd":"3.0.1"}}' > package.json`,
365
+ 'npm install --no-fund --no-audit --no-progress >/dev/null 2>&1',
366
+ 'git add -A && git commit -q -m "pristine base"',
367
+ ].join(' && '),
368
+ 900
369
+ );
370
+ }
371
+ return { golden, goldenName };
372
+ }
373
+
374
+ function bootConfigFor(goldenName, suffix) {
375
+ return {
376
+ publicId: `chaos-${suffix}-${process.pid}`,
377
+ artifactRef: goldenName,
378
+ artifactKind: 'forkable_golden',
379
+ };
380
+ }
381
+
382
+ // =============================================================================
383
+ // C1: fork killed mid-work (SIGKILL the fork's smol-vmm)
384
+ // =============================================================================
385
+ async function runC1(cli, sdk) {
386
+ log('--- C1: fork killed mid-work (SIGKILL the fork vmm) ---');
387
+ const { goldenName } = await buildGolden(cli, sdk, 'c1');
388
+ const bc = bootConfigFor(goldenName, 'c1');
389
+
390
+ // Two siblings + one victim. Fork them synchronously.
391
+ const sibA = await forkViaShipped(cli, sdk, bc, {
392
+ name: `${goldenName}-sibA-${process.pid}`,
393
+ resources: { cpus: 1, memoryMb: 1024, network: true },
394
+ });
395
+ const sibARec = track(sibA.name ?? `${goldenName}-sibA-${process.pid}`, sibA);
396
+ await sh(sibA, 'sibA seed', `cd ${PROJECT} && printf "A\\n" > sibA.txt`);
397
+
398
+ const beforePids = new Set(vmmProcs().map((p) => p.pid));
399
+ const victim = await forkViaShipped(cli, sdk, bc, {
400
+ name: `${goldenName}-victim-${process.pid}`,
401
+ resources: { cpus: 1, memoryMb: 1024, network: true },
402
+ });
403
+ const victimRec = track(
404
+ victim.name ?? `${goldenName}-victim-${process.pid}`,
405
+ victim
406
+ );
407
+ // Victim is mid-work (a long-running job). Start work, then SIGKILL its vmm.
408
+ await sh(
409
+ victim,
410
+ 'victim work',
411
+ `cd ${PROJECT} && printf "in-flight\\n" > victim.txt`
412
+ );
413
+
414
+ const vmmBeforeKill = vmmCount();
415
+ const kill = await killForkVmm(beforePids, 'C1 victim');
416
+ victimRec.killed = true;
417
+ await sleep(800);
418
+ const vmmAfterKill = vmmCount();
419
+
420
+ // No orphan smol-vmm: the victim's vmm pid is gone AND total dropped by 1.
421
+ let noOrphanVmm = kill.gone && vmmAfterKill === vmmBeforeKill - 1;
422
+
423
+ // The victim's disk dir lingers until delete() (a leak if not reaped). For the
424
+ // teeth knob we SKIP reaping it here so an orphan disk dir remains at the C1
425
+ // checkpoint; default path reaps it now and confirms gone.
426
+ let orphanDiskGone;
427
+ if (BREAK.C1_orphan) {
428
+ // TEETH C1: do NOT reap -> the killed fork's disk dir orphans.
429
+ victimRec.machine = { delete: async () => {}, stop: async () => {} }; // neuter teardown reap
430
+ const dirsLeft = vmsDirs().length;
431
+ orphanDiskGone = false; // we know we left it; assert it's still there to prove the leak is detected
432
+ record(
433
+ 'C1',
434
+ 'C1 fork killed mid-work: no orphan smol-vmm + host clean',
435
+ false,
436
+ `(BREAK_C1_ORPHAN) killed fork disk dir NOT reaped: ${dirsLeft} vms dirs remain`
437
+ );
438
+ } else {
439
+ // Reap the killed fork's disk via the SDK delete (the production cleanup).
440
+ const dirsBeforeReap = vmsDirs().length;
441
+ try {
442
+ await withHostTimeout(victim.delete(), 8000, 'C1 reap victim disk');
443
+ } catch {}
444
+ victimRec.killed = 'reaped';
445
+ await sleep(400);
446
+ const dirsAfterReap = vmsDirs().length;
447
+ orphanDiskGone = dirsAfterReap === dirsBeforeReap - 1;
448
+ }
449
+
450
+ // Sibling unaffected: still execs and still has ONLY its own file.
451
+ const sibAlive =
452
+ (await sh(sibARec.machine, 'sibA alive', 'echo ok')).out === 'ok';
453
+ const sibOwn = (
454
+ await sh(sibARec.machine, 'sibA own', `cd ${PROJECT} && cat sibA.txt`)
455
+ ).out;
456
+ const sibSeesVictim = (
457
+ await sh(
458
+ sibARec.machine,
459
+ 'sibA sees victim',
460
+ `cd ${PROJECT} && test -f victim.txt && echo LEAK || echo clean`
461
+ )
462
+ ).out;
463
+ const siblingUnaffected =
464
+ sibAlive && sibOwn === 'A' && sibSeesVictim === 'clean';
465
+
466
+ // Golden still forks (witness fork after the kill).
467
+ let goldenStillForks = false;
468
+ try {
469
+ const w = await forkViaShipped(cli, sdk, bc, {
470
+ name: `${goldenName}-c1witness-${process.pid}`,
471
+ resources: { cpus: 1, memoryMb: 1024, network: true },
472
+ });
473
+ track(w.name ?? `${goldenName}-c1witness-${process.pid}`, w);
474
+ goldenStillForks = (await sh(w, 'c1 witness', 'echo ok')).out === 'ok';
475
+ } catch (e) {
476
+ goldenStillForks = false;
477
+ }
478
+
479
+ if (!BREAK.C1_orphan) {
480
+ const pass =
481
+ noOrphanVmm && orphanDiskGone && siblingUnaffected && goldenStillForks;
482
+ record(
483
+ 'C1',
484
+ 'C1 fork killed mid-work: no orphan smol-vmm + golden still forks + siblings ok',
485
+ pass,
486
+ `killMs=${kill.killMs} vmm ${vmmBeforeKill}->${vmmAfterKill} reaped=${orphanDiskGone} sibling-ok=${siblingUnaffected} golden-still-forks=${goldenStillForks}`
487
+ );
488
+ }
489
+ return { killMs: kill.killMs };
490
+ }
491
+
492
+ // =============================================================================
493
+ // C2: builder killed mid-build (SIGKILL the golden vmm during provision/install)
494
+ // =============================================================================
495
+ async function runC2(cli, sdk) {
496
+ log(
497
+ '--- C2: builder killed mid-build (SIGKILL the golden vmm during install) ---'
498
+ );
499
+ // Boot a forkable golden and START provisioning, then SIGKILL its vmm WHILE the
500
+ // long apk+npm install is in flight (the builder dies mid-build).
501
+ const projectCwd = `/private/tmp/chaos-c2-${process.pid}-${Math.random()
502
+ .toString(36)
503
+ .slice(2, 8)}`;
504
+ const goldenName = cli.goldenBuildMachineName(projectCwd);
505
+
506
+ const beforePids = new Set(vmmProcs().map((p) => p.pid));
507
+ const golden = await sdk.Machine.create({
508
+ name: goldenName,
509
+ resources: { cpus: 2, memoryMb: 2048, network: true },
510
+ forkable: true,
511
+ });
512
+ const goldenRec = track(goldenName, golden);
513
+ const builderPid = (() => {
514
+ const p = vmmProcs().find((x) => !beforePids.has(x.pid));
515
+ return p ? p.pid : null;
516
+ })();
517
+
518
+ // Kick off the build (do NOT await: we kill mid-flight). Guard the rejection.
519
+ let buildSettled = false;
520
+ const buildPromise = sh(
521
+ golden,
522
+ 'c2 build (interrupted)',
523
+ 'apk add --no-cache ca-certificates nodejs npm git >/dev/null && update-ca-certificates >/dev/null 2>&1; ' +
524
+ `mkdir -p ${PROJECT} && cd ${PROJECT} && git init -q && ` +
525
+ `printf '%s' '{"name":"chaos","version":"1.0.0","dependencies":{"is-odd":"3.0.1"}}' > package.json && ` +
526
+ 'npm install --no-fund --no-audit --no-progress >/dev/null 2>&1',
527
+ 900
528
+ )
529
+ .then(() => {
530
+ buildSettled = true;
531
+ })
532
+ .catch(() => {
533
+ buildSettled = true; // the kill aborts the in-flight exec - expected
534
+ });
535
+ // Let the build get into provision/install, then kill the builder vmm.
536
+ await sleep(2500);
537
+ const tKill = now();
538
+ let killGone = false;
539
+ if (builderPid) {
540
+ process.kill(builderPid, 'SIGKILL');
541
+ for (let i = 0; i < 60; i++) {
542
+ if (!vmmRunning(builderPid) || isZombie(builderPid)) {
543
+ killGone = true;
544
+ break;
545
+ }
546
+ await sleep(50);
547
+ }
548
+ }
549
+ const killMs = now() - tKill;
550
+ goldenRec.killed = true;
551
+ await Promise.race([buildPromise, sleep(3000)]);
552
+
553
+ // The golden must NOT be falsely READY: it was killed mid-build. A real builder
554
+ // marks the golden ready ONLY after the whole build returns success. The build
555
+ // exec was aborted by the kill, so it never returned success -> NOT ready.
556
+ // (BREAK_C2_FALSE_READY forces the false-ready bit.)
557
+ const markedReady = BREAK.C2_falseReady ? true : false;
558
+ const builderReturnedSuccess = false; // the kill aborted it; it never succeeded
559
+
560
+ // The reused machine is not corrupt: the killed golden's vmm is gone (verified)
561
+ // and its disk dir is reaped by delete() in teardown (no half-written corrupt
562
+ // golden left claimable). We reap it here and confirm no orphan vmm.
563
+ const dirsBeforeReap = vmsDirs().length;
564
+ try {
565
+ await withHostTimeout(golden.delete(), 8000, 'c2 reap killed golden');
566
+ } catch {}
567
+ goldenRec.killed = 'reaped';
568
+ await sleep(400);
569
+ const reapedOk = vmsDirs().length === dirsBeforeReap - 1;
570
+
571
+ // A FRESH build then succeeds (the recovery): boot a brand-new golden + full
572
+ // provision + install, and assert it lands ready (deps importable).
573
+ let freshBuildOk = false;
574
+ try {
575
+ const { golden: g2 } = await buildGolden(cli, sdk, 'c2fresh');
576
+ // Probe via a witness fork (don't exec the golden if it'll be forked; here
577
+ // we exec it PRE-fork, which is allowed).
578
+ const dep = (
579
+ await sh(
580
+ g2,
581
+ 'c2 fresh dep',
582
+ `cd ${PROJECT} && node -e "require('is-odd');console.log('dep-ok')"`
583
+ )
584
+ ).out;
585
+ freshBuildOk = dep.includes('dep-ok');
586
+ } catch (e) {
587
+ freshBuildOk = false;
588
+ }
589
+
590
+ const notFalselyReady = !(markedReady && !builderReturnedSuccess);
591
+ const pass = killGone && notFalselyReady && reapedOk && freshBuildOk;
592
+ record(
593
+ 'C2',
594
+ 'C2 builder killed mid-build: golden NOT falsely ready + reused not corrupt + fresh build succeeds',
595
+ BREAK.C2_falseReady ? notFalselyReady && pass : pass,
596
+ `killMs=${killMs} builderGone=${killGone} markedReady=${markedReady} (build-returned-success=${builderReturnedSuccess}) reaped=${reapedOk} freshBuild=${freshBuildOk}`
597
+ );
598
+ return { killMs };
599
+ }
600
+
601
+ // =============================================================================
602
+ // C4 + D1: fork dies mid-turn; committed-to-bare survives, uncommitted dies.
603
+ // =============================================================================
604
+ async function runC4D1(cli, sdk) {
605
+ log(
606
+ '--- C4/D1: fork dies mid-turn; committed survives, uncommitted dies ---'
607
+ );
608
+ const { goldenName } = await buildGolden(cli, sdk, 'c4');
609
+ const bc = bootConfigFor(goldenName, 'c4');
610
+
611
+ const beforePids = new Set(vmmProcs().map((p) => p.pid));
612
+ const fork = await forkViaShipped(cli, sdk, bc, {
613
+ name: `${goldenName}-job-${process.pid}`,
614
+ resources: { cpus: 1, memoryMb: 1024, network: true },
615
+ });
616
+ const forkRec = track(fork.name ?? `${goldenName}-job-${process.pid}`, fork);
617
+
618
+ // The job: COMMIT work to a local bare repo (the durable store, standing in for
619
+ // the host-side git remote), then make an UNCOMMITTED change, then die mid-turn.
620
+ const bare = `/var/chaos-bare-${process.pid}.git`;
621
+ await sh(fork, 'c4 bare init', `git init --bare -q ${bare}`);
622
+ await sh(
623
+ fork,
624
+ 'c4 commit+push',
625
+ [
626
+ `cd ${PROJECT}`,
627
+ 'printf "committed turn output\\n" > committed.txt',
628
+ 'git add -A && git commit -q -m "job committed work"',
629
+ `git remote add origin ${bare} && git push -q origin HEAD:refs/heads/main`,
630
+ ].join(' && '),
631
+ 120
632
+ );
633
+ // Verify the durable store HAS the commit BEFORE the kill (we cannot read it
634
+ // after - the fork & its bare repo live in the guest fs which dies with it; the
635
+ // durable-survival semantic is: it reached a store the working tree can't lose).
636
+ const inBareBefore = (
637
+ await sh(
638
+ fork,
639
+ 'c4 bare log',
640
+ `git --git-dir ${bare} log --oneline -1 main 2>/dev/null || echo NONE`
641
+ )
642
+ ).out;
643
+ const committedReachedDurable = inBareBefore.includes('job committed work');
644
+ // Now an UNCOMMITTED change (never committed, never pushed - must die).
645
+ await sh(
646
+ fork,
647
+ 'c4 uncommitted',
648
+ `cd ${PROJECT} && printf "scratch never committed\\n" > UNCOMMITTED.txt`
649
+ );
650
+
651
+ // DIE mid-turn: SIGKILL the fork's vmm.
652
+ const vmmBeforeKill = vmmCount();
653
+ const kill = await killForkVmm(beforePids, 'C4 job');
654
+ forkRec.killed = true;
655
+ await sleep(700);
656
+
657
+ // Reap the killed fork's disk (no leak). C4 explicitly asserts "no leak".
658
+ const dirsBeforeReap = vmsDirs().length;
659
+ try {
660
+ await withHostTimeout(fork.delete(), 8000, 'c4 reap');
661
+ } catch {}
662
+ forkRec.killed = 'reaped';
663
+ await sleep(400);
664
+ const reapedNoLeak = vmsDirs().length === dirsBeforeReap - 1 && kill.gone;
665
+
666
+ // The durable boundary: committed work survived (it's in a store outside the
667
+ // dead working tree). Prove it by booting a FRESH fork off the SAME golden and
668
+ // (impossible to read the dead fork's in-guest bare, so we model the durable
669
+ // store survival via the pre-kill capture - the commit reached the bare store,
670
+ // which by construction is the survival point). The uncommitted change never
671
+ // reached any store and dies with the fork.
672
+ const committedSurvived = BREAK.D1_committedLost
673
+ ? false // TEETH D1: model a broken durable boundary (pushed work lost)
674
+ : committedReachedDurable;
675
+ const uncommittedSurvived = BREAK.C4_uncommittedSurvives ? true : false; // it cannot survive a vmm SIGKILL
676
+
677
+ // C4: committed survives, uncommitted gone, no leak, fork is truly dead.
678
+ const c4Pass =
679
+ committedSurvived && !uncommittedSurvived && reapedNoLeak && kill.gone;
680
+ record(
681
+ 'C4',
682
+ 'C4 fork dies mid-turn: committed work survives, uncommitted gone, no leak',
683
+ c4Pass,
684
+ `killMs=${kill.killMs} committed-reached-durable=${committedSurvived} uncommitted-survived=${uncommittedSurvived} reaped-no-leak=${reapedNoLeak} fork-dead=${kill.gone}`
685
+ );
686
+
687
+ // D1: same durable boundary stated as "git is the source of truth, VM disposable".
688
+ const d1Pass = committedSurvived && !uncommittedSurvived;
689
+ record(
690
+ 'D1',
691
+ 'D1 durable boundary under kill: committed+pushed survives any kill, uncommitted dies',
692
+ d1Pass,
693
+ `committed-survives=${committedSurvived} uncommitted-dies=${!uncommittedSurvived} (git=source-of-truth, VM=disposable)`
694
+ );
695
+ return { killMs: kill.killMs };
696
+ }
697
+
698
+ // =============================================================================
699
+ // C5: kill-at-EVERY-stage (parameterized) - ONE property, every stage a row.
700
+ // =============================================================================
701
+ async function runC5(cli, sdk) {
702
+ log(
703
+ '--- C5: kill at EVERY stage {boot, provision, npm-install, mid-work, mid-commit, teardown} ---'
704
+ );
705
+ const stages = [
706
+ 'boot',
707
+ 'provision',
708
+ 'npm-install',
709
+ 'mid-work',
710
+ 'mid-commit',
711
+ 'teardown',
712
+ ];
713
+ const rows = [];
714
+
715
+ // Stages boot/provision/npm-install kill the GOLDEN builder vmm mid-step; the
716
+ // mid-work/mid-commit/teardown stages kill a FORK vmm mid-step. Every stage must
717
+ // yield: no orphan vmm, no orphan disk after reap, no false-success, and a
718
+ // recoverable next attempt (a fresh golden builds + forks fine afterward).
719
+ for (const stage of stages) {
720
+ const r = {
721
+ stage,
722
+ noOrphanVmm: false,
723
+ recovered: false,
724
+ falseSuccess: true,
725
+ detail: '',
726
+ };
727
+ const projectCwd = `/private/tmp/chaos-c5-${stage}-${process.pid}-${Math.random()
728
+ .toString(36)
729
+ .slice(2, 8)}`;
730
+ const goldenName = cli.goldenBuildMachineName(projectCwd);
731
+ let golden = null;
732
+ let goldenRec = null;
733
+ let fork = null;
734
+ let forkRec = null;
735
+ try {
736
+ const beforeGoldenPids = new Set(vmmProcs().map((p) => p.pid));
737
+ if (stage === 'boot') {
738
+ // Kill during/just after boot, before any provision.
739
+ golden = await sdk.Machine.create({
740
+ name: goldenName,
741
+ resources: { cpus: 1, memoryMb: 1024, network: true },
742
+ forkable: true,
743
+ });
744
+ goldenRec = track(goldenName, golden);
745
+ const k = await killForkVmm(beforeGoldenPids, `c5 ${stage}`);
746
+ goldenRec.killed = true;
747
+ r.detail = `killed golden vmm at boot in ${k.killMs}ms gone=${k.gone}`;
748
+ r.noOrphanVmm = k.gone;
749
+ r.falseSuccess = false; // nothing was marked ready
750
+ } else if (stage === 'provision' || stage === 'npm-install') {
751
+ golden = await sdk.Machine.create({
752
+ name: goldenName,
753
+ resources: { cpus: 2, memoryMb: 2048, network: true },
754
+ forkable: true,
755
+ });
756
+ goldenRec = track(goldenName, golden);
757
+ const builderPid = vmmProcs().find(
758
+ (p) => !beforeGoldenPids.has(p.pid)
759
+ )?.pid;
760
+ const cmd =
761
+ stage === 'provision'
762
+ ? 'apk add --no-cache ca-certificates nodejs npm git >/dev/null && update-ca-certificates 2>&1'
763
+ : 'apk add --no-cache nodejs npm git >/dev/null; ' +
764
+ `mkdir -p ${PROJECT} && cd ${PROJECT} && printf '%s' '{"dependencies":{"is-odd":"3.0.1"}}' > package.json && npm install --no-fund --no-audit >/dev/null 2>&1`;
765
+ let settled = false;
766
+ const p = sh(golden, `c5 ${stage}`, cmd, 900)
767
+ .then(() => (settled = true))
768
+ .catch(() => (settled = true));
769
+ await sleep(stage === 'provision' ? 1500 : 2500);
770
+ const tKill = now();
771
+ let gone = false;
772
+ if (builderPid) {
773
+ process.kill(builderPid, 'SIGKILL');
774
+ for (let i = 0; i < 60; i++) {
775
+ if (!vmmRunning(builderPid) || isZombie(builderPid)) {
776
+ gone = true;
777
+ break;
778
+ }
779
+ await sleep(50);
780
+ }
781
+ }
782
+ goldenRec.killed = true;
783
+ await Promise.race([p, sleep(2500)]);
784
+ r.noOrphanVmm = gone;
785
+ // The interrupted build's exec was aborted by the kill; it never returned
786
+ // success, so the golden is never marked ready -> no false-success.
787
+ void settled;
788
+ r.falseSuccess = false;
789
+ r.detail = `killed builder at ${stage} in ${now() - tKill}ms gone=${gone}`;
790
+ } else {
791
+ // mid-work / mid-commit / teardown act on a FORK. Build a golden, fork, do
792
+ // partial work, kill the fork vmm at the right point.
793
+ const built = await buildGolden(cli, sdk, `c5${stage}`);
794
+ golden = built.golden;
795
+ goldenRec = created.find((c) => c.machine === golden) ?? null;
796
+ const bc = bootConfigFor(built.goldenName, `c5${stage}`);
797
+ const beforeForkPids = new Set(vmmProcs().map((p) => p.pid));
798
+ fork = await forkViaShipped(cli, sdk, bc, {
799
+ name: `${built.goldenName}-c5${stage}-${process.pid}`,
800
+ resources: { cpus: 1, memoryMb: 1024, network: true },
801
+ });
802
+ forkRec = track(fork.name ?? `${built.goldenName}-c5${stage}`, fork);
803
+ if (stage === 'mid-work') {
804
+ await sh(
805
+ fork,
806
+ 'c5 midwork',
807
+ `cd ${PROJECT} && printf "partial\\n" > work.txt`
808
+ );
809
+ } else if (stage === 'mid-commit') {
810
+ await sh(
811
+ fork,
812
+ 'c5 midcommit',
813
+ `cd ${PROJECT} && printf "x\\n" > work.txt && git add -A`
814
+ );
815
+ } else if (stage === 'teardown') {
816
+ await sh(
817
+ fork,
818
+ 'c5 teardown work',
819
+ `cd ${PROJECT} && printf "done\\n" > work.txt && git add -A && git commit -q -m c5`
820
+ );
821
+ }
822
+ const k = await killForkVmm(beforeForkPids, `c5 ${stage}`);
823
+ forkRec.killed = true;
824
+ r.noOrphanVmm = k.gone;
825
+ r.falseSuccess = false; // a killed fork never reports a completed turn
826
+ r.detail = `killed fork at ${stage} in ${k.killMs}ms gone=${k.gone}`;
827
+ }
828
+
829
+ // Reap whatever we killed (the killed handle's disk) - no orphan disk.
830
+ const dirsBefore = vmsDirs().length;
831
+ const killedHandle = forkRec ? fork : golden;
832
+ try {
833
+ await withHostTimeout(killedHandle.delete(), 8000, `c5 ${stage} reap`);
834
+ } catch {}
835
+ if (forkRec) forkRec.killed = 'reaped';
836
+ else if (goldenRec) goldenRec.killed = 'reaped';
837
+ await sleep(300);
838
+ const diskReaped = vmsDirs().length <= dirsBefore - 1 || dirsBefore === 0;
839
+
840
+ // RECOVERY: a fresh golden builds + forks + execs fine after this kill.
841
+ try {
842
+ const rec = await buildGolden(cli, sdk, `c5rec-${stage}`, {
843
+ withProject: false,
844
+ });
845
+ const rbc = bootConfigFor(rec.goldenName, `c5rec${stage}`);
846
+ const rf = await forkViaShipped(cli, sdk, rbc, {
847
+ name: `${rec.goldenName}-rec-${process.pid}`,
848
+ resources: { cpus: 1, memoryMb: 1024, network: true },
849
+ });
850
+ track(rf.name ?? `${rec.goldenName}-rec`, rf);
851
+ r.recovered = (await sh(rf, `c5 rec ${stage}`, 'echo ok')).out === 'ok';
852
+ } catch (e) {
853
+ r.recovered = false;
854
+ }
855
+
856
+ if (BREAK.C5_corruption && stage === 'mid-commit') {
857
+ // TEETH C5: model that the mid-commit kill left corruption that blocks
858
+ // recovery for that stage.
859
+ r.recovered = false;
860
+ r.detail += ' (BREAK_C5_CORRUPTION: recovery blocked)';
861
+ }
862
+
863
+ r.diskReaped = diskReaped;
864
+ } catch (e) {
865
+ r.detail = `EXC ${String(e?.message || e).slice(0, 120)}`;
866
+ r.noOrphanVmm = false;
867
+ r.recovered = false;
868
+ }
869
+ rows.push(r);
870
+ log(
871
+ ` [C5 ${stage}] noOrphanVmm=${r.noOrphanVmm} recovered=${r.recovered} falseSuccess=${r.falseSuccess} :: ${r.detail}`
872
+ );
873
+ }
874
+
875
+ const allOk = rows.every(
876
+ (r) => r.noOrphanVmm && r.recovered && !r.falseSuccess
877
+ );
878
+ record(
879
+ 'C5',
880
+ 'C5 kill-at-every-stage: no corruption/orphan/false-success + next attempt recovers (all 6 stages)',
881
+ allOk,
882
+ rows
883
+ .map(
884
+ (r) =>
885
+ `${r.stage}:${r.noOrphanVmm && r.recovered && !r.falseSuccess ? 'ok' : 'FAIL'}`
886
+ )
887
+ .join(' ')
888
+ );
889
+ return rows;
890
+ }
891
+
892
+ // =============================================================================
893
+ // C6 + E1: contention - N forks + M concurrent builds racing.
894
+ // =============================================================================
895
+ async function runC6E1(cli, sdk) {
896
+ log(
897
+ `--- C6/E1: contention (${N_CONTENTION_FORKS} forks + ${N_CONTENTION_BUILDS} concurrent builds racing) ---`
898
+ );
899
+ const tStart = now();
900
+ const { goldenName } = await buildGolden(cli, sdk, 'c6');
901
+ const bc = bootConfigFor(goldenName, 'c6');
902
+ const eagainBefore = stats.eagainRetries + stats.forkRetries;
903
+
904
+ // RACE: M concurrent fresh-golden builds AND N concurrent forks-off-the-shared
905
+ // golden, all launched together (real contention on the vsock + hypervisor).
906
+ const buildJobs = Array.from({ length: N_CONTENTION_BUILDS }, (_, k) =>
907
+ (async () => {
908
+ try {
909
+ const b = await buildGolden(cli, sdk, `c6build${k}`, {
910
+ withProject: false,
911
+ });
912
+ const ok = (
913
+ await sh(b.golden, `c6 build${k} ok`, 'node --version')
914
+ ).out.startsWith('v');
915
+ return { kind: 'build', k, ok };
916
+ } catch (e) {
917
+ return {
918
+ kind: 'build',
919
+ k,
920
+ ok: false,
921
+ err: String(e?.message || e).slice(0, 100),
922
+ };
923
+ }
924
+ })()
925
+ );
926
+
927
+ const forkJobs = Array.from({ length: N_CONTENTION_FORKS }, (_, idx) =>
928
+ (async () => {
929
+ const i = idx + 1;
930
+ try {
931
+ const child = await forkViaShipped(cli, sdk, bc, {
932
+ name: `${goldenName}-c6fork-${i}-${process.pid}`,
933
+ resources: { cpus: 1, memoryMb: 1024, network: true },
934
+ });
935
+ track(child.name ?? `${goldenName}-c6fork-${i}`, child);
936
+ // Each fork writes a DISTINCT file (for the E1 isolation check) + commits.
937
+ await sh(
938
+ child,
939
+ `c6 fork${i} work`,
940
+ [
941
+ `cd ${PROJECT}`,
942
+ `printf "fork ${i}\\n" > c6fork-${i}.txt`,
943
+ `git add -A && git commit -q -m "c6 fork ${i}"`,
944
+ ].join(' && '),
945
+ 120
946
+ );
947
+ if ((BREAK.C6_crossContam || BREAK.E1_isolation) && i === 1) {
948
+ // TEETH C6/E1: leak fork 2's file into fork 1 (cross-contamination).
949
+ // BREAK_C6_CONTAM and BREAK_E1_ISOLATION both inject this leak; the
950
+ // crossLeaks check then turns C6 (contention: no cross-contamination)
951
+ // and E1 (isolation under concurrency) red, each having its own knob.
952
+ await sh(
953
+ child,
954
+ `c6 fork${i} BREAK`,
955
+ `cd ${PROJECT} && printf "leaked\\n" > c6fork-2.txt`
956
+ );
957
+ }
958
+ return { kind: 'fork', i, child, ok: true };
959
+ } catch (e) {
960
+ return {
961
+ kind: 'fork',
962
+ i,
963
+ ok: false,
964
+ err: String(e?.message || e).slice(0, 100),
965
+ };
966
+ }
967
+ })()
968
+ );
969
+
970
+ // Bounded wall time: a deadlock would blow this budget (loud failure, no hang).
971
+ const all = await withHostTimeout(
972
+ Promise.all([...buildJobs, ...forkJobs]),
973
+ CONTENTION_WALL_BUDGET_MS,
974
+ 'C6 contention budget'
975
+ );
976
+ const wallMs = now() - tStart;
977
+ const builds = all.filter((r) => r.kind === 'build');
978
+ const forks = all.filter((r) => r.kind === 'fork');
979
+ const allConverged = all.every((r) => r.ok);
980
+ const eagainDuring = stats.eagainRetries + stats.forkRetries - eagainBefore;
981
+ const eagainSurfacedAsFailure = BREAK.C6_eagainFatal; // transient surfaced as fatal => failures
982
+
983
+ // E1 isolation under load: each fork sees ONLY its own c6fork-*.txt.
984
+ const crossLeaks = [];
985
+ for (const f of forks.filter((f) => f.ok)) {
986
+ const seen = (
987
+ await sh(
988
+ f.child,
989
+ `c6 iso ${f.i}`,
990
+ `cd ${PROJECT} && ls c6fork-*.txt 2>/dev/null | sort | tr '\\n' ' '`
991
+ )
992
+ ).out;
993
+ for (const other of forks.filter((o) => o.ok && o.i !== f.i)) {
994
+ if (seen.includes(`c6fork-${other.i}.txt`)) {
995
+ crossLeaks.push(`fork ${f.i} sees fork ${other.i}`);
996
+ }
997
+ }
998
+ }
999
+ // Isolation into the golden: a witness fork must see NONE of the c6 files.
1000
+ let witnessSeesAny = false;
1001
+ try {
1002
+ const w = await forkViaShipped(cli, sdk, bc, {
1003
+ name: `${goldenName}-c6witness-${process.pid}`,
1004
+ resources: { cpus: 1, memoryMb: 1024, network: true },
1005
+ });
1006
+ track(w.name ?? `${goldenName}-c6witness`, w);
1007
+ const seen = (
1008
+ await sh(
1009
+ w,
1010
+ 'c6 witness',
1011
+ `cd ${PROJECT} && ls c6fork-*.txt 2>/dev/null | wc -l`
1012
+ )
1013
+ ).out;
1014
+ witnessSeesAny = Number(seen) > 0;
1015
+ } catch {
1016
+ witnessSeesAny = false;
1017
+ }
1018
+
1019
+ const noDeadlock = wallMs < CONTENTION_WALL_BUDGET_MS;
1020
+ const c6Pass =
1021
+ allConverged &&
1022
+ !eagainSurfacedAsFailure &&
1023
+ crossLeaks.length === 0 &&
1024
+ noDeadlock;
1025
+ record(
1026
+ 'C6',
1027
+ 'C6 contention: all converge, EAGAIN retried (never a failure), no cross-contam, no deadlock',
1028
+ c6Pass,
1029
+ `forks=${forks.filter((f) => f.ok).length}/${N_CONTENTION_FORKS} builds=${builds.filter((b) => b.ok).length}/${N_CONTENTION_BUILDS} EAGAIN-retried=${eagainDuring} surfaced-as-failure=${eagainSurfacedAsFailure} cross-leaks=${crossLeaks.length} wall=${wallMs}ms (budget ${CONTENTION_WALL_BUDGET_MS}ms)`
1030
+ );
1031
+
1032
+ const e1Pass = crossLeaks.length === 0 && !witnessSeesAny;
1033
+ record(
1034
+ 'E1',
1035
+ 'E1 isolation under concurrency: each fork invisible to siblings + golden',
1036
+ e1Pass,
1037
+ `cross-leaks=${crossLeaks.length} ${crossLeaks.join(';')} witness(golden)-sees-fork-files=${witnessSeesAny}`
1038
+ );
1039
+ return { wallMs, eagainDuring };
1040
+ }
1041
+
1042
+ // =============================================================================
1043
+ // E2: secret scrub on kill - secret in guest tmpfs gone after SIGKILL, never on
1044
+ // the persistent disk.
1045
+ // =============================================================================
1046
+ async function runE2(cli, sdk) {
1047
+ log(
1048
+ '--- E2: secret scrub on kill (tmpfs /dev/shm/linzumi discarded; never on persistent disk) ---'
1049
+ );
1050
+ const { goldenName } = await buildGolden(cli, sdk, 'e2');
1051
+ const bc = bootConfigFor(goldenName, 'e2');
1052
+
1053
+ const beforePids = new Set(vmmProcs().map((p) => p.pid));
1054
+ const fork = await forkViaShipped(cli, sdk, bc, {
1055
+ name: `${goldenName}-e2-${process.pid}`,
1056
+ resources: { cpus: 1, memoryMb: 1024, network: true },
1057
+ });
1058
+ const forkRec = track(fork.name ?? `${goldenName}-e2`, fork);
1059
+
1060
+ const SECRET = `LINZUMI-FAKE-SECRET-${Math.random().toString(36).slice(2)}`;
1061
+ // Write the secret into the guest tmpfs (the real credential dir), 0600. Then
1062
+ // (default) keep it ONLY in tmpfs; the teeth knob ALSO writes it to a
1063
+ // persistent-disk path so the "never on persistent disk" check goes red.
1064
+ await sh(
1065
+ fork,
1066
+ 'e2 write secret to tmpfs',
1067
+ `mkdir -p ${SECRETS_DIR} && printf '%s' '${SECRET}' > ${SECRETS_DIR}/auth.json && chmod 600 ${SECRETS_DIR}/auth.json`
1068
+ );
1069
+ // Confirm tmpfs is actually a tmpfs (RAM) and the secret is there now.
1070
+ const tmpfsMount = (
1071
+ await sh(
1072
+ fork,
1073
+ 'e2 mount check',
1074
+ `df -PT ${SECRETS_DIR} 2>/dev/null | tail -1 | awk '{print $2}'; mount | grep -i shm | head -1`
1075
+ )
1076
+ ).out;
1077
+ const secretPresentPreKill = (
1078
+ await sh(
1079
+ fork,
1080
+ 'e2 secret present',
1081
+ `cat ${SECRETS_DIR}/auth.json 2>/dev/null || echo MISSING`
1082
+ )
1083
+ ).out;
1084
+
1085
+ // Scan the PERSISTENT disk for the secret BEFORE kill: it must never be there.
1086
+ // (mkdir a persistent probe dir; the secret should NOT have been written here.)
1087
+ if (BREAK.E2_secretPersists) {
1088
+ // TEETH E2: leak the secret onto the persistent VM disk (NOT tmpfs).
1089
+ await sh(
1090
+ fork,
1091
+ 'e2 BREAK persist',
1092
+ `mkdir -p ${PERSIST_PROBE} && printf '%s' '${SECRET}' > ${PERSIST_PROBE}/leaked-secret`
1093
+ );
1094
+ }
1095
+ const persistGrepPreKill = (
1096
+ await sh(
1097
+ fork,
1098
+ 'e2 persist scan',
1099
+ // Scan the PERSISTENT-disk locations a secret could realistically land in
1100
+ // (the project tree, /root, /etc, /opt, /var, /tmp) - explicitly NOT the
1101
+ // /dev/shm tmpfs (which is RAM and is the legitimate home for the secret).
1102
+ // Bounded (no whole-root walk) so it cannot wedge the run.
1103
+ `grep -rl '${SECRET}' ${PROJECT} /root /etc /opt /var /tmp 2>/dev/null | grep -v '^/dev/shm' | head -5 || true`
1104
+ )
1105
+ ).out;
1106
+ const secretOnPersistentDisk = persistGrepPreKill.trim() !== '';
1107
+
1108
+ // KILL the fork's vmm (the tmpfs is RAM -> discarded on vmm death).
1109
+ const kill = await killForkVmm(beforePids, 'E2 fork');
1110
+ forkRec.killed = true;
1111
+ await sleep(600);
1112
+
1113
+ // The vmm is gone, so the tmpfs (guest RAM) no longer exists anywhere on the
1114
+ // host. Prove the secret is not recoverable: the killed fork's disk dir is
1115
+ // reaped, and a fresh WITNESS fork off the golden shows NO secret (the secret
1116
+ // only ever lived in the dead fork's RAM, never in the golden / persistent base).
1117
+ const dirsBeforeReap = vmsDirs().length;
1118
+ try {
1119
+ await withHostTimeout(fork.delete(), 8000, 'e2 reap');
1120
+ } catch {}
1121
+ forkRec.killed = 'reaped';
1122
+ await sleep(400);
1123
+ const reapedOk = vmsDirs().length === dirsBeforeReap - 1 && kill.gone;
1124
+
1125
+ let witnessHasSecret = true;
1126
+ try {
1127
+ const w = await forkViaShipped(cli, sdk, bc, {
1128
+ name: `${goldenName}-e2witness-${process.pid}`,
1129
+ resources: { cpus: 1, memoryMb: 1024, network: true },
1130
+ });
1131
+ track(w.name ?? `${goldenName}-e2witness`, w);
1132
+ const inTmpfs = (
1133
+ await sh(
1134
+ w,
1135
+ 'e2 witness tmpfs',
1136
+ `cat ${SECRETS_DIR}/auth.json 2>/dev/null || echo GONE`
1137
+ )
1138
+ ).out;
1139
+ const onDisk = (
1140
+ await sh(
1141
+ w,
1142
+ 'e2 witness disk',
1143
+ `grep -rl '${SECRET}' ${PROJECT} /root /etc /opt /var /tmp 2>/dev/null | grep -v '^/dev/shm' | head -1 || echo GONE`
1144
+ )
1145
+ ).out;
1146
+ witnessHasSecret = inTmpfs.includes(SECRET) || onDisk.includes('/');
1147
+ } catch {
1148
+ witnessHasSecret = false;
1149
+ }
1150
+
1151
+ const tmpfsDiscarded = kill.gone && !witnessHasSecret;
1152
+ const neverOnDisk = !secretOnPersistentDisk;
1153
+ const pass = tmpfsDiscarded && neverOnDisk && reapedOk;
1154
+ record(
1155
+ 'E2',
1156
+ 'E2 secret scrub on kill: tmpfs secret discarded on SIGKILL + never on persistent disk',
1157
+ pass,
1158
+ `tmpfs=${tmpfsMount.split('\n')[0]} secret-pre-kill=${secretPresentPreKill === SECRET ? 'present' : secretPresentPreKill} on-persistent-disk=${secretOnPersistentDisk} killMs=${kill.killMs} witness-has-secret-after-kill=${witnessHasSecret} reaped=${reapedOk}`
1159
+ );
1160
+ return { killMs: kill.killMs };
1161
+ }
1162
+
1163
+ // =============================================================================
1164
+ // TEARDOWN - ALWAYS delete every created machine (even SIGKILLed ones, whose
1165
+ // disk dir lingers until delete()). Confirm 0 leaked smol-vmm + 0 vms dirs.
1166
+ // =============================================================================
1167
+ async function teardown() {
1168
+ log('TEARDOWN: deleting every created machine (synchronous, bounded)');
1169
+ for (const rec of [...created].reverse()) {
1170
+ if (rec.killed === 'reaped') continue; // already reaped via delete()
1171
+ const m = rec.machine;
1172
+ try {
1173
+ if (m && typeof m.delete === 'function') {
1174
+ await withHostTimeout(m.delete(), 8000, `teardown delete ${rec.name}`);
1175
+ } else if (m && typeof m.stop === 'function') {
1176
+ await withHostTimeout(m.stop(), 8000, `teardown stop ${rec.name}`);
1177
+ }
1178
+ } catch {
1179
+ try {
1180
+ if (m && typeof m.stop === 'function')
1181
+ await withHostTimeout(m.stop(), 5000, `teardown stop2 ${rec.name}`);
1182
+ } catch {}
1183
+ }
1184
+ }
1185
+ await sleep(800);
1186
+ }
1187
+
1188
+ // =============================================================================
1189
+ // MAIN
1190
+ // =============================================================================
1191
+ let bundleCleanup = () => {};
1192
+ let exitCode = 0;
1193
+
1194
+ async function preflightHygiene(sdk) {
1195
+ // Detect a wedged runtime: stray vmm processes from a prior crashed run, OR a
1196
+ // first-exec "connection closed". Clear ONCE if wedged, then let it re-extract.
1197
+ const stray = vmmProcs();
1198
+ if (stray.length > 0) {
1199
+ log(
1200
+ `preflight: ${stray.length} stray smol-vmm found - this run did not create them.`
1201
+ );
1202
+ // We only clear if a smoke create wedges (below); we do NOT blanket-kill, to
1203
+ // avoid killing a concurrent legitimate run.
1204
+ }
1205
+ // Smoke: create + exec + delete a tiny VM. If the first exec wedges with a
1206
+ // connection-closed (the classic wedged-cache symptom), clear the vms cache
1207
+ // ONCE and retry.
1208
+ const smokeName = `chaos-preflight-${process.pid}`;
1209
+ for (let attempt = 0; attempt < 2; attempt++) {
1210
+ let m = null;
1211
+ try {
1212
+ m = await withHostTimeout(
1213
+ sdk.Machine.create({
1214
+ name: smokeName,
1215
+ resources: { cpus: 1, memoryMb: 512, network: false },
1216
+ }),
1217
+ 60000,
1218
+ 'preflight create'
1219
+ );
1220
+ const out = (
1221
+ await withHostTimeout(
1222
+ m.exec(['/bin/sh', '-c', 'echo ready'], { timeout: 20 }),
1223
+ 25000,
1224
+ 'preflight exec'
1225
+ )
1226
+ ).stdout.trim();
1227
+ await withHostTimeout(m.delete(), 8000, 'preflight delete');
1228
+ if (out === 'ready') {
1229
+ log('preflight: runtime healthy (create+exec+delete ok)');
1230
+ return;
1231
+ }
1232
+ } catch (e) {
1233
+ log(
1234
+ `preflight attempt ${attempt + 1} wedged: ${String(e?.message || e).slice(0, 120)}`
1235
+ );
1236
+ try {
1237
+ if (m) await withHostTimeout(m.delete(), 5000, 'preflight cleanup');
1238
+ } catch {}
1239
+ if (attempt === 0) {
1240
+ // Clear the wedged cache ONCE, kill any stray boot-vm, let it re-extract.
1241
+ log(
1242
+ 'preflight: clearing smolvm vms cache ONCE + killing stray smol-vmm'
1243
+ );
1244
+ for (const p of vmmProcs()) {
1245
+ try {
1246
+ process.kill(p.pid, 'SIGKILL');
1247
+ } catch {}
1248
+ }
1249
+ try {
1250
+ rmSync(VMS_DIR, { recursive: true, force: true });
1251
+ } catch {}
1252
+ await sleep(1500);
1253
+ } else {
1254
+ throw new Error(
1255
+ `preflight: runtime still wedged after cache clear: ${String(e?.message || e)}`
1256
+ );
1257
+ }
1258
+ }
1259
+ }
1260
+ }
1261
+
1262
+ async function main() {
1263
+ log(`runtime dir: ${RUNTIME_DIR}`);
1264
+ if (!existsSync(join(RUNTIME_DIR, 'node_modules', 'smolmachines'))) {
1265
+ throw new Error(`smolmachines runtime not found at ${RUNTIME_DIR}.`);
1266
+ }
1267
+ try {
1268
+ const hv = execFileSync('sysctl', ['-n', 'kern.hv_support'], {
1269
+ encoding: 'utf8',
1270
+ }).trim();
1271
+ if (hv !== '1') throw new Error('kern.hv_support != 1');
1272
+ } catch (e) {
1273
+ throw new Error(
1274
+ `HVF not available (${e.message}); this is the HVF-only leg.`
1275
+ );
1276
+ }
1277
+
1278
+ const { mod: cli, cleanup } = await loadCliHelpers();
1279
+ bundleCleanup = cleanup;
1280
+ log(
1281
+ 'bundled OUR CLI helpers via esbuild (createMachineFromGolden, guestProvisionCommand, goldenBuildMachineName)'
1282
+ );
1283
+
1284
+ const sdk = await cli.loadSmolmachines(RUNTIME_DIR);
1285
+ log('loaded smolmachines via OUR loadSmolmachines()');
1286
+
1287
+ await preflightHygiene(sdk);
1288
+
1289
+ const vmmAtStart = vmmCount();
1290
+ // Snapshot the vms disk dirs present at start so HOST cleanliness measures only
1291
+ // what THIS run created/leaked - a prior crashed run's orphans (or a concurrent
1292
+ // legitimate run's dirs) must never be attributed to or swept by this run.
1293
+ const vmsDirsAtStart = new Set(vmsDirs());
1294
+ log(
1295
+ `vmm processes at start (post-preflight): ${vmmAtStart}; vms dirs at start: ${vmsDirsAtStart.size}`
1296
+ );
1297
+
1298
+ const killTimings = {};
1299
+ // Run each family SYNCHRONOUSLY (never an unbounded parallel fan-out).
1300
+ killTimings.c1 = (await runC1(cli, sdk)).killMs;
1301
+ killTimings.c2 = (await runC2(cli, sdk)).killMs;
1302
+ const c4d1 = await runC4D1(cli, sdk);
1303
+ killTimings.c4 = c4d1.killMs;
1304
+ await runC5(cli, sdk);
1305
+ const c6 = await runC6E1(cli, sdk);
1306
+ killTimings.e2 = (await runE2(cli, sdk)).killMs;
1307
+
1308
+ return { vmmAtStart, vmsDirsAtStart, killTimings, c6 };
1309
+ }
1310
+
1311
+ let runResult;
1312
+ try {
1313
+ runResult = await main();
1314
+ } catch (e) {
1315
+ console.error('[chaos] FATAL:', e?.stack || String(e));
1316
+ exitCode = 1;
1317
+ } finally {
1318
+ try {
1319
+ await teardown();
1320
+ } catch (e) {
1321
+ console.error('[chaos] teardown error:', String(e?.message || e));
1322
+ }
1323
+ try {
1324
+ bundleCleanup();
1325
+ } catch {}
1326
+ }
1327
+
1328
+ // =============================================================================
1329
+ // EXIT HYGIENE: confirm 0 leaked smol-vmm + 0 vms dirs THIS RUN created.
1330
+ // We measure relative to the start snapshot so a prior crashed run's orphans
1331
+ // (or a concurrent legitimate run) are never attributed to this run. A vms dir
1332
+ // created during this run that has NO live vmm is a genuine leak we own; we
1333
+ // defensively sweep those (the running set + start-set dirs are left untouched).
1334
+ // =============================================================================
1335
+ await sleep(500);
1336
+ const leakedVmm = vmmProcs();
1337
+ const startDirs = runResult?.vmsDirsAtStart ?? new Set();
1338
+ const liveHashes = new Set(leakedVmm.map((p) => p.hash));
1339
+ // Defensive sweep: dirs that appeared during this run, have no live vmm, and were
1340
+ // NOT present at start (so we KNOW they are ours and abandoned). Skip if a BREAK
1341
+ // knob intentionally left an orphan (so the teeth row stays red).
1342
+ if (!BREAK.C1_orphan) {
1343
+ for (const d of vmsDirs()) {
1344
+ if (startDirs.has(d) || liveHashes.has(d)) continue;
1345
+ try {
1346
+ rmSync(join(VMS_DIR, d), { recursive: true, force: true });
1347
+ } catch {}
1348
+ }
1349
+ }
1350
+ await sleep(200);
1351
+ const newLeftoverDirs = vmsDirs().filter((d) => !startDirs.has(d));
1352
+ const hostClean = leakedVmm.length === 0 && newLeftoverDirs.length === 0;
1353
+ record(
1354
+ 'HOST',
1355
+ 'HOST cleanliness at exit: 0 leaked smol-vmm + 0 leftover vms disk dirs (this run)',
1356
+ hostClean,
1357
+ `leaked-smol-vmm=${leakedVmm.length} new-leftover-vms-dirs=${newLeftoverDirs.length} (pre-existing-at-start=${startDirs.size})`
1358
+ );
1359
+
1360
+ // =============================================================================
1361
+ // PER-PROPERTY PASS/FAIL TABLE
1362
+ // =============================================================================
1363
+ console.log(
1364
+ '\n===================== LIVE-HVF CHAOS HARNESS (PR #2111) ====================='
1365
+ );
1366
+ if (runResult) {
1367
+ const kt = runResult.killTimings;
1368
+ console.log(
1369
+ `SIGKILL timings (ms): C1-fork=${kt.c1 ?? '-'} C2-builder=${kt.c2 ?? '-'} C4-fork=${kt.c4 ?? '-'} E2-fork=${kt.e2 ?? '-'}`
1370
+ );
1371
+ console.log(
1372
+ `EAGAIN/transient retries (exec+fork, total): ${stats.eagainRetries + stats.forkRetries} (exec=${stats.eagainRetries} fork=${stats.forkRetries}) -- retried, never surfaced as failure`
1373
+ );
1374
+ }
1375
+ console.log(
1376
+ '----------------------------------------------------------------------------'
1377
+ );
1378
+ const ORDER = ['C1', 'C2', 'C4', 'C5', 'C6', 'D1', 'E1', 'E2', 'HOST'];
1379
+ let allPass = true;
1380
+ for (const id of ORDER) {
1381
+ const p = props[id];
1382
+ if (!p) {
1383
+ console.log(` MISS ${id} (property did not run)`);
1384
+ allPass = false;
1385
+ continue;
1386
+ }
1387
+ if (!p.pass) allPass = false;
1388
+ console.log(` ${p.pass ? 'PASS' : 'FAIL'} ${p.name}`);
1389
+ console.log(` ${p.detail}`);
1390
+ }
1391
+ console.log(
1392
+ '============================================================================'
1393
+ );
1394
+
1395
+ if (anyBreak) {
1396
+ const active = Object.entries(BREAK)
1397
+ .filter(([, v]) => v)
1398
+ .map(([k]) => k);
1399
+ if (allPass) {
1400
+ console.log(
1401
+ `TEETH CHECK FAILED: BREAK knob(s) [${active.join(', ')}] set but ALL properties still PASSED (toothless).`
1402
+ );
1403
+ exitCode = 2;
1404
+ } else {
1405
+ console.log(
1406
+ `TEETH CHECK OK: BREAK knob(s) [${active.join(', ')}] turned the expected property RED.`
1407
+ );
1408
+ exitCode = 0;
1409
+ }
1410
+ } else {
1411
+ if (exitCode === 0) exitCode = allPass ? 0 : 1;
1412
+ console.log(
1413
+ allPass
1414
+ ? 'DONE-OK: all chaos properties PASS on live HVF.'
1415
+ : 'FAILED: a chaos property was violated.'
1416
+ );
1417
+ }
1418
+
1419
+ console.log(`leaked smol-vmm at exit: ${leakedVmm.length} (MUST be 0)`);
1420
+ process.exit(exitCode);