Verify packaged terminal UX in installed ISO acceptance

This commit is contained in:
Codex
2026-10-13 15:33:15 +03:00
parent 61a5628f9e
commit 73bcf616ee
3 changed files with 299 additions and 3 deletions
+248
View File
@@ -308,6 +308,253 @@ def lifecycle_guest_script():
'\n'.join(inspect.getsource(helper) for helper in helpers) + LIFECYCLE_BODY)
def terminal_ux_iso():
"""An empty ISO9660 filesystem, intentionally without an El Torito boot image."""
def both(value, width):
return value.to_bytes(width, 'little') + value.to_bytes(width, 'big')
def directory_record(identifier):
record = bytearray(34)
record[0] = 34
record[2:10] = both(20, 4)
record[10:18] = both(2048, 4)
record[18:25] = bytes((126, 10, 13, 0, 0, 0, 0))
record[25] = 2
record[28:32] = both(1, 2)
record[32] = 1
record[33] = identifier
return record
data = bytearray(21 * 2048)
primary = bytearray(2048)
primary[:7] = b'\x01CD001\x01'
primary[8:40] = b'HYDRA OS'.ljust(32, b' ')
primary[40:72] = b'HYDRA_UX_PROBE'.ljust(32, b' ')
primary[80:88] = both(21, 4)
primary[120:124] = both(1, 2)
primary[124:128] = both(1, 2)
primary[128:132] = both(2048, 2)
primary[132:140] = both(10, 4)
primary[140:144] = (18).to_bytes(4, 'little')
primary[148:152] = (19).to_bytes(4, 'big')
primary[156:190] = directory_record(0)
primary[190:813] = b' ' * 623
primary[813:830] = b'2026101300000000\x00'
primary[830:847] = b'2026101300000000\x00'
primary[847:881] = b'0000000000000000\x00' * 2
primary[881] = 1
data[16 * 2048:17 * 2048] = primary
data[17 * 2048:17 * 2048 + 7] = b'\xffCD001\x01'
for sector, order in ((18, 'little'), (19, 'big')):
data[sector * 2048:sector * 2048 + 10] = (
b'\x01\x00' + (20).to_bytes(4, order) + (1).to_bytes(2, order) + b'\x00\x00')
data[20 * 2048:20 * 2048 + 68] = directory_record(0) + directory_record(1)
return bytes(data)
def verify_terminal_ux_branding(output, errors):
rendered = re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]', '', output)
if errors.strip() or re.search(r'\b(error|failed|unknown module)\b', rendered, re.I):
raise SmokeError('Installed default Fastfetch emitted errors: ' + (errors or rendered)[-1500:])
if not re.search(r'\bOS\s*:\s*Hydra OS\b', rendered):
raise SmokeError('Installed default Fastfetch did not render the Hydra OS pretty name')
if 'H Y D R A O S' not in rendered or rendered.count('(o >') != 3:
raise SmokeError('Installed default Fastfetch did not render the three-headed Hydra logo')
return rendered
def terminal_ux_snapshot(context):
"""Verify CLI-owned live process/QMP identity, excluding OS boot or audio claims."""
try:
if context['uid'] != os.getuid():
raise SmokeError('Terminal UX process is not owned by the installed normal user')
runtime = Path(context['runtime'])
run = runtime / context['name']
for directory in (runtime, run):
info = directory.lstat()
if (not stat.S_ISDIR(info.st_mode) or info.st_uid != context['uid']
or stat.S_IMODE(info.st_mode) != 0o700):
raise SmokeError('Terminal UX runtime is not private and owned')
if json.loads((run / 'identity.json').read_text()) != {
'name': context['name'], 'uuid': context['uuid']}:
raise SmokeError('Terminal UX metadata belongs to a different VM')
pid = int((run / 'qemu.pid').read_text())
process = Path('/proc') / str(pid)
fields = (process / 'stat').read_text().rsplit(')', 1)[1].split()
if process.stat().st_uid != context['uid'] or fields[0] == 'Z':
raise SmokeError('Terminal UX VM process exited or changed owner')
argv = (process / 'cmdline').read_bytes().split(b'\0')
for option, expected in ((b'-name', context['name']), (b'-uuid', context['uuid'])):
if argv[argv.index(option) + 1] != expected.encode():
raise SmokeError('Terminal UX PID belongs to a different VM')
lifecycle_socket(run / 'qmp.sock', connect_socket=False)
lifecycle_socket(run / 'spice.sock')
with QMPClient(run / 'qmp.sock', timeout=2) as client:
if (client.command('query-name') != {'name': context['name']}
or client.command('query-uuid') != {'UUID': context['uuid']}):
raise SmokeError('Terminal UX QMP belongs to a different VM')
if client.command('query-status').get('status') != 'running':
raise SmokeError('Terminal UX VM is not running')
if not client.command('query-kvm').get('enabled'):
raise SmokeError('Terminal UX VM did not initialise nested KVM')
return {'name': context['name'], 'uuid': context['uuid'], 'pid': pid,
'process_start': fields[19], 'kvm': True}
except (OSError, ValueError, KeyError, IndexError, QMPError) as exc:
raise SmokeError('Terminal UX cannot establish owned live identity: ' + str(exc)) from exc
TERMINAL_UX_BODY = r'''
if os.getuid() == 0 or subprocess.check_output(['id', '-un'], text=True).strip() != 'hydratest':
raise SmokeError('Terminal UX probe must run as the installed normal hydratest user')
if not os.access('/dev/kvm', os.R_OK | os.W_OK):
raise SmokeError('Terminal UX needs installed-user nested KVM access')
if not Path('/usr/bin/hydra').is_file():
raise SmokeError('Installed product launcher /usr/bin/hydra is missing')
fetch = subprocess.run(['fastfetch', '--pipe', 'false', '--show-errors'],
capture_output=True, text=True, timeout=30)
if fetch.returncode:
raise SmokeError('Installed default Fastfetch failed: ' + fetch.stderr[-1500:])
fastfetch_output = verify_terminal_ux_branding(fetch.stdout, fetch.stderr)
config_hash = hashlib.sha256(Path('/etc/fastfetch/config.jsonc').read_bytes()).hexdigest()
logo_hash = hashlib.sha256(Path('/usr/share/hydra/fastfetch.txt').read_bytes()).hexdigest()
base = Path(tempfile.mkdtemp(prefix='hux-', dir='/tmp'))
name = 'ux-' + secrets.token_hex(4)
context = {'name': name, 'uid': os.getuid(), 'config': str(base / 'config'),
'state': str(base / 'state'), 'runtime': str(base / 'run')}
context['uuid'] = str(uuid.uuid5(uuid.NAMESPACE_URL, str(base / 'state' / name / 'disk.qcow2')))
environment = dict(os.environ, XDG_CACHE_HOME=str(base / 'cache'))
environment.pop('HYDRA_SESSION_MANIFEST', None)
command = ['/usr/bin/hydra', '--config', context['config'], '--state', context['state'],
'--runtime', context['runtime']]
def cli(*arguments, input=None):
result = subprocess.run(command + list(arguments), input=input, env=environment,
capture_output=True, text=True, timeout=60)
if result.returncode:
raise SmokeError('Installed hydra ' + arguments[0] + ' failed: ' +
(result.stdout + result.stderr)[-2000:])
return result.stdout
def cli_json(*arguments):
return json.loads(cli(*arguments))
fixture = base / 'fixture.iso'
fixture.write_bytes(terminal_ux_iso())
iso_hash = hashlib.sha256(fixture.read_bytes()).hexdigest()
requests = []
class ISOHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
requests.append(self.path)
if self.path != '/fixture.iso':
self.send_error(404)
return
payload = fixture.read_bytes()
self.send_response(200)
self.send_header('Content-Type', 'application/x-iso9660-image')
self.send_header('Content-Length', str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def log_message(self, *args):
pass
server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), ISOHandler)
worker = threading.Thread(target=server.serve_forever, daemon=True)
worker.start()
url = 'http://127.0.0.1:' + str(server.server_port) + '/fixture.iso'
result = None
try:
if cli_json('list'):
raise SmokeError('Terminal UX private configuration was not initially empty')
empty_menu = cli('menu', input=':quit\n')
if ('Hydra OS' not in empty_menu or 'No VM profiles' not in empty_menu
or list(base.glob('run/*/qemu.pid')) or cli_json('list')):
raise SmokeError('Installed empty menu was unreadable or started a VM')
run_output = cli('run', url, '--name', name, '--memory', '512', '--cpus', '1',
'--disk-size', '64M', '--sha256', iso_hash, '--no-view')
if requests != ['/fixture.iso']:
raise SmokeError('Installed hydra run did not download exactly its loopback ISO')
before = terminal_ux_snapshot(context)
profiles = cli_json('list')
if len(profiles) != 1 or profiles[0]['name'] != name:
raise SmokeError('Installed hydra run did not create exactly the requested private profile')
cached_iso = Path(profiles[0]['iso'])
cache = base / 'cache' / 'hydra' / 'iso'
info = cached_iso.lstat()
if (cached_iso.parent != cache or not stat.S_ISREG(info.st_mode)
or info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) != 0o600
or hashlib.sha256(cached_iso.read_bytes()).hexdigest() != iso_hash
or list(cache.glob('*.part'))):
raise SmokeError('Installed hydra run cache is unsafe, incomplete or has a different checksum')
profile = tomllib.loads((base / 'config' / (name + '.toml')).read_text())
if (profile['memory'], profile['cpus'], profile['disk_size']) != (512, 1, '64M'):
raise SmokeError('Installed hydra run lost its explicit allocations')
downloaded = cli('download', url, '--sha256', iso_hash)
if (Path(downloaded.splitlines()[-1]) != cached_iso or requests != ['/fixture.iso']
or hashlib.sha256(cached_iso.read_bytes()).hexdigest() != iso_hash):
raise SmokeError('Installed hydra download did not reuse the complete cached ISO')
cli('open', name, '--no-view')
after_open = terminal_ux_snapshot(context)
if before != after_open:
raise SmokeError('Installed hydra open restarted or replaced its running VM')
menu = cli('menu', input=':quit\n')
after_menu = terminal_ux_snapshot(context)
if ('Hydra OS' not in menu or name not in menu or 'running' not in menu
or '512 MiB' not in menu or '"status":' in menu or before != after_menu):
raise SmokeError('Installed running menu was unreadable or changed VM lifetime')
result = {'status': 'passed', 'uid': os.getuid(), 'normal_user': 'hydratest',
'launcher': '/usr/bin/hydra', 'fixture_bytes': fixture.stat().st_size,
'iso_sha256': iso_hash, 'cache_reused': True, 'http_requests': requests,
'before': before, 'after_open': after_open, 'after_menu': after_menu,
'memory_mib': profile['memory'], 'cpus': profile['cpus'],
'disk_size': profile['disk_size'], 'network': profile['network'],
'empty_menu': empty_menu, 'running_menu': menu, 'run_output': run_output,
'download_output': downloaded, 'fastfetch_command': ['fastfetch', '--pipe', 'false', '--show-errors'],
'fastfetch_default_config_sha256': config_hash, 'fastfetch_logo_sha256': logo_hash,
'fastfetch_output': fastfetch_output,
'scope': 'Packaged installed CLI, cache, branding and live nested QMP/KVM; nonboot ISO fixture, no nested OS boot or performance claim'}
finally:
try:
status = cli_json('status', name).get('status')
if status == 'running':
# Never signal a saved PID: verify the exact private live identity
# and let the product CLI perform its own identity-checked QMP quit.
terminal_ux_snapshot(context)
if cli_json('stop', name, '--force').get('status') != 'stopped':
raise SmokeError('Owned terminal UX fixture VM did not stop')
elif status != 'stopped':
raise SmokeError('Refusing terminal UX cleanup of an unverified VM identity at ' + str(base))
finally:
server.shutdown()
server.server_close()
worker.join(timeout=5)
if result is not None:
result['cleanup'] = 'Only the verified private fixture VM stopped through hydra stop --force'
print(json.dumps(result))
shutil.rmtree(base)
'''
def terminal_ux_guest_script():
helpers = (lifecycle_socket, terminal_ux_iso, verify_terminal_ux_branding, terminal_ux_snapshot)
return ('import hashlib, http.server, json, os, re, secrets, shutil, socket, stat, subprocess, sys, tempfile, threading, tomllib, uuid\n'
'from pathlib import Path\nsys.path.insert(0, "/usr/lib/hydra")\n'
'from hydra.qmp import QMPClient, QMPError\n'
'class SmokeError(RuntimeError): pass\n' +
'\n'.join(inspect.getsource(helper) for helper in helpers) + TERMINAL_UX_BODY)
def installed_terminal_ux_probe(console):
preparation = ("python3 - <<'HYDRA_TERMINAL_UX_SOURCE'\n" + terminal_ux_guest_script() +
'\nHYDRA_TERMINAL_UX_SOURCE')
output = console.command(preparation, 180)
try:
result = json.loads(output)
except ValueError as exc:
raise SmokeError('Installed terminal UX did not return framed JSON evidence') from exc
if result.get('status') != 'passed':
raise SmokeError('Installed terminal UX did not verify its packaged CLI')
return output
def installed_admin(console, password):
console.buffer = b''
prompt_encoded = base64.b64encode(b'HYDRA_SUDO_PASSWORD: ').decode()
@@ -875,6 +1122,7 @@ def main(argv=None):
if output.strip() != b'hydratest':
raise SmokeError('Installed account identity mismatch')
checkpoint('installed_password_login', output)
checkpoint('installed_terminal_ux', installed_terminal_ux_probe(console))
console.buffer = b''
prompt_encoded = base64.b64encode(b'HYDRA_SUDO_PASSWORD: ').decode()
console.send(f'sudo -p "$(printf %s {prompt_encoded} | base64 -d)" -i\n')