Files
archy/scripts/tests/runtime_service_fixture.py

648 lines
37 KiB
Python

#!/usr/bin/python3
"""Disposable root/kernel fixture; requires its own private run/home/data/net.
No native compilation. The protocol client implements the frozen C proxy wire.
The access lease fixture is synthetic; it is NOT production access qualification.
"""
import array
import ctypes
import json
import os
from pathlib import Path
import select
import shutil
import signal
import socket
import struct
import subprocess
import sys
import time
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import c01_runtime as runtime
from c01_runtime_view import RuntimeLease
from c01_runtime_groups import current_group
LIBC = ctypes.CDLL(None, use_errno=True)
UID, MANAGER, AUTHORITY, OPERATOR = 61000, 61001, 61002, 61003
HOME = Path('/home/runtime-fixture')
BACKING = Path('/var/lib/runtime-home')
RUN = Path(f'/run/user/{UID}')
DATA = Path('/var/lib/runtime-data')
CODE = Path('/run/runtime-code')
UNITS = Path('/run/runtime-units')
def require(value, message='fixture assertion failed'):
if not value:
raise AssertionError(message)
def wait_for(check, timeout=20):
end = time.monotonic() + timeout
while time.monotonic() < end:
result = check()
if result:
return result
time.sleep(.05)
raise AssertionError('fixture deadline expired')
def drop(uid):
os.setgroups([]); os.setresgid(uid, uid, uid); os.setresuid(uid, uid, uid)
def mount(source, target, kind=None, flags=4096, data=None):
require(LIBC.mount(os.fsencode(source) if source else None, os.fsencode(target),
os.fsencode(kind) if kind else None, flags, os.fsencode(data) if data else None) == 0,
f'mount failed: {source} -> {target}: errno {ctypes.get_errno()}')
def owned(path, uid=UID):
path.mkdir(parents=True, exist_ok=True)
path.chmod(0o700); os.chown(path, uid, uid)
def packet(argv, cwd=str(HOME), env=()):
def string(text):
raw = text.encode(); return struct.pack('!I', len(raw)) + raw
raw = struct.pack('!IIII', runtime.runtime_owner.MAGIC, 1, len(argv), len(env))
raw += b''.join(string(text) for text in [cwd, *argv, *env])
return struct.pack('!I', len(raw)) + raw
def client(argv, uid=MANAGER, gid=None, disconnect=False):
# Fork gives real SO_PEERCRED. Regular fixture files avoid pipe-EOF ambiguity
# from an intentionally persistent container/tmux process retaining stdout.
out = os.memfd_create('runtime-output', os.MFD_CLOEXEC)
err = os.memfd_create('runtime-error', os.MFD_CLOEXEC)
report_read, report_write = os.pipe2(os.O_CLOEXEC)
cancel_read, cancel_write = os.pipe2(os.O_CLOEXEC)
pid = os.fork()
if pid == 0:
try:
os.close(report_read)
os.close(cancel_write)
os.setgroups([MANAGER] if gid is not None else [])
os.setresgid(gid or uid, gid or uid, gid or uid); os.setresuid(uid, uid, uid)
channel = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
channel.settimeout(45)
channel.connect('/run/archipelago-managed/runtime.sock')
# Exactly what runtime-proxy.c requires; root creator must remain >1.
peer_pid, peer_uid, peer_gid = runtime.peer(channel)
require(peer_pid > 1 and (peer_uid, peer_gid) == (0, 0), 'root listener credential mismatch')
with open('/dev/null', 'rb') as source:
raw = packet(argv)
count = channel.sendmsg([raw], [(socket.SOL_SOCKET, socket.SCM_RIGHTS,
array.array('i', [source.fileno(), out, err]))])
channel.sendall(raw[count:])
if disconnect:
require(os.read(cancel_read, 1) == b'x', 'fixture cancellation barrier')
channel.close(); os.write(report_write, b'cancelled')
else:
raw = b''
while len(raw) < 12:
chunk = channel.recv(12 - len(raw))
if not chunk:
break
raw += chunk
os.write(report_write, raw)
os._exit(0)
except (PermissionError, ConnectionResetError, BrokenPipeError):
if (uid, gid or uid) != (MANAGER, MANAGER):
os.write(report_write, b'REFUSED'); os._exit(0)
import traceback; traceback.print_exc(); os._exit(111)
except BaseException:
import traceback; traceback.print_exc(); os._exit(111)
os.close(report_write)
os.close(cancel_read)
if disconnect:
wait_for(lambda: (DATA / 'descendant.json').exists(), 5)
os.write(cancel_write, b'x')
os.close(cancel_write)
poll = select.poll(); handle = os.pidfd_open(pid); poll.register(handle, select.POLLIN)
require(poll.poll(50000), 'fixture client timeout')
_, status = os.waitpid(pid, 0); os.close(handle)
result = os.read(report_read, 128); os.close(report_read)
os.lseek(out, 0, 0); os.lseek(err, 0, 0)
output, error = os.read(out, 1024 * 1024), os.read(err, 1024 * 1024)
os.close(out); os.close(err)
require(os.waitstatus_to_exitcode(status) == 0, f'client failed: {error!r}')
if disconnect:
return result
return struct.unpack('!III', result)[1:] if len(result) == 12 else None, output, error
def command(argv):
out = os.memfd_create('static-proxy-output', os.MFD_CLOEXEC)
err = os.memfd_create('static-proxy-error', os.MFD_CLOEXEC)
try:
result = subprocess.run(['/opt/archipelago-runtime-proxy', '--', *argv], cwd='/', preexec_fn=lambda: drop(MANAGER),
stdin=subprocess.DEVNULL, stdout=out, stderr=err,
env={'PATH': '/usr/bin:/bin'}, timeout=45)
os.lseek(out, 0, 0); os.lseek(err, 0, 0)
output, error = os.read(out, 1024 * 1024), os.read(err, 1024 * 1024)
require(result.returncode == 0, f'{argv}: {result.returncode}: {error.decode(errors="replace")}')
return output.decode().strip()
finally:
os.close(out); os.close(err)
def asynchronous_command(source):
return subprocess.Popen(['/opt/archipelago-runtime-proxy', '--', '/usr/bin/python3', '-c', source],
cwd='/', preexec_fn=lambda: drop(MANAGER),
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
env={'PATH': '/usr/bin:/bin'})
def rendered_home_view(repo):
"""Exercise the rendered template's home/runtime binds over covered roots.
This checks kernel visibility/inodes with the actual bus/services below.
It is NOT a systemd SYSTEM-manager/drop-in dependency-order qualification;
that still requires a disposable boot of the rendered user@ composition.
"""
text = (repo / 'image-recipe/configs/80-archipelago-runtime-user.conf.in').read_text()
for key, value in {'ORIGINAL_RUNTIME_UID': str(UID), 'ORIGINAL_HOME': str(HOME),
'SANITIZED_RUNTIME_HOME': str(BACKING),
'MANAGER_BACKING_PARENT': '/var/lib/private-manager',
'SNAPSHOT_BACKING_PARENT': '/var/lib/private-snapshot',
'AUTHORITY_BACKING_PARENT': '/var/lib/private-authority'}.items():
text = text.replace('@' + key + '@', value)
require('ProtectHome=tmpfs' in text)
binds = []
for line in text.splitlines():
if line.startswith('BindPaths='):
source, _, target = line.split('=', 1)[1].partition(':')
binds.append((os.open(source, os.O_PATH | os.O_DIRECTORY | os.O_CLOEXEC), Path(target or source)))
try:
for target in ('/home', '/root', '/run/user'):
mount('tmpfs', target, 'tmpfs', 0, 'mode=755,size=4M')
for fd, target in binds:
target.mkdir(parents=True, exist_ok=True)
mount('/proc/self/fd/' + str(fd), target)
for target in ('/home', '/root', '/run/user'):
mount(None, target, flags=32 | 1) # readonly cover, writable sub-binds
require(RUN.stat().st_ino == next(os.fstat(fd).st_ino for fd, path in binds if path == RUN))
require(not Path('/run/user/61003').exists(), 'foreign user runtime exposed')
finally:
for fd, _ in binds:
os.close(fd)
def native_terminal_contract(identity):
binary = Path('/opt/runtime-terminal-tests')
if not binary.exists():
print('NOT RUN native handler/runtime workspace: supply --terminal-tests rebuilt test binary', flush=True)
return
require(not (DATA / 'native-terminal-pass').exists(), 'stale native terminal success marker')
work_identity = (BACKING / 'Work').stat()
original = [(path.stat().st_dev, path.stat().st_ino, path.read_bytes())
for path in (BACKING / 'Work/preserved', BACKING / 'Work/explicit ; literal/preserved')]
view = RuntimeLease(MANAGER, MANAGER, UID, UID, identity['configuration_sha256'])
child = os.fork()
if child == 0:
try:
require(LIBC.unshare(0x00020000) == 0, 'private terminal manager namespace unavailable')
mount(None, '/', flags=16384 | (1 << 18))
mount('tmpfs', '/home', 'tmpfs', 0, 'mode=755,size=4M')
mount('tmpfs', '/run', 'tmpfs', 0, 'mode=755,size=16M')
view.install(); view.close()
home = Path('/run/archipelago-c01/manager')
owned(home, MANAGER)
aliases = Path('/run/archipelago-c01/bin'); aliases.mkdir()
for name in ('sh', 'tmux'):
(aliases / name).symlink_to('/opt/archipelago-runtime-proxy')
drop(MANAGER)
os.chdir(home) # Handler commands must explicitly use runtime-visible /.
os.execve(binary, [str(binary), 'api::handler::terminal::runtime_workspace_tests::actual_runtime_workspace_contract',
'--exact', '--ignored', '--nocapture', '--test-threads=1'],
{'PATH': str(aliases), 'HOME': str(home), 'ARCHY_SESSION_STATE_DIR': str(home / 'sessions'),
'ARCHIPELAGO_RUNTIME_WORKSPACE': str(HOME / 'Work'), 'ARCHY_RUNTIME_TERMINAL_FIXTURE': '1'})
except BaseException:
import traceback; traceback.print_exc(); os._exit(111)
try:
poll = select.poll(); handle = os.pidfd_open(child); poll.register(handle, select.POLLIN)
reaped = False
try:
require(poll.poll(60000), 'native terminal contract deadline')
_, status = os.waitpid(child, 0)
reaped = True
require(os.waitstatus_to_exitcode(status) == 0, 'native terminal contract failed')
finally:
try:
if not reaped:
os.kill(child, signal.SIGKILL) # Still pinned by our unreaped child ownership.
require(poll.poll(5000), 'native terminal fixture retirement deadline')
os.waitpid(child, 0)
finally:
os.close(handle)
finally:
view.close()
after = [(path.stat().st_dev, path.stat().st_ino, path.read_bytes())
for path in (BACKING / 'Work/preserved', BACKING / 'Work/explicit ; literal/preserved')]
require(after == original, 'preserved workspace inode/content changed')
require((work_identity.st_dev, work_identity.st_ino) ==
((BACKING / 'Work').stat().st_dev, (BACKING / 'Work').stat().st_ino), 'preserved Work inode replaced')
require((DATA / 'native-terminal-pass').read_text() == 'passed', 'native test did not run to completion')
print('PASS native handler -> proxy -> runtime workspace -> persistent tmux; default/explicit pwd, preserved files, reconnect metadata', flush=True)
def main():
require(os.geteuid() == 0 and os.environ.get('ARCHY_RUNTIME_FIXTURE') == '1', 'isolated root fixture required')
rows = Path('/proc/self/mountinfo').read_text().splitlines()
for target in ('/run', '/home', '/var/lib'):
require(any(row.split()[4] == target and ' - tmpfs ' in row for row in rows), 'private fixture mount missing')
Path('/tmp').chmod(0o1777)
Path('/var/tmp').chmod(0o1777)
require('INTERP' not in subprocess.check_output(['/usr/bin/readelf', '-l', '/opt/archipelago-runtime-proxy'], text=True),
'existing proxy is not static')
root = current_group()
require('c01-runtime-fixture-' in str(root), 'owned transient unit required')
control = root / 'control'; control.mkdir(); (control / 'cgroup.procs').write_text(str(os.getpid()))
(root / 'cgroup.subtree_control').write_text('+cpu +memory +pids')
user_group = root / 'runtime-user.service'; user_group.mkdir()
for name in ('', 'cgroup.procs', 'cgroup.threads', 'cgroup.subtree_control'):
os.chown(user_group / name, UID, UID)
CODE.mkdir(); CODE.chmod(0o755)
repo = Path(__file__).resolve().parents[2]
subprocess.run(['/usr/bin/python3', '-B', '-m', 'unittest', 'discover', '-s', str(repo / 'scripts/tests'),
'-p', 'test*runtime*.py', '-v'], check=True, timeout=10)
for name in ('c01_runtime.py', 'c01_runtime_groups.py', 'runtime_owner.py'):
shutil.copyfile(repo / 'scripts' / name, CODE / name)
(CODE / name).chmod(0o644)
for path in (BACKING, RUN, DATA):
owned(path)
owned(Path('/run/user/61003'), OPERATOR)
for path in (BACKING / 'Work', BACKING / 'Work/explicit ; literal',
BACKING / 'Work/literal##dir', BACKING / 'Work/literal#{pane_id}###'):
owned(path)
(path / 'preserved').write_text('preserved-work-inode\n')
os.chown(path / 'preserved', UID, UID)
(BACKING / 'root-alias').symlink_to('/', target_is_directory=True)
HOME.mkdir(parents=True)
for path in (Path('/run/archipelago-runtime'), Path('/run/archipelago-managed'),
Path('/run/archipelago-c01'), Path('/run/systemd/system')):
path.mkdir(parents=True, exist_ok=True); path.chmod(0o755)
# Root-only backing parents retain synthetic old-UID private file ownership.
for name in ('manager', 'authority'):
parent = Path('/var/lib/private-' + name); parent.mkdir(mode=0o700)
(parent / 'secret').write_text('synthetic-private-state')
os.chown(parent / 'secret', UID, UID)
config_dir = Path('/run/runtime-etc'); config_dir.mkdir()
for name, content in {
'passwd': f'root:x:0:0:root:/root:/bin/sh\nruntime-fixture:x:{UID}:{UID}::/home/runtime-fixture:/usr/sbin/nologin\n',
'group': f'root:x:0:\nruntime-fixture:x:{UID}:\n',
'subuid': 'runtime-fixture:2000000:65536\n',
'subgid': 'runtime-fixture:2000000:65536\n',
}.items():
source = config_dir / name; source.write_text(content); source.chmod(0o644)
mount(source, '/etc/' + name)
# Minimal private /etc, synthesized rather than copying host configuration.
etc = Path('/run/etc-view'); etc.mkdir()
for name in ('passwd', 'group', 'subuid', 'subgid'):
shutil.copyfile(config_dir / name, etc / name)
(etc / 'machine-id').write_text('684f9c0509564804bc9ad0816083f71d\n')
(etc / 'nsswitch.conf').write_text('passwd: files\ngroup: files\nhosts: files\n')
(etc / 'hosts').write_text('127.0.0.1 localhost\n')
(etc / 'resolv.conf').touch()
(etc / 'os-release').symlink_to('/usr/lib/os-release')
(etc / 'dbus-1').mkdir()
# --session loads /usr/share/dbus-1/session.conf itself; that distro file
# optionally includes /etc/dbus-1/session.conf. Do not create a recursive alias.
(etc / 'dbus-1/session.d').mkdir()
(etc / 'containers').mkdir()
(etc / 'containers/policy.json').write_text('{"default":[{"type":"insecureAcceptAnything"}]}')
(etc / 'archipelago-c01').mkdir()
mount(etc, '/etc')
value = {'version': 1, 'runtime_user': 'runtime-fixture', 'runtime_uid': UID, 'runtime_gid': UID,
'manager_uid': MANAGER, 'manager_gid': MANAGER, 'authority_uid': AUTHORITY, 'authority_gid': AUTHORITY,
'operator_uid': OPERATOR, 'operator_gid': OPERATOR, 'home': str(HOME), 'home_backing': str(BACKING),
'runtime_dir': str(RUN), 'runtime_data_dir': str(DATA), 'graphroot': str(DATA / 'storage'),
'subuids': [[2000000, 65536]], 'subgids': [[2000000, 65536]],
'user_cgroup': str(user_group).removeprefix('/sys/fs/cgroup'),
'private_parents': ['/var/lib/private-manager', '/var/lib/private-authority']}
runtime.CONFIG = Path('/etc/archipelago-c01/runtime.json')
runtime.CONFIG.write_text(json.dumps(value)); runtime.CONFIG.chmod(0o600)
shutil.copyfile(repo / 'image-recipe/configs/archipelago-runtime-tmux.conf',
'/etc/archipelago-c01/runtime-tmux.conf')
containers = BACKING / '.config/containers'; containers.mkdir(parents=True)
(containers / 'storage.conf').write_text('[storage]\ndriver="vfs"\ngraphroot="' + str(DATA / 'storage') +
'"\nrunroot="' + str(RUN / 'containers') + '"\n')
for path in (BACKING / '.config', containers, containers / 'storage.conf'):
os.chown(path, UID, UID)
UNITS.mkdir(); UNITS.chmod(0o755)
for name in ('archipelago-runtime-worker.service', 'archipelago-runtime-tmux.service', 'archipelago-runtime-podman.service'):
text = (repo / 'image-recipe/configs' / name).read_text().replace('/usr/lib/archipelago/', str(CODE) + '/')
text = text.replace('[Service]', '[Service]\nStandardOutput=append:/var/lib/runtime-data/' + name + '.log\nStandardError=inherit')
(UNITS / name).write_text(text)
for name in ('basic', 'sockets', 'paths', 'timers', 'shutdown', 'exit'):
(UNITS / (name + '.target')).write_text('[Unit]\nDescription=Private fixture ' + name + '\nDefaultDependencies=no\n')
(UNITS / 'default.target').write_text('[Unit]\nRequires=dbus.socket archipelago-runtime-tmux.service\n')
# User bus: distro dbus-daemon with the distro session policy, private socket.
(UNITS / 'dbus.socket').write_text('[Socket]\nListenStream=%t/bus\nSocketMode=0600\n')
(UNITS / 'dbus.service').write_text('[Unit]\nRequires=dbus.socket\n[Service]\nType=notify\n'
'StandardOutput=append:/var/lib/runtime-data/dbus.log\nStandardError=inherit\n'
'ExecStart=/usr/bin/dbus-daemon --session --address=systemd: --nofork --nopidfile --systemd-activation\n')
log = open('/run/fixture-service.log', 'wb', buffering=0)
# Actual dependency refusal, not a saved qualification flag or mocked answer.
refused = subprocess.run(['/usr/bin/python3', '-I', '-S', str(CODE / 'c01_runtime.py'), 'broker'],
stdout=log, stderr=log, env={'PATH': '/usr/bin:/bin'}, timeout=5)
require(refused.returncode != 0 and not (runtime.PUBLIC / 'runtime.sock').exists(),
'missing access owner published a command listener')
def rejects():
try:
runtime.load_config()
except (ValueError, KeyError):
return
raise AssertionError('invalid runtime prerequisite accepted')
original = runtime.CONFIG.read_text()
for key, wrong in (('runtime_uid', MANAGER), ('subuids', [[2000001, 65536]]), ('graphroot', '/var/lib/private-manager/storage')):
runtime.CONFIG.write_text(json.dumps({**value, key: wrong})); rejects()
runtime.CONFIG.write_text(original)
group_file = Path('/etc/group'); group_original = group_file.read_text()
group_file.write_text(group_original + 'sudo:x:27:runtime-fixture\n'); rejects(); group_file.write_text(group_original)
(BACKING / '.ssh').mkdir(); rejects(); (BACKING / '.ssh').rmdir()
runtime.load_config()
print('PASS fail-closed missing live access owner, overlapping UID/private root, changed subuids, supplementary privilege, credential home', flush=True)
access = runtime.listen(runtime.ACCESS, 0, socket.SOCK_SEQPACKET)
broker = subprocess.Popen(['/usr/bin/python3', '-I', '-S', str(CODE / 'c01_runtime.py'), 'broker'],
stdout=log, stderr=log, env={'PATH': '/usr/bin:/bin'})
require(select.select([access], [], [], 5)[0], 'broker never requested live lease')
lease, _ = access.accept(); lease.settimeout(2)
request, _ = runtime.receive(lease); runtime.send(lease, request)
wait_for(lambda: (runtime.BASE / 'grant.sock').exists())
user = os.fork()
if user == 0:
try:
(user_group / 'cgroup.procs').write_text(str(os.getpid()))
require(LIBC.unshare(0x00020000) == 0, 'user private view unavailable')
mount(None, '/', flags=16384 | (1 << 18))
rendered_home_view(repo)
for fd in (1, 2):
os.dup2(log.fileno(), fd)
drop(UID)
os.chdir(HOME)
os.execve('/usr/lib/systemd/systemd', ['systemd', '--user'],
{'HOME': str(HOME), 'USER': 'runtime-fixture', 'LOGNAME': 'runtime-fixture',
'PATH': '/usr/bin:/bin', 'XDG_RUNTIME_DIR': str(RUN), 'DBUS_SESSION_BUS_ADDRESS': 'unix:path=' + str(RUN / 'bus'),
'SYSTEMD_UNIT_PATH': str(UNITS), 'SYSTEMD_LOG_TARGET': 'console'})
except BaseException:
import traceback; traceback.print_exc(); os._exit(111)
watch = None
try:
user_environment = {'HOME': str(HOME), 'XDG_RUNTIME_DIR': str(RUN),
'DBUS_SESSION_BUS_ADDRESS': 'unix:path=' + str(RUN / 'bus'), 'PATH': '/usr/bin:/bin'}
def user_control(*args):
return subprocess.run(['/usr/bin/systemctl', '--user', *args], env=user_environment,
preexec_fn=lambda: drop(UID), capture_output=True, timeout=10)
wait_for(lambda: (RUN / 'systemd/private').exists())
bus = user_control('start', 'dbus.service')
require(bus.returncode == 0, 'bus preflight: ' + bus.stderr.decode())
bus = subprocess.run(['/usr/bin/busctl', '--user', '--no-pager', 'list'], env=user_environment,
preexec_fn=lambda: drop(UID), capture_output=True, timeout=10)
require(bus.returncode == 0 and b'org.freedesktop.systemd1' in bus.stdout,
'user bus systemd name unavailable: ' + bus.stderr.decode())
check = user_control('is-active', 'archipelago-runtime-tmux.service')
require(check.returncode == 0, 'tmux preflight: ' + check.stdout.decode() + check.stderr.decode())
wait_for(lambda: (Path('/tmp') / f'tmux-{UID}' / 'default').is_socket(), 5)
print('PASS staged private user-manager/tmux startup preflight', flush=True)
podman = user_control('--no-block', 'start', 'archipelago-runtime-podman.service')
require(podman.returncode == 0, podman.stderr.decode())
wait_for(lambda: user_control('is-active', 'archipelago-runtime-podman.service').returncode == 0, 35)
print('PASS independent USER-unit Podman bootstrap preflight', flush=True)
started = user_control('--no-block', 'start', 'archipelago-runtime-worker.service')
require(started.returncode == 0, started.stderr.decode())
def ready():
nonlocal watch
require(broker.poll() is None, 'broker exited: ' + Path('/run/fixture-service.log').read_text())
channel = runtime.root_connect(runtime.PUBLIC / 'readiness.sock')
try:
result, rights = runtime.receive(channel, 3)
require(result['version'] == 2, 'runtime detached-mount ABI mismatch')
for fd in rights:
os.close(fd)
watch = channel
return result
except (ValueError, ConnectionResetError):
channel.close(); return None
identity = wait_for(ready, 40)
print('PASS live root broker/user-worker readiness:', json.dumps(identity), flush=True)
info = json.loads(command(['/usr/bin/python3', '-c',
'import os,json;print(json.dumps([os.getuid(),os.getgid(),os.getgroups(),os.environ["HOME"],os.environ["XDG_RUNTIME_DIR"]]))']))
require(info == [UID, UID, [], str(HOME), str(RUN)], str(info))
runtime_meta = json.loads(command(['/usr/bin/python3', '-c',
'import os,json;from pathlib import Path;p=Path(os.environ["XDG_RUNTIME_DIR"]);s=p.stat();'
'assert not Path("/run/user/61003").exists();print(json.dumps([s.st_dev,s.st_ino,s.st_uid,s.st_mode & 511]))']))
meta = RUN.stat()
require(runtime_meta == [meta.st_dev, meta.st_ino, UID, 0o700], 'runtime directory identity changed under ProtectHome cover')
print('PASS rendered home/runtime bindings over /home /root /run/user covers preserve exact runtime inode, owner, mode and real bus', flush=True)
require(client(['/usr/bin/true'], uid=MANAGER, gid=AUTHORITY)[0] is None, 'foreign GID accepted')
require(client(['/usr/bin/true'], uid=AUTHORITY, gid=MANAGER)[0] is None, 'foreign UID accepted')
command(['/usr/bin/python3', '-c', 'from pathlib import Path\n'
'for p in ["/var/lib/private-manager/secret","/var/lib/private-authority/secret"]:\n'
' try:Path(p).read_text()\n except PermissionError:continue\n raise SystemExit(1)'])
print('PASS actual permanent identity, exact peer UID/GID, private-state denial', flush=True)
fds = len(list(Path('/proc/self/fd').iterdir()))
try:
RuntimeLease(MANAGER, MANAGER, UID, UID, '0' * 64)
except ValueError:
pass
else:
raise AssertionError('stale configuration accepted')
require(len(list(Path('/proc/self/fd').iterdir())) == fds, 'refused readiness leaked capabilities')
view = RuntimeLease(MANAGER, MANAGER, UID, UID, identity['configuration_sha256'])
child = os.fork()
if child == 0:
try:
require(LIBC.unshare(0x00020000) == 0, 'private manager view unavailable')
mount(None, '/', flags=16384 | (1 << 18))
mount('tmpfs', '/run', 'tmpfs', 0, 'mode=755,size=16M')
view.install()
require(command(['/usr/bin/id', '-u']) == str(UID), 'mounted static proxy delegation failed')
os._exit(0)
except BaseException:
import traceback; traceback.print_exc(); os._exit(111)
_, status = os.waitpid(child, 0)
require(os.waitstatus_to_exitcode(status) == 0, 'detached socket view failed')
view.close()
print('PASS exact socket capability survives covered /run via detached mount; actual static proxy executes', flush=True)
escaped = ('import os,time,json\nfrom pathlib import Path\n'
'p=os.fork()\n'
'if p==0:\n os.setsid();Path("/var/lib/runtime-data/descendant.json").write_text(json.dumps([os.getpid()]));time.sleep(120)\n'
'else:time.sleep(120)')
require(client(['/usr/bin/python3', '-c', escaped], disconnect=True) == b'cancelled')
descendant = json.loads((DATA / 'descendant.json').read_text())[0]
wait_for(lambda: not Path(f'/proc/{descendant}').exists(), 5)
wait_for(lambda: not list(Path(identity['cgroup']).parent.glob('job-*')), 5)
print('PASS disconnected job retires escaped setsid descendant and cgroup', flush=True)
(DATA / 'descendant.json').unlink()
command(['/usr/bin/python3', '-c',
'import os,time,json\nfrom pathlib import Path\nr,w=os.pipe();p=os.fork()\n'
'if p==0:\n os.close(r);os.setsid();Path("/var/lib/runtime-data/descendant.json").write_text(json.dumps([os.getpid()]));os.write(w,b"x");time.sleep(120)\n'
'else:\n os.close(w);os.read(r,1);os._exit(0)'])
descendant = json.loads((DATA / 'descendant.json').read_text())[0]
wait_for(lambda: not Path(f'/proc/{descendant}').exists(), 5)
wait_for(lambda: not list(Path(identity['cgroup']).parent.glob('job-*')), 5)
print('PASS normal leader exit also retires escaped descendant before completion', flush=True)
# Keep one leader alive while it repeatedly double-forks exited helpers.
# Each wave must disappear BEFORE releasing the leader; unrelated work
# must still be admitted. A killed leader cannot make this case pass.
producer = asynchronous_command(
'import os,time,json\nfrom pathlib import Path\n'
'root=Path("/var/lib/runtime-data")\n'
'root.joinpath("producer-pid").write_text(str(os.getpid()))\n'
'for wave in range(4):\n'
' pids=[]\n'
' for i in range(32):\n'
' r,w=os.pipe(); helper=os.fork()\n'
' if helper==0:\n'
' os.close(r); orphan=os.fork()\n'
' if orphan==0: os.setsid();os._exit(0)\n'
' os.write(w,str(orphan).encode());os._exit(0)\n'
' os.close(w);pids.append(int(os.read(r,32)));os.close(r);os.waitpid(helper,0)\n'
' tmp=root/"wave-pending";tmp.write_text(json.dumps(pids));tmp.rename(root/("wave-"+str(wave)))\n'
' while not root.joinpath("release-"+str(wave)).exists(): time.sleep(.02)\n'
'while not root.joinpath("producer-done").exists(): time.sleep(.02)')
try:
for wave in range(4):
path = DATA / ('wave-' + str(wave))
wait_for(path.exists, 5)
pids = json.loads(path.read_text())
wait_for(lambda: all(not Path(f'/proc/{pid}').exists() for pid in pids), 5)
require(producer.poll() is None, 'producer exited before live-leader reaping assertion')
require(Path('/proc/' + (DATA / 'producer-pid').read_text()).exists())
command(['/usr/bin/true'])
(DATA / ('release-' + str(wave))).touch()
# Nested empty cgroups must retire without cancelling that leader.
command(['/usr/bin/python3', '-c',
'from pathlib import Path\n'
'root=Path("/sys/fs/cgroup")/Path("/proc/self/cgroup").read_text().strip()[4:]\n'
'for i in range(80): (root/str(i)/"nested").mkdir(parents=True)'])
require(producer.poll() is None, 'nested retirement cancelled unrelated producer')
# A genuinely unreadable descendant retains only its own slot until
# the fixture repairs permissions; successful response is withheld.
blocked = asynchronous_command(
'from pathlib import Path\n'
'root=Path("/sys/fs/cgroup")/Path("/proc/self/cgroup").read_text().strip()[4:]\n'
'child=root/"unreadable";child.mkdir();child.chmod(0)\n'
'tmp=Path("/var/lib/runtime-data/blocked-pending");tmp.write_text(str(child));tmp.rename(tmp.with_name("blocked-group"))')
locked = None
try:
wait_for(lambda: (DATA / 'blocked-group').exists(), 5)
locked = Path((DATA / 'blocked-group').read_text())
time.sleep(.2)
require(blocked.poll() is None and locked.exists(), 'failed cleanup released ownership')
command(['/usr/bin/true'])
require(producer.poll() is None, 'cleanup failure cancelled unrelated producer')
locked.chmod(0o755)
require(blocked.wait(timeout=10) == 0, 'retained cleanup did not recover')
require(not locked.parent.exists(), 'nested invocation root remained')
finally:
if locked is not None and locked.exists():
locked.chmod(0o755)
if blocked.poll() is None:
blocked.terminate(); blocked.wait(timeout=5)
(DATA / 'producer-done').touch()
require(producer.wait(timeout=10) == 0)
wait_for(lambda: not list(Path(identity['cgroup']).parent.glob('job-*')), 5)
finally:
if producer.poll() is None:
producer.terminate(); producer.wait(timeout=5)
print('PASS live-leader orphan waves, nested empty cgroups, retained unreadable cleanup and concurrent admission', flush=True)
native_terminal_contract(identity)
command(['/usr/bin/tmux', 'new-session', '-d', '-s', 'fixture', '/bin/sh'])
command(['/usr/bin/tmux', 'send-keys', '-t', 'fixture', 'printf "runtime-terminal-ok\\n"', 'Enter'])
command(['/usr/bin/tmux', 'resize-window', '-t', 'fixture', '-x', '90', '-y', '30'])
wait_for(lambda: 'runtime-terminal-ok' in command(['/usr/bin/tmux', 'capture-pane', '-p', '-t', 'fixture']))
print('PASS actual persistent tmux new-session/send-keys/resize/capture', flush=True)
command(['/usr/bin/python3', '-c', 'import tempfile\n'
'for p in ["/tmp","/var/tmp"]:\n'
' with tempfile.TemporaryFile(dir=p) as f:f.write(b"private-temp-probe")'])
command(['/usr/bin/podman', 'load', '-i', '/run/public.oci.tar'])
image = command(['/usr/bin/podman', 'images', '--format', '{{.ID}}']).splitlines()[0]
cid = command(['/usr/bin/podman', 'run', '-d', '--name', 'runtime-fixture', '--network=none',
'--entrypoint=/bin/sh', image, '-c', 'while :; do sleep 1; done'])
require(command(['/usr/bin/podman', 'exec', cid, '/bin/sh', '-c', 'printf actual-podman-exec']) == 'actual-podman-exec')
before = command(['/usr/bin/podman', 'inspect', '--format', '{{.Id}} {{.State.Pid}} {{.State.StartedAt}}', cid])
# Real manager surrogate service, separate from all runtime user groups.
manager_group = root / 'manager-surrogate'; manager_group.mkdir()
for _ in range(2):
manager = os.fork()
if manager == 0:
(manager_group / 'cgroup.procs').write_text(str(os.getpid()))
drop(MANAGER); time.sleep(60); os._exit(0)
wait_for(lambda: (manager_group / 'cgroup.procs').read_text().strip())
(manager_group / 'cgroup.kill').write_text('1'); os.waitpid(manager, 0)
manager_group.rmdir()
require(command(['/usr/bin/podman', 'inspect', '--format', '{{.Id}} {{.State.Pid}} {{.State.StartedAt}}', cid]) == before)
require(command(['/usr/bin/tmux', 'has-session', '-t', 'fixture']) == '')
require(command(['/usr/bin/podman', 'exec', cid, '/bin/true']) == '')
print('PASS actual Podman USER-unit load/run/exec; container and tmux survive manager cgroup stop', flush=True)
live = RuntimeLease(MANAGER, MANAGER, UID, UID, identity['configuration_sha256'])
stop_case = os.environ.get('ARCHY_RUNTIME_STOP', 'revoke')
if stop_case == 'stall':
os.kill(broker.pid, signal.SIGSTOP)
time.sleep(3.3)
try:
live.check()
except ValueError:
pass
else:
raise AssertionError('stalled root observer retained readiness')
finally:
os.kill(broker.pid, signal.SIGCONT)
else:
lease.close()
wait_for(lambda: broker.poll() is not None, 5)
def revoked():
for _ in range(128):
if not select.select([watch], [], [], 0)[0]:
return False
if watch.recv(16384) == b'':
return True
return False
wait_for(revoked, 5)
try:
live.check()
except ValueError:
pass
else:
raise AssertionError('revoked root lease remained valid')
live.close()
wait_for(lambda: not Path(f'/proc/{identity["pid"]}').exists(), 5)
def persistent_command(argv):
# Actual transient USER unit; never system-unit --uid Podman exec.
result = subprocess.run(['/usr/bin/systemd-run', '--user', '--quiet', '--wait', '--pipe', '--collect', *argv],
env=user_environment, preexec_fn=lambda: drop(UID), capture_output=True, timeout=15)
require(result.returncode == 0, result.stderr.decode())
return result.stdout.decode().strip()
require(persistent_command(['/usr/bin/podman', 'inspect', '--format', '{{.Id}} {{.State.Pid}} {{.State.StartedAt}}', cid]) == before)
persistent_command(['/usr/bin/podman', 'exec', cid, '/bin/true'])
persistent_command(['/usr/bin/tmux', 'has-session', '-t', 'fixture'])
persistent_command(['/usr/bin/podman', 'rm', '-f', cid])
persistent_command(['/usr/bin/tmux', 'kill-session', '-t', 'fixture'])
print('PASS ' + stop_case + ' revokes broker/readiness/worker; persistent container and tmux survive worker exit too', flush=True)
finally:
for path in DATA.glob('*.log'):
print(path.name + ':', path.read_text(), flush=True)
for path in user_group.rglob('cgroup.procs'):
for pid in path.read_text().split():
try:
print('OWNED TASK', pid, path, Path('/proc/' + pid + '/cmdline').read_bytes(),
Path('/proc/' + pid + '/wchan').read_text(), flush=True)
except FileNotFoundError:
pass
if watch is not None:
watch.close()
if broker.poll() is None:
broker.terminate(); broker.wait(timeout=5)
# Fixture-owned hierarchy only, AFTER workload lifecycle assertions.
(user_group / 'cgroup.kill').write_text('1')
os.waitpid(user, 0)
print('SERVICE LOG:', Path('/run/fixture-service.log').read_text(), flush=True)
if __name__ == '__main__':
main()