commit b49e05e66a98cf0a87c15ee7f08000f48ab4e5f3
parent a3b1cc20776e20ddcb160db9d7ba520caa92e91b
Author: James Eversole <james@eversole.co>
Date: Fri, 16 Oct 2026 12:51:58 -0500
Give isolated generations matching procfs and preserve prelaunch continuation evidence
Native_math parity probe passes under nested user/PID/mount isolation. Continue only after re-auditing completed jobs and identical frozen input/runtime checks; never resume partially executed generation stores.
Diffstat:
2 files changed, 64 insertions(+), 14 deletions(-)
diff --git a/ext/zig/README.md b/ext/zig/README.md
@@ -285,7 +285,8 @@ comparisons, and no timing threshold is a test gate.
`test/bootstrap_cross_host.py --probe` runs the frozen `native_math` closure on
both hosts, with identical kits and a 1 GiB/120-second budget per job. It requires
-Linux `bwrap` with working user/PID namespaces and a Nix Python environment:
+Linux `bwrap` and util-linux `unshare` with working user/PID/mount namespaces,
+and a Nix Python environment:
```sh
nix develop -c python3 test/bootstrap_cross_host.py --probe \
@@ -302,3 +303,12 @@ independently audited object inventories. Its experiment definition records the
source commit/archive, mapped-input hashes, runtimes, Python, tooling, limits and
comparison boundaries. Historical `ext/zig/result` is the sole excluded archive
entry: a tracked absolute build-output symlink, not a compiler input.
+
+The launcher first creates a private PID namespace with a matching procfs mount,
+then starts the filesystem sandbox. This avoids assuming that the caller's `/proc`
+uses its PID numbering. Both isolation tool identities are recorded. A full
+experiment stopped before a job creates its generation directory can explicitly
+use `--continue-experiment`: frozen sources/runtimes and completed generations
+are reverified, the failed launch and original tooling are preserved, and the
+pending job still starts fresh. Incomplete executed generations are never resumed
+by this acceptance route.
diff --git a/test/bootstrap_cross_host.py b/test/bootstrap_cross_host.py
@@ -13,6 +13,7 @@ import shutil
import subprocess
import sys
import tarfile
+import time
from bootstrap_acceptance import ARTIFACTS, sha, verify, verify_generation
from bootstrap_generations import source_map, snapshot
@@ -74,7 +75,8 @@ def verify_cross(first, second, control, zig, runtime, library, source):
def sandbox_command(source, kit, stage, runtime, library, host, root, memory, seconds):
- command = ['bwrap','--die-with-parent','--unshare-all',
+ command = ['unshare','--user','--map-root-user','--mount','--pid','--fork',
+ '--kill-child','--mount-proc','--','bwrap','--die-with-parent','--unshare-all',
'--ro-bind','/nix/store','/nix/store','--proc','/proc','--dev','/dev',
'--tmpfs','/tmp','--ro-bind',str(source),'/source',
'--ro-bind',str(kit),'/kit','--bind',str(stage),'/work',
@@ -99,6 +101,7 @@ def main():
p.add_argument('--zig-library',type=Path,required=True)
p.add_argument('--probe',action='store_true',help='native_math on both hosts, 1 GiB/120s; no full generations')
p.add_argument('--verify-only',action='store_true')
+ p.add_argument('--continue-experiment',action='store_true',help='re-audit completed jobs; continue only jobs that never entered execution')
args=p.parse_args()
directory=args.directory.resolve()
runtime,library=args.tricu.resolve(),args.zig_library.resolve()
@@ -109,19 +112,33 @@ def main():
runtime,library,source)
print(json.dumps(evidence,indent=2))
return
- directory.mkdir(parents=True,exist_ok=False)
commit=subprocess.check_output(['git','rev-parse',args.source_commit+'^{commit}'],cwd=ROOT,text=True).strip()
archive=directory/'source.tar'
- with archive.open('wb') as out:
- subprocess.run(['git','archive',commit],cwd=ROOT,stdout=out,check=True)
- source.mkdir()
- with tarfile.open(archive) as tar:
- # Historical tracked build-output symlink, excluded by recovery archives
- # too. Keep all other extraction safety checks; never follow it.
- tar.extractall(source,members=(m for m in tar if m.name!='ext/zig/result'),filter='data')
+ if args.continue_experiment:
+ original=json.loads((directory/'experiment.json').read_text())
+ if (args.probe or original['source_commit']!=commit or original['source_archive_sha256']!=sha(archive)
+ or original['runtime']!=dict(path=str(runtime),sha256=sha(runtime))
+ or original['zig_library']!=dict(path=str(library),sha256=sha(library))):
+ raise ValueError('continuation requires identical frozen sources and runtimes')
+ with tarfile.open(archive) as tar:
+ for member in tar:
+ if member.isfile() and (source/member.name).read_bytes()!=tar.extractfile(member).read():
+ raise ValueError('frozen source changed: '+member.name)
+ # Preserve the failed attempt, including its exact original tooling.
+ shutil.copyfile(directory/'report.json',directory/('report-before-'+str(time.time_ns())+'.json'))
+ else:
+ directory.mkdir(parents=True,exist_ok=False)
+ with archive.open('wb') as out:
+ subprocess.run(['git','archive',commit],cwd=ROOT,stdout=out,check=True)
+ source.mkdir()
+ with tarfile.open(archive) as tar:
+ # Historical tracked build-output symlink, excluded by recovery
+ # archives too. Keep all other extraction safety checks.
+ tar.extractall(source,members=(m for m in tar if m.name!='ext/zig/result'),filter='data')
# The sandbox also mounts this snapshot read-only; never use live sources.
map_text,entries,required=source_map(source/'bootstrap/compiler-source-map.v1',source,'source_map')
- harness=directory/'harness/test'
+ attempt='continuation-'+str(time.time_ns()) if args.continue_experiment else 'harness'
+ harness=directory/attempt/'test'
harness.mkdir(parents=True)
for path in (ROOT/'test').glob('*.py'): shutil.copyfile(path,harness/path.name)
tooling={path.name:sha(path) for path in harness.glob('*.py')}
@@ -132,12 +149,12 @@ def main():
zig_library=dict(path=str(library),sha256=sha(library)),
python=dict(path=sys.executable,sha256=sha(Path(sys.executable))),
harness_sha256=sha(Path(__file__)), tooling_sha256=tooling,
+ isolation_tools={name:dict(path=str(Path(shutil.which(name)).resolve()),sha256=sha(Path(shutil.which(name)))) for name in ('unshare','bwrap')},
limits=dict(memory_mib=memory,wall_seconds=seconds,capabilities=8),
expected='H2 = H3 = Z3: complete objects, receipts, checkpoints and three bundles',
comparison_exclusions=['runtime/transport provenance','timings','logs','filesystem locations','invocation records'],
**snapshot(map_text,entries,source))
- save(directory/'experiment.json',definition)
- shutil.copyfile(__file__,directory/'experiment-harness.py')
+ save(directory/(attempt+'-definition.json' if args.continue_experiment else 'experiment.json'),definition)
report=dict(complete=False,scope='probe' if args.probe else 'full',stages=[])
save(directory/'report.json',report)
def job(label,kit,host):
@@ -145,6 +162,18 @@ def main():
# independently captures/authenticates it again inside.
load_kit(kit,'stable')
stage=directory/label
+ if args.continue_experiment and stage.exists():
+ if (stage/'generation/status.json').is_file():
+ inputs,_,_=verify_generation(stage/'generation',runtime if host=='haskell' else library,source,host)
+ if inputs['compiler_bundle_sha256']!=sha(kit/'rebuilt-compiler.arboricx') or inputs['adapter_sha256']!=sha(kit/'io-adapter.arboricx'):
+ raise ValueError('completed job used a different kit')
+ report['stages'].append(label)
+ save(directory/'report.json',report)
+ print('Re-audited completed '+label,flush=True)
+ return stage/'generation'
+ if (stage/'generation').exists():
+ raise ValueError('cannot resume a partially executed generation; select a fresh experiment')
+ stage.rename(directory/(label+'-launch-failed-'+str(time.time_ns())))
stage.mkdir()
command=sandbox_command(source,kit,stage,runtime,library,host,
'native_math' if args.probe else 'source_map',memory,seconds)
@@ -163,6 +192,12 @@ def main():
return stage/'generation'
def kit_from(run,label):
kit=directory/(label+'-kit')
+ if kit.exists():
+ load_kit(kit,'stable')
+ for name in ('rebuilt-compiler.arboricx','io-adapter.arboricx'):
+ if (kit/name).read_bytes()!=(run/name).read_bytes():
+ raise ValueError('retained kit differs from parent generation')
+ return kit
kit.mkdir()
artifacts={}
for role,name,export in [('compiler','rebuilt-compiler.arboricx','sourceMapBuildStable'),
@@ -178,7 +213,12 @@ def main():
if inventory(first)!=inventory(zig): raise ValueError('probe complete object inventories differ')
else:
second=job('H2',kit_from(first,'H1'),'haskell')
- save(directory/'accepted-pair.json',verify(first,second,runtime,source))
+ accepted=verify(first,second,runtime,source)
+ if (directory/'accepted-pair.json').exists():
+ if json.loads((directory/'accepted-pair.json').read_text())!=accepted:
+ raise ValueError('existing pair acceptance changed')
+ else:
+ save(directory/'accepted-pair.json',accepted)
kit=kit_from(second,'H2')
control=job('H3',kit,'haskell')
zig=job('Z3',kit,'zig')