Make ISO acceptance probes fail on forbidden state

This commit is contained in:
Codex
2026-10-13 04:42:34 +03:00
parent f005c2ee28
commit 30ac68ed60
2 changed files with 187 additions and 7 deletions
+31 -7
View File
@@ -260,19 +260,43 @@ findmnt -n -o FSTYPE / | grep -qx overlay
test -f /run/initramfs/live/LiveOS/squashfs.img
sudo -n true
for tool in hydra hydra-install qemu-system-x86_64 qemu-img virt-viewer foot labwc; do command -v "$tool"; done
systemctl is-active NetworkManager hydra-network hydra-dnsmasq
for service in NetworkManager hydra-network hydra-dnsmasq; do systemctl is-active "$service"; done
'''
INSTALLED_CHECK = r'''set -eu
assert_no_match() {
if grep "$@"; then
printf 'Forbidden live configuration remains\n' >&2
exit 1
else
status=$?
[ "$status" -eq 1 ] || exit "$status"
fi
}
[ "$(findmnt -n -o FSTYPE /)" = ext4 ]
[ "$(readlink -f "$(findmnt -n -o SOURCE /)")" = /dev/vda2 ]
! grep -Eq 'root=live:|rd\.live' /proc/cmdline
! findmnt -n -o FSTYPE / | grep -Eq 'overlay|squashfs'
assert_no_match -Eq 'root=live:|rd\.live' /proc/cmdline
test ! -e /etc/hydra/live-mode
! getent passwd hydra-live
! find /etc/systemd/system /etc/sudoers.d -name 90-hydra-live.conf -o -name 90-hydra-live | grep .
test ! -L /etc/hydra/live-mode
if getent passwd hydra-live; then
printf 'Live account remains\n' >&2
exit 1
else
status=$?
[ "$status" -eq 2 ] || exit "$status"
fi
remaining=$(find /etc/systemd/system /etc/sudoers.d \( -name 90-hydra-live.conf -o -name 90-hydra-live \) -print)
[ -z "$remaining" ]
test ! -e /etc/greetd/live.toml
! grep -R -E -- '--autologin|NOPASSWD' /etc/systemd/system/getty@*.service.d /etc/systemd/system/serial-getty@*.service.d /etc/sudoers.d 2>/dev/null
test ! -L /etc/greetd/live.toml
for directory in /etc/systemd/system/getty@*.service.d /etc/systemd/system/serial-getty@*.service.d /etc/sudoers.d; do
if [ -d "$directory" ]; then
assert_no_match -R -E -- '--autologin|NOPASSWD' "$directory"
elif [ -e "$directory" ] || [ -L "$directory" ]; then
printf 'Unexpected non-directory configuration path\n' >&2
exit 1
fi
done
getent shadow root | cut -d: -f2 | grep -Eq '^[!*]'
getent shadow hydratest | cut -d: -f2 | grep -Eq '^\$'
for service in NetworkManager greetd hydra-network hydra-dnsmasq; do systemctl is-active "$service"; done
@@ -329,7 +353,7 @@ try:
port = 43117
server_code = "import socket; s=socket.socket(); s.settimeout(15); s.bind(('10.203.0.250',43117)); s.listen(1); c,a=s.accept(); c.sendall(b'HYDRA_LAN_TO_VM_OK'); c.close(); s.close()"
server = subprocess.Popen(['ip', 'netns', 'exec', guest, 'python3', '-c', server_code], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
client_code = "import socket,time; deadline=time.monotonic()+10\nwhile True:\n try:\n s=socket.create_connection(('10.203.0.250',43117),1); break\n except OSError:\n if time.monotonic()>deadline: raise\n time.sleep(.1)\nassert s.recv(100)==b'HYDRA_LAN_TO_VM_OK'; s.close(); print('PASS: routed new LAN-to-VM TCP connection on arbitrary port 43117')"
client_code = "import socket,time; deadline=time.monotonic()+10\nwhile True:\n try:\n s=socket.create_connection(('10.203.0.250',43117),1); break\n except OSError:\n if time.monotonic()>deadline: raise\n time.sleep(.1)\nexpected=b'HYDRA_LAN_TO_VM_OK'; received=b''\nwhile len(received)<len(expected):\n remaining=deadline-time.monotonic()\n if remaining<=0: raise TimeoutError('TCP marker deadline exceeded')\n s.settimeout(remaining)\n part=s.recv(len(expected)-len(received))\n if not part: raise RuntimeError('Truncated TCP marker')\n received+=part\nassert received==expected; s.close(); print('PASS: routed new LAN-to-VM TCP connection on arbitrary port 43117')"
print(run('ip', 'netns', 'exec', lan, 'python3', '-c', client_code), end='')
assert server.wait(timeout=10) == 0
finally:
+156
View File
@@ -141,3 +141,159 @@ class SmokeTests(unittest.TestCase):
with self.assertRaises(self.mod.SmokeError):
self.mod.qemu_argv('qemu', self.root, comma, self.code,
self.variables, None, 2048)
def probe_fixture(self, case, live=False):
# Run the production shell against ordinary scratch files. Commands that
# query guest state are boundary fixtures; grep/find/test are real tools.
import os
root = self.root / 'guest'
for directory in ('etc/hydra', 'etc/greetd', 'etc/sudoers.d',
'etc/systemd/system/getty@tty2.service.d',
'etc/systemd/system/serial-getty@ttyS0.service.d',
'usr/share/firmware', 'proc', 'sys/firmware/efi',
'run/initramfs/live/LiveOS'):
(root / directory).mkdir(parents=True, exist_ok=True)
(root / 'etc/greetd/config.toml').write_text('normal authentication')
(root / 'usr/share/firmware/OVMF_CODE.fd').write_text('firmware')
(root / 'proc/cmdline').write_text('root=UUID=disk ro' if case != 'live-cmdline' else 'root=live:CDLABEL=HYDRA_OS rd.live.image')
if live or case == 'live-marker':
(root / 'etc/hydra/live-mode').mkdir(exist_ok=True)
(root / 'etc/hydra/live-mode/active').touch()
if live:
(root / 'run/initramfs/live/LiveOS/squashfs.img').touch()
files = {
'live-dropin': ('etc/systemd/system/getty@tty2.service.d/90-hydra-live.conf', 'live'),
'live-sudoers': ('etc/sudoers.d/90-hydra-live', 'live'),
'autologin': ('etc/systemd/system/serial-getty@ttyS0.service.d/other.conf', 'ExecStart=agetty --autologin hydratest'),
'nopasswd': ('etc/sudoers.d/other', 'hydratest ALL=(ALL) NOPASSWD: ALL'),
'live-greetd': ('etc/greetd/live.toml', 'live'),
}
if case in files:
path, text = files[case]
(root / path).write_text(text)
if case == 'config-read-error':
unreadable = root / 'etc/sudoers.d/unreadable'
unreadable.write_text('configuration')
unreadable.chmod(0)
if case == 'cmdline-read-error':
(root / 'proc/cmdline').unlink()
commands = self.root / 'fixture-bin'
commands.mkdir(exist_ok=True)
dispatcher = commands / 'fixture'
dispatcher.write_text('''#!/usr/bin/env python3
import os, pathlib, sys
name = pathlib.Path(sys.argv[0]).name
args = sys.argv[1:]
case = os.environ['SMOKE_CASE']
if name == 'id': print('hydra-live')
elif name == 'findmnt': print('/dev/vda2' if 'SOURCE' in args else ('overlay' if os.environ['SMOKE_LIVE']=='1' else 'ext4'))
elif name == 'systemctl':
if args[0] == 'is-active':
states = [unit != case for unit in args[1:]]
print('\\n'.join('active' if state else 'failed' for state in states))
sys.exit(0 if any(states) else 3)
print('success' if 'Result' in args else '0')
elif name == 'getent':
if args[:2] == ['passwd','hydra-live']:
if case == 'account-lookup-error': sys.exit(3)
if case != 'live-account': sys.exit(2)
print('hydra-live:x:1000:1000::/home/hydra-live:/bin/bash')
elif args[0] == 'shadow': print(args[1] + ':' + ('!' if args[1]=='root' else '$6$hash') + ':1::::::')
else: print('127.0.0.1 STREAM example')
elif name == 'ip': print('10.203.0.1/24' if 'addr' in args else 'default via 10.0.2.2')
elif name == 'nmcli': print('connected')
else: print('fixture tool')
''')
dispatcher.chmod(0o755)
for command in ('id', 'findmnt', 'systemctl', 'getent', 'ip', 'nft',
'nmcli', 'sudo', 'hydra', 'hydra-install',
'qemu-system-x86_64', 'qemu-img', 'virt-viewer', 'foot', 'labwc'):
path = commands / command
if not path.exists():
path.symlink_to(dispatcher)
probe = self.mod.LIVE_CHECK if live else self.mod.INSTALLED_CHECK
for path in ('/etc/', '/usr/share', '/proc/cmdline', '/sys/firmware/efi', '/run/initramfs/live/LiveOS/squashfs.img'):
probe = probe.replace(path, str(root) + path)
env = dict(os.environ, PATH=str(commands) + ':' + os.environ['PATH'],
SMOKE_CASE=case, SMOKE_LIVE='1' if live else '0')
result = subprocess.run(['/bin/sh', '-c', self.mod.frame_command(probe, 'probecheck')],
env=env, capture_output=True, timeout=10)
parsed = self.mod.parse_frame(result.stdout, 'probecheck')
self.assertIsNotNone(parsed, result.stderr)
return parsed[0]
def test_installed_probe_rejects_each_retained_live_artifact(self):
self.assertEqual(self.probe_fixture('clean'), 0)
for case in ('live-cmdline', 'live-account', 'live-dropin', 'live-sudoers',
'autologin', 'nopasswd', 'live-marker', 'live-greetd'):
with self.subTest(case=case):
# Give each negative case an independent clean filesystem.
import shutil
shutil.rmtree(self.root / 'guest')
self.assertNotEqual(self.probe_fixture(case), 0)
def test_installed_probe_does_not_treat_account_query_error_as_absence(self):
self.assertNotEqual(self.probe_fixture('account-lookup-error'), 0)
def test_installed_probe_rejects_unreadable_configuration(self):
self.assertNotEqual(self.probe_fixture('config-read-error'), 0)
def test_installed_probe_rejects_unreadable_boot_arguments(self):
self.assertNotEqual(self.probe_fixture('cmdline-read-error'), 0)
def test_installed_probe_rejects_each_failed_required_service(self):
self.assertEqual(self.probe_fixture('clean'), 0)
for service in ('NetworkManager', 'greetd', 'hydra-network', 'hydra-dnsmasq'):
with self.subTest(service=service):
self.assertNotEqual(self.probe_fixture(service), 0)
def test_live_probe_rejects_each_failed_required_service(self):
self.assertEqual(self.probe_fixture('clean', live=True), 0)
for service in ('NetworkManager', 'hydra-network', 'hydra-dnsmasq'):
with self.subTest(service=service):
self.assertNotEqual(self.probe_fixture(service, live=True), 0)
def tcp_probe(self, fragments):
# Execute the exact Python client embedded in the guest network probe,
# redirecting only its address to a fresh loopback socket.
import ast
import time
source = self.mod.NETWORK_CHECK.split("<<'NETWORK'\n", 1)[1].rsplit('\nNETWORK\n', 1)[0]
assignments = (node for node in ast.walk(ast.parse(source)) if isinstance(node, ast.Assign))
value = next(node.value for node in assignments
if any(isinstance(target, ast.Name) and target.id == 'client_code' for target in node.targets))
client = ast.parse(ast.literal_eval(value))
listener = socket.socket()
self.addCleanup(listener.close)
listener.bind(('127.0.0.1', 0))
listener.listen(1)
listener.settimeout(3)
host, port = listener.getsockname()
class Redirect(ast.NodeTransformer):
def visit_Tuple(self, node):
if isinstance(node.elts[0], ast.Constant) and node.elts[0].value == '10.203.0.250':
return ast.Tuple(elts=[ast.Constant(host), ast.Constant(port)], ctx=ast.Load())
return self.generic_visit(node)
client = ast.fix_missing_locations(Redirect().visit(client))
def server():
connection, _ = listener.accept()
with connection:
for fragment in fragments:
try:
connection.sendall(fragment)
except BrokenPipeError:
break
time.sleep(.05)
thread = threading.Thread(target=server)
thread.start()
result = subprocess.run(['python3', '-c', ast.unparse(client)], capture_output=True, timeout=5)
thread.join(timeout=3)
self.assertFalse(thread.is_alive())
return result
def test_tcp_probe_accepts_complete_marker_across_fragmented_reads(self):
result = self.tcp_probe([b'HYDRA_', b'LAN_TO_', b'VM_OK'])
self.assertEqual(result.returncode, 0, result.stderr)
def test_tcp_probe_rejects_truncated_marker(self):
self.assertNotEqual(self.tcp_probe([b'HYDRA_LAN_TO_']).returncode, 0)