fix(test): restore pruned live boot entries and verify mounted ESP
This commit is contained in:
+3
-2
@@ -651,8 +651,9 @@ def qemu_argv(qemu, scratch, disk, code, variables, iso, memory, *, keep_cdrom_d
|
||||
if not Path(iso).is_file():
|
||||
raise SmokeError('Trusted ISO must exist as a regular file')
|
||||
if keep_cdrom_drive:
|
||||
# Keep one SATA0 device even without media so OVMF retains its DVD boot
|
||||
# entry across ISO-free boots. No file= means there is no optical medium.
|
||||
# Keep SATA0 stable so a saved DVD boot option has the same device path
|
||||
# when media returns. OVMF may still prune the empty drive's boot entry.
|
||||
# No file= means there is no optical medium.
|
||||
optical = 'if=ide,index=0,id=acceptance-cdrom,media=cdrom,readonly=on'
|
||||
if iso is not None:
|
||||
optical += ',format=raw,file=' + qemu_path(iso)
|
||||
|
||||
+120
-7
@@ -44,8 +44,11 @@ def argument_parser():
|
||||
return parser
|
||||
|
||||
|
||||
def verify_preserved(before, after):
|
||||
for name in ('gpt', 'foreign_sha256', 'foreign_efi', 'boot_order'):
|
||||
def verify_preserved(before, after, *, firmware_reboot=False):
|
||||
fields = ('gpt', 'foreign_sha256', 'foreign_efi')
|
||||
if not firmware_reboot:
|
||||
fields += ('boot_order',)
|
||||
for name in fields:
|
||||
if before[name] != after[name]:
|
||||
raise SmokeError('Preserved installation changed ' + name)
|
||||
|
||||
@@ -137,6 +140,72 @@ def sha(path):
|
||||
def tree(path):
|
||||
return {str(p.relative_to(path)): sha(p) for p in sorted(path.rglob('*')) if p.is_file()}
|
||||
|
||||
EFI_GUID = '8be4df61-93ca-11d2-aa0d-00e098032b8c'
|
||||
DVD_LABEL = 'UEFI QEMU DVD-ROM QM00001'
|
||||
|
||||
def efi_description(data):
|
||||
# The file starts with efivarfs attributes, EFI_LOAD_OPTION attributes and
|
||||
# FilePathListLength, followed by a bounded UTF-16 description.
|
||||
for end in range(10, min(len(data) - 1, 522), 2):
|
||||
if data[end:end + 2] == b'\0\0':
|
||||
try: return data[10:end].decode('utf-16-le').strip(), end + 2
|
||||
except UnicodeDecodeError: break
|
||||
return None, None
|
||||
|
||||
def validate_dvd_variable(data):
|
||||
label, offset = efi_description(data)
|
||||
path = bytes.fromhex('02010c00d041030a0000000001010600021f03120a000000ffff00007fff0400')
|
||||
automatic = bytes.fromhex('4eac0881119f594d850ee21a522c59b2')
|
||||
if (len(data) > 512 or data[:10] != bytes.fromhex('07000000010000002000')
|
||||
or label != DVD_LABEL or data[offset:offset + 32] != path
|
||||
or data[offset + 32:] not in (b'', automatic)):
|
||||
raise RuntimeError('Invalid saved SATA0 DVD EFI load option')
|
||||
|
||||
def dvd_variables(variables):
|
||||
matches = []
|
||||
for path in variables.glob('Boot????-' + EFI_GUID):
|
||||
if not re.fullmatch('Boot[0-9A-Fa-f]{4}-' + EFI_GUID, path.name): continue
|
||||
data = path.read_bytes()
|
||||
if efi_description(data)[0] == DVD_LABEL:
|
||||
validate_dvd_variable(data)
|
||||
matches.append((path.name[4:8], data))
|
||||
if len(matches) > 1: raise RuntimeError('Ambiguous DVD EFI load options')
|
||||
return matches
|
||||
|
||||
def capture_dvd_variable(variables):
|
||||
matches = dvd_variables(variables)
|
||||
if len(matches) != 1: raise RuntimeError('Expected one active SATA0 DVD EFI load option')
|
||||
entry, data = matches[0]
|
||||
current = (variables / ('BootCurrent-' + EFI_GUID)).read_bytes()
|
||||
if len(current) != 6 or int.from_bytes(current[4:], 'little') != int(entry, 16):
|
||||
raise RuntimeError('Initial live system did not boot from the captured DVD entry')
|
||||
return {'entry': entry, 'raw_hex': data.hex(), 'sha256': hashlib.sha256(data).hexdigest()}
|
||||
|
||||
def restore_dvd_variable(variables, data):
|
||||
validate_dvd_variable(data)
|
||||
order = variables / ('BootOrder-' + EFI_GUID)
|
||||
before = order.read_bytes()
|
||||
matches = dvd_variables(variables)
|
||||
created = not matches
|
||||
if matches:
|
||||
entry, existing = matches[0]
|
||||
if existing != data: raise RuntimeError('Existing DVD option differs from captured bytes')
|
||||
else:
|
||||
occupied = {int(p.name[4:8], 16) for p in variables.glob('Boot????-' + EFI_GUID)
|
||||
if re.fullmatch('Boot[0-9A-Fa-f]{4}-' + EFI_GUID, p.name)}
|
||||
free = next((number for number in range(65536) if number not in occupied), None)
|
||||
if free is None: raise RuntimeError('No unused UEFI boot variable')
|
||||
entry = f'{free:04X}'
|
||||
path = variables / ('Boot' + entry + '-' + EFI_GUID)
|
||||
# Never overwrite an existing entry, including a concurrently created one.
|
||||
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
||||
with os.fdopen(fd, 'wb', buffering=0) as target:
|
||||
if target.write(data) != len(data): raise RuntimeError('Short EFI variable write')
|
||||
if path.read_bytes() != data: raise RuntimeError('Saved DVD variable readback mismatch')
|
||||
if order.read_bytes() != before: raise RuntimeError('DVD variable creation changed BootOrder')
|
||||
return {'entry': entry, 'created': created, 'sha256': hashlib.sha256(data).hexdigest(),
|
||||
'boot_order_sha256': hashlib.sha256(before).hexdigest()}
|
||||
|
||||
@contextlib.contextmanager
|
||||
def mounted(device, filesystem, options=''):
|
||||
import tempfile
|
||||
@@ -146,10 +215,35 @@ def mounted(device, filesystem, options=''):
|
||||
try: yield Path(directory)
|
||||
finally: run('umount', directory)
|
||||
|
||||
def installed_esp(marker):
|
||||
mounts = json.loads(run('findmnt', '--json', '--mountpoint', '/boot/efi', '--output',
|
||||
'SOURCE,TARGET,FSTYPE,UUID,MAJ:MIN'))['filesystems']
|
||||
devices = json.loads(run('lsblk', '--json', '--nodeps', '--output',
|
||||
'NAME,MAJ:MIN,FSTYPE,UUID', '/dev/vda1'))['blockdevices']
|
||||
if len(mounts) != 1 or len(devices) != 1:
|
||||
raise RuntimeError('Expected one mounted installed ESP and one test ESP device')
|
||||
mount, device = mounts[0], devices[0]
|
||||
if (mount.get('source') != '/dev/vda1' or mount.get('target') != '/boot/efi'
|
||||
or mount.get('fstype') != 'vfat' or device.get('name') != 'vda1'
|
||||
or device.get('fstype') != 'vfat' or not device.get('maj:min')
|
||||
or mount.get('maj:min') != device['maj:min'] or not device.get('uuid')
|
||||
or mount.get('uuid') != device['uuid'] or marker.get('esp_uuid') != device['uuid']):
|
||||
raise RuntimeError('Installed ESP mount/device identity does not match the storage marker')
|
||||
return Path('/boot/efi')
|
||||
|
||||
@contextlib.contextmanager
|
||||
def readonly_esp():
|
||||
if Path('/etc/hydra/live-mode/active').exists():
|
||||
with mounted('/dev/vda1', 'vfat', 'ro') as esp: yield esp
|
||||
else:
|
||||
# The installed ESP is already mounted rw by fstab. Verify it, then only
|
||||
# read hashes; a second ro mount would conflict with its superblock state.
|
||||
yield installed_esp(json.loads(Path('/etc/hydra/storage.json').read_text()))
|
||||
|
||||
def preserved():
|
||||
table = json.loads(run('sfdisk', '--json', '/dev/vda'))['partitiontable']
|
||||
# No filesystem UUIDs: selected root UUID must change on formatting.
|
||||
with mounted('/dev/vda1', 'vfat', 'ro') as esp:
|
||||
with readonly_esp() as esp:
|
||||
foreign = {k: v for k, v in tree(esp / 'EFI').items() if not k.startswith('HydraOS/')}
|
||||
order = re.search(r'^BootOrder:\s*(.*)$', run('efibootmgr'), re.M)
|
||||
assert order, 'UEFI BootOrder was not observable'
|
||||
@@ -202,6 +296,8 @@ def stop_child(source):
|
||||
'''
|
||||
|
||||
GUEST_BODIES = {
|
||||
'capture-live-boot': "result = capture_dvd_variable(Path('/sys/firmware/efi/efivars'))\n",
|
||||
'restore-live-boot': "result = restore_dvd_variable(Path('/sys/firmware/efi/efivars'), bytes.fromhex(context['dvd_variable']))\n",
|
||||
'seed': r'''
|
||||
# Guest-only target is the new empty disk attached by this harness.
|
||||
assert os.geteuid() == 0 and Path('/etc/hydra/live-mode/active').exists()
|
||||
@@ -263,7 +359,7 @@ assert marker['root_subvolume'] == '@root'
|
||||
assert Path('/home/hydratest/acceptance-home').read_text() == 'current-' + context['sentinel']
|
||||
assert not Path('/var/db/pkg/app-misc/hydra-acceptance-2.0').exists()
|
||||
assert Path('/usr/share/hydra/base-fixture.txt').read_text().strip() == '1.0'
|
||||
with mounted('/dev/vda1', 'vfat', 'ro') as esp: result = base_state(Path('/'), esp)
|
||||
with readonly_esp() as esp: result = base_state(Path('/'), esp)
|
||||
result['home'] = Path('/home/hydratest/acceptance-home').read_text()
|
||||
result['vm_data'] = tree(Path('/home/hydratest/.local/share/hydra/acceptance-guest'))
|
||||
result['root_mount'] = run('findmnt', '-n', '-o', 'SOURCE,OPTIONS', '/')
|
||||
@@ -417,7 +513,8 @@ def main(argv=None):
|
||||
inputs = {str(p): iso_smoke.digest(p) for p in (args.iso, args.ovmf_code, args.ovmf_vars)}
|
||||
report = {'schema': 1, 'status': 'running', 'inputs_sha256': inputs, 'checks': {},
|
||||
'physical_hardware': 'pending', 'kernel_scope': 'exact kernel restoration; no two-kernel upgrade claim',
|
||||
'trust_scope': 'disposable TEST key temporarily pinned only in this disposable installed guest'}
|
||||
'trust_scope': 'disposable TEST key temporarily pinned only in this disposable installed guest',
|
||||
'firmware_scope': 'BootOrder checked around each installer/checkpoint/restore/selector operation; firmware may prune empty-media entries between boots'}
|
||||
context = {'sentinel': secrets.token_hex(32)}
|
||||
password = secrets.token_urlsafe(24); vm = None; server = None
|
||||
def record(name, evidence):
|
||||
@@ -456,6 +553,8 @@ def main(argv=None):
|
||||
threading.Thread(target=server.serve_forever, daemon=True).start()
|
||||
console = boot(True, 'live-install')
|
||||
before = probe(console, 'seed', context); record('seeded_foreign_invariants', before)
|
||||
live_boot = probe(console, 'capture-live-boot', context)
|
||||
context['dvd_variable'] = live_boot['raw_hex']; record('captured_live_boot_option', live_boot)
|
||||
token = secrets.token_hex(12); console.buffer = b''
|
||||
console.send(partition_installer_command(token) + '\n')
|
||||
console.expect_installer(rb'Installed account password: ', 120, token); console.send(password + '\n')
|
||||
@@ -472,8 +571,11 @@ def main(argv=None):
|
||||
iso_smoke.installed_admin(console, password)
|
||||
baseline = probe(console, 'baseline', context, 720); record('real_portage_baseline_doctor_vmdata', baseline)
|
||||
record('signed_runtime_update_recovery', probe(console, 'runtime', context, 300))
|
||||
record('harness_checkpoint_dvd_option', probe(console, 'restore-live-boot', context))
|
||||
record('harness_checkpoint_bootnext', select_live(console)); shutdown(console)
|
||||
console = boot(True, 'live-checkpoint')
|
||||
checkpoint_preserved = probe(console, 'preserved', context)
|
||||
verify_preserved(before, checkpoint_preserved, firmware_reboot=True)
|
||||
base_before = probe(console, 'base-state', context); record('base_before_checkpoint', base_before)
|
||||
context['root_uuid'] = console.command('sudo -n blkid -s UUID -o value /dev/vda3').decode().strip()
|
||||
esp_partuuid = console.command('sudo -n blkid -s PARTUUID -o value /dev/vda1').decode().strip()
|
||||
@@ -483,10 +585,16 @@ def main(argv=None):
|
||||
cp = json.loads(console.command(command + ' --execute --confirm-root-uuid ' + shlex.quote(context['root_uuid']), 300))
|
||||
context['checkpoint'] = cp['id']; record('real_offline_checkpoint', cp)
|
||||
record('checkpoint_list', json.loads(console.command('sudo -n hydra-system list --root-uuid ' + shlex.quote(context['root_uuid']), 180)))
|
||||
checkpoint_after = probe(console, 'preserved', context)
|
||||
verify_preserved(checkpoint_preserved, checkpoint_after)
|
||||
record('checkpoint_preserved_firmware_and_data', checkpoint_after)
|
||||
select_installed(console, recreate=True); shutdown(console)
|
||||
console = boot(False, 'installed-upgrade'); record('real_portage_upgrade_current_home', probe(console, 'upgrade', context, 720))
|
||||
record('harness_restore_dvd_option', probe(console, 'restore-live-boot', context))
|
||||
record('harness_restore_bootnext', select_live(console)); shutdown(console)
|
||||
console = boot(True, 'live-restore')
|
||||
restore_preserved = probe(console, 'preserved', context)
|
||||
verify_preserved(before, restore_preserved, firmware_reboot=True)
|
||||
record('deliberate_kernel_efi_damage', probe(console, 'damage', context))
|
||||
record('restore_sigkill_real_btrfs', probe(console, 'restore-interrupt', context, 300))
|
||||
restore = ('sudo -n hydra-system restore --root-uuid ' + shlex.quote(context['root_uuid']) +
|
||||
@@ -495,13 +603,18 @@ def main(argv=None):
|
||||
record('fresh_process_restore_resume', json.loads(console.command(restore, 300)))
|
||||
base_after = probe(console, 'base-state', context)
|
||||
if base_after != base_before: raise SmokeError('Offline restore did not restore exact base/Portage/kernel/ownedEFI hashes')
|
||||
preserved_after = probe(console, 'preserved', context); verify_preserved(before, preserved_after)
|
||||
preserved_after = probe(console, 'preserved', context)
|
||||
verify_preserved(restore_preserved, preserved_after)
|
||||
verify_preserved(before, preserved_after, firmware_reboot=True)
|
||||
record('restored_base_foreign_invariants', {'base': base_after, 'preserved': preserved_after})
|
||||
select_installed(console, recreate=True); shutdown(console)
|
||||
console = boot(False, 'installed-restored'); restored = probe(console, 'restored', context)
|
||||
if any(restored[k] != v for k, v in base_before.items()): raise SmokeError('Disk-only boot did not retain restored base')
|
||||
if restored['vm_data'] != baseline['vm_data']: raise SmokeError('VM disk/profile bytes changed')
|
||||
record('disk_only_restored_boot_current_home_vmdata', restored); shutdown(console)
|
||||
record('disk_only_restored_boot_current_home_vmdata', restored)
|
||||
final_preserved = probe(console, 'preserved', context)
|
||||
verify_preserved(before, final_preserved, firmware_reboot=True)
|
||||
record('final_disk_boot_foreign_invariants', final_preserved); shutdown(console)
|
||||
if any(iso_smoke.digest(Path(path)) != digest for path, digest in inputs.items()): raise SmokeError('Input ISO/firmware changed')
|
||||
report['status'] = 'passed'; return 0
|
||||
except (SmokeError, OSError, ValueError, subprocess.SubprocessError, iso_smoke.QMPError) as exc:
|
||||
|
||||
Reference in New Issue
Block a user