Files
archy/scripts/tests/kernel_artifact_vm.py

110 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""Diskless/networkless kernel smoke boot. Run under release-heavy.lock.
Uses an already authenticated payload receipt, never installs a host package.
This proves direct kernel boot only, not ISO/EFI/GRUB or physical-node acceptance.
"""
import hashlib
import argparse
import json
from pathlib import Path
import re
import shutil
import subprocess
import sys
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('receipt', type=Path)
parser.add_argument('output', type=Path)
parser.add_argument('--timing-collector', type=Path, help='statically linked read-only collector')
parser.add_argument('--guest-suspend-seconds', type=int, choices=range(3, 31),
help='explicit disposable guest-only RTC wake + mem suspend experiment')
args = parser.parse_args()
if args.guest_suspend_seconds and not args.timing_collector:
parser.error('--guest-suspend-seconds requires --timing-collector')
receipt = json.loads(args.receipt.read_text())
output = args.output
output.mkdir(exist_ok=False)
release = receipt['package'].removeprefix('linux-image-')
image = '/boot/vmlinuz-' + release
with open(image, 'rb') as stream:
assert hashlib.file_digest(stream, 'sha256').hexdigest() == receipt['files'][image]
shutil.copyfile(image, output / 'vmlinuz')
root = output / 'initramfs'
for directory in ('bin', 'dev', 'proc', 'sys'):
(root / directory).mkdir(parents=True)
shutil.copyfile('/usr/bin/busybox', root / 'bin/busybox')
(root / 'bin/busybox').chmod(0o755)
dependencies = subprocess.check_output(['ldd', '/usr/bin/busybox'], text=True)
for library in re.findall(r'(/[^\s()]+)', dependencies):
target = root / library.lstrip('/')
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(library, target)
for applet in ('sh', 'mount', 'uname', 'cat', 'poweroff', 'sleep'):
(root / 'bin' / applet).symlink_to('busybox')
timing_command = ''
if args.timing_collector:
shutil.copyfile(args.timing_collector, root / 'bin/timing-collector')
(root / 'bin/timing-collector').chmod(0o755)
timing_command = '/bin/timing-collector 201 100 && echo TIMING_COLLECTION_PASS\n'
if args.guest_suspend_seconds:
# This command exists only in the generated disposable initramfs.
# No host rtcwake, power control, clock write, or attached host disk.
rtcwake = Path('/usr/sbin/rtcwake')
shutil.copy2(rtcwake, root / 'bin/rtcwake')
libraries = subprocess.check_output(['ldd', str(rtcwake)], text=True)
for library in re.findall(r'(/[^\s()]+)', libraries):
target = root / library.lstrip('/')
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(library, target)
timing_command = ('/bin/timing-collector 201 100 &\ncollector=$!\nsleep 5\n'
'echo GUEST_SUSPEND_REQUEST\n'
f'/bin/rtcwake -u -m mem -s {args.guest_suspend_seconds} && echo GUEST_SUSPEND_RETURNED\n'
'wait "$collector" && echo TIMING_COLLECTION_PASS\n')
(root / 'init').write_text('#!/bin/sh\n'
'export PATH=/bin\nmount -t devtmpfs devtmpfs /dev\nexec </dev/console >/dev/console 2>&1\n'
'mount -t proc proc /proc\nmount -t sysfs sysfs /sys\n'
'uname -a\ncat /proc/version\n'
f'[ "$(uname -r)" = "{release}" ] && echo KERNEL_ARTIFACT_BOOT_PASS\n'
+ timing_command +
'poweroff -f\n')
(root / 'init').chmod(0o755)
names = '.\n' + '\n'.join(str(p.relative_to(root)) for p in sorted(root.rglob('*'))) + '\n'
with (output / 'initrd.cpio').open('wb') as stream:
subprocess.run(['cpio', '-o', '-H', 'newc'], cwd=root, input=names.encode(), stdout=stream, check=True)
command = ['qemu-system-x86_64', '-machine', 'pc-i440fx-10.0', '-accel', 'tcg', '-m', '256', '-smp', '1',
'-nodefaults', '-no-reboot', '-display', 'none', '-serial', 'stdio',
'-monitor', 'none', '-nic', 'none', '-kernel', str(output / 'vmlinuz'),
'-initrd', str(output / 'initrd.cpio'),
'-append', 'console=ttyS0 rdinit=/init panic=1']
try:
result = subprocess.run(command, capture_output=True, timeout=120)
except subprocess.TimeoutExpired as error:
# subprocess.run kills/reaps its own guest; retain partial failure evidence.
result = subprocess.CompletedProcess(command, 124, error.stdout or b'', error.stderr or b'')
(output / 'serial.log').write_bytes(result.stdout + result.stderr)
passed = result.returncode == 0 and b'KERNEL_ARTIFACT_BOOT_PASS' in result.stdout
if args.timing_collector:
passed = passed and b'TIMING_COLLECTION_PASS' in result.stdout
if args.guest_suspend_seconds:
passed = passed and b'GUEST_SUSPEND_RETURNED' in result.stdout
summary = dict(command=command, exit=result.returncode, passed=passed,
archive_sha256=receipt['archive_sha256'], kernel_sha256=receipt['files'][image])
summary['qualification_scope'] = 'direct boot and optional read-only samples; not elapsed-error qualification'
summary['guest_suspend_seconds'] = args.guest_suspend_seconds
summary['qemu_version'] = subprocess.check_output([command[0], '--version'], text=True)
summary['host_kernel'] = subprocess.check_output(['uname', '-a'], text=True).strip()
for name, path in [('receipt', args.receipt), ('initrd', output / 'initrd.cpio'),
('collector', args.timing_collector), ('harness', Path(__file__))]:
if path:
summary[name + '_sha256'] = hashlib.sha256(path.read_bytes()).hexdigest()
(output / 'result.json').write_text(json.dumps(summary, indent=2) + '\n')
print(json.dumps(summary))
return 0 if passed else 1
if __name__ == '__main__':
sys.exit(main())