FDS/OS 1.0
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise comparison and frozen-lock rejection contracts without claiming an OS rebuild."""
|
||||
import hashlib
|
||||
import importlib.machinery
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
project = Path(__file__).resolve().parents[2]
|
||||
loader = importlib.machinery.SourceFileLoader('fds_frozen_fixture', str(project / 'tools/frozen-inputs'))
|
||||
spec = importlib.util.spec_from_loader(loader.name, loader)
|
||||
frozen = importlib.util.module_from_spec(spec)
|
||||
loader.exec_module(frozen)
|
||||
work = Path(tempfile.mkdtemp(prefix='m12-release-contracts.', dir=project / 'out'))
|
||||
|
||||
|
||||
def invoke(*args, ok=True):
|
||||
result = subprocess.run(list(map(str, args)), capture_output=True, text=True, timeout=60)
|
||||
with (work / 'commands.log').open('a') as log:
|
||||
log.write(repr(args) + '\n' + result.stdout + result.stderr)
|
||||
assert (result.returncode == 0) == ok, (args, result.returncode, result.stdout, result.stderr)
|
||||
return result
|
||||
|
||||
|
||||
lock = {'format': 1, 'version': '0.1.0', 'void_commit': 'a' * 40, 'source_epoch': 123,
|
||||
'source_sha256': 'b' * 64, 'rust_toolchain': '1.98.0', 'host_prerequisites': {}, 'files': []}
|
||||
snapshot = work / 'snapshot'
|
||||
(snapshot / 'project').mkdir(parents=True)
|
||||
(snapshot / 'project/source.txt').write_bytes(b'bounded input fixture\n')
|
||||
(snapshot / 'temporary').mkdir()
|
||||
(snapshot / 'temporary').chmod(0o1777)
|
||||
(snapshot / 'project/executable').write_bytes(b'archive mode fixture\n')
|
||||
(snapshot / 'project/executable').chmod(0o775)
|
||||
(snapshot / 'source-link').symlink_to('project/source.txt')
|
||||
lock['files'] = frozen.inventory(snapshot)
|
||||
lock['source_sha256'] = frozen.source_digest(lock['files'])
|
||||
(snapshot / 'lock.json').write_text(json.dumps(lock))
|
||||
verify = project / 'tools/frozen-inputs'
|
||||
invoke(verify, 'verify', snapshot)
|
||||
for case in ('bytes', 'mode', 'extra', 'missing', 'link', 'metadata'):
|
||||
damaged = work / ('damaged-' + case)
|
||||
shutil.copytree(snapshot, damaged, symlinks=True)
|
||||
leaf = damaged / 'project/source.txt'
|
||||
if case == 'bytes': leaf.write_bytes(b'changed input fixture\n')
|
||||
elif case == 'mode': leaf.chmod(0o600)
|
||||
elif case == 'extra': (damaged / 'extra').mkdir()
|
||||
elif case == 'missing': leaf.unlink()
|
||||
elif case == 'link':
|
||||
(damaged / 'source-link').unlink()
|
||||
(damaged / 'source-link').symlink_to('/etc/passwd')
|
||||
elif case == 'metadata':
|
||||
wrong = dict(lock, rust_toolchain='../unexpected')
|
||||
(damaged / 'lock.json').write_text(json.dumps(wrong))
|
||||
invoke(verify, 'verify', damaged, ok=False)
|
||||
print('PASS: frozen input byte/mode/path/symlink and metadata tampering rejected', flush=True)
|
||||
|
||||
# Exercise the actual release archive writer and the documented extraction
|
||||
# recipe. Default tar extraction masks modes and strips sticky bits, which would
|
||||
# make an otherwise intact build-input archive fail its frozen lock immediately.
|
||||
sys.path.insert(0, str(project / 'tools'))
|
||||
archive_loader = importlib.machinery.SourceFileLoader('fds_assembly_fixture', str(project / 'tools/assemble-release'))
|
||||
archive_spec = importlib.util.spec_from_loader(archive_loader.name, archive_loader)
|
||||
assembly = importlib.util.module_from_spec(archive_spec)
|
||||
archive_loader.exec_module(assembly)
|
||||
for name in ('one', 'two'):
|
||||
assembly.packed_tree(snapshot, work / f'{name}.tar.zst', lock['source_epoch'])
|
||||
assert frozen.digest(work / 'one.tar.zst') == frozen.digest(work / 'two.tar.zst')
|
||||
unpacked = work / 'unpacked'
|
||||
unpacked.mkdir()
|
||||
invoke('tar', '--extract', '--zstd', '--same-permissions', '--no-same-owner',
|
||||
'--file', work / 'one.tar.zst', '--directory', unpacked)
|
||||
invoke(verify, 'verify', unpacked)
|
||||
print('PASS: deterministic release archive and complete mode/symlink-preserving extraction', flush=True)
|
||||
|
||||
first, second = work / 'first', work / 'second'
|
||||
paths = [f'out/{name}' for name in ('fds-system-cli.img', 'fds-system-development.img',
|
||||
'fds-recovery.img', 'fds-boot.img', 'fds-internal.img', 'rootfs-cli.tar',
|
||||
'rootfs-development.tar', 'rootfs-recovery.tar')]
|
||||
paths += ['out/initramfs/initramfs.cpio' + suffix for suffix in ('', '.gz', '.lz4', '.zst')]
|
||||
paths += ['out/kernel/boot/' + name for name in ('kernel_2712.img', 'bcm2712-rpi-5-b.dtb')]
|
||||
paths += ['out/eeprom-production-latest/' + name for name in ('configured.bin', 'rollback.bin', 'configured.conf', 'original.conf')]
|
||||
for name in ('base', 'base-files', 'init', 'cli', 'cartridged', 'dasungd', 'kernel', 'dhcpcd', 'eink'):
|
||||
paths.append(f'out/packages/fds-{name}-0.1.0_1.aarch64.xbps')
|
||||
for name in paths:
|
||||
path = first / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(hashlib.sha256(name.encode()).digest())
|
||||
(first / '.host/frozen').mkdir(parents=True)
|
||||
(first / '.host/frozen/lock.json').write_text(json.dumps(lock))
|
||||
shutil.copytree(first, second)
|
||||
compare = project / 'tools/compare-builds'
|
||||
invoke(compare, first, second, '--report', work / 'equal.json')
|
||||
result = json.loads((work / 'equal.json').read_text())
|
||||
assert result['status'] == 'passed' and len(result['artifacts']) == 27
|
||||
invoke(compare, first, first, '--report', work / 'same-tree.json', ok=False)
|
||||
invoke(compare, first, second, '--report', work / 'equal.json', ok=False)
|
||||
changed = second / 'out/fds-system-cli.img'
|
||||
changed.write_bytes(b'changed image fixture')
|
||||
invoke(compare, first, second, '--report', work / 'different.json', ok=False)
|
||||
result = json.loads((work / 'different.json').read_text())
|
||||
assert result['status'] == 'failed'
|
||||
assert [x['name'] for x in result['artifacts'] if not x['identical']] == ['fds-system-cli.img']
|
||||
changed.unlink()
|
||||
invoke(compare, first, second, '--report', work / 'missing.json', ok=False)
|
||||
assert not (work / 'missing.json').exists()
|
||||
shutil.copyfile(first / 'out/fds-system-cli.img', changed)
|
||||
(second / '.host/frozen/lock.json').write_text(json.dumps(dict(lock, source_epoch=456)))
|
||||
invoke(compare, first, second, '--report', work / 'wrong-inputs.json', ok=False)
|
||||
assert not (work / 'wrong-inputs.json').exists()
|
||||
print('PASS: complete comparison, changed/missing artifact, same-tree, existing-report and different-input rejection', flush=True)
|
||||
|
||||
# Even before a complete build exists, assembly must reject an existing output
|
||||
# and must not resolve a private-key symlink past the signer's no-follow policy.
|
||||
key = work / 'test.key'
|
||||
key.write_bytes(bytes(32))
|
||||
key.chmod(0o600)
|
||||
output = work / 'existing-output'
|
||||
output.mkdir()
|
||||
(output / 'marker').write_text('preserve')
|
||||
command = [project / 'tools/assemble-release', '--inputs', snapshot, '--comparison-build', second,
|
||||
'--output-directory', output, '--key', key]
|
||||
invoke(*command, ok=False)
|
||||
assert (output / 'marker').read_text() == 'preserve' and len(list(output.iterdir())) == 1
|
||||
linked = work / 'linked.key'
|
||||
linked.symlink_to(key)
|
||||
command[-1] = linked
|
||||
result = invoke(*command, ok=False)
|
||||
assert 'not a symlink' in result.stderr
|
||||
print(f'PASS: release output preservation and private-key symlink rejection: {work}')
|
||||
print('NOTE: these are small contract fixtures; actual source-to-image offline reproduction remains a separate gate')
|
||||
Reference in New Issue
Block a user