Files
2026-09-21 22:29:23 +08:00

72 lines
3.5 KiB
Python

#!/usr/bin/env python3
"""Preserve a newer wall clock through the actual native startup sequence."""
import io
import json
from pathlib import Path
import subprocess
import sys
import tarfile
import tempfile
import time
project = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(project / 'tools'))
from image_formats import digest
from vm_test import VM
work = Path(tempfile.mkdtemp(prefix='m12-clock.', dir=project / 'out'))
rootfs = (project / 'out/rootfs-development.tar').resolve(strict=True)
stage2 = 'etc/s6-linux-init/current/scripts/rc.init'
future = 1_893_456_000 # 2030-01-01 UTC, set only inside the disposable guest.
with tarfile.open(rootfs) as source, tarfile.open(work / 'clock.tar', 'w', format=tarfile.PAX_FORMAT) as output:
assert source.extractfile('usr/share/fds/image-profile').read().strip() == b'development'
epoch = int(source.extractfile('usr/share/fds/build-epoch').read())
assert epoch < future
found = False
for member in source:
if member.name == stage2:
member.name += '.real'
found = True
output.addfile(member, source.extractfile(member) if member.isfile() else None)
assert found
# Preserve the actual stage 2 and boottrace binary. This fixture only sets
# the guest kernel clock before the normal startup code adopts boot events.
data = f'#!/bin/bash\nset -euo pipefail\ndate -u -s @{future} >/dev/null\nexec /{stage2}.real "$@"\n'.encode()
member = tarfile.TarInfo(stage2)
member.mode = 0o755
member.size = len(data)
output.addfile(member, io.BytesIO(data))
(work / 'image').mkdir()
with (work / 'image.log').open('wb') as log:
subprocess.run([str(project / 'image/build-system-cartridge'), '--profile', 'development',
'--rootfs', str(work / 'clock.tar'), '--output-directory', str(work / 'image')],
check=True, stdout=log, stderr=subprocess.STDOUT)
image = work / 'image/system.img'
before = digest(image)
with VM(work, 'retained-clock', image) as vm:
vm.expect(rb'FDS> ')
assert vm.capture('id -u') == '1000'
status = json.loads(vm.capture('cat /run/fds/clock.json'))
assert status['source'] == 'retained_kernel_clock', status
assert status['previous_unix_seconds'] >= future, status
assert status['minimum_unix_seconds'] == status['previous_unix_seconds'], status
assert int(vm.capture('date -u +%s')) >= future
assert 'requires root' in vm.capture('fds-boottrace clock-floor', ok=False)
report = json.loads(vm.capture('fds --json boot-profile'))
assert report['clock'] == 'Linux CLOCK_BOOTTIME', report
assert 0 < report['durations_ns']['kernel-to-console'] < 120_000_000_000, report
(work / 'clock.json').write_text(json.dumps(status, indent=2) + '\n')
(work / 'boot-profile.json').write_text(json.dumps(report, indent=2) + '\n')
deadline = time.monotonic() + 60
while vm.capture('test -S /run/fds/control.sock && echo ready || echo pending') != 'ready':
assert time.monotonic() < deadline
vm.send('fds poweroff')
vm.expect(rb'reboot: Power down')
assert vm.child.wait(timeout=20) == 0
assert digest(image) == before
link = project / 'out/m12-clock-latest.next'
link.symlink_to(work.name)
link.replace(project / 'out/m12-clock-latest')
print(f'PASS: newer guest clock preserved, ordinary-user mutation rejected, monotonic boot measurement retained, native halt: {work}')
print('NOTE: the test changes only a disposable VM clock; physical Pi RTC behavior remains deferred')