Files
archy/tests/regression/radio-owner-retirement.py

83 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""PTY-only abrupt-owner death and exact cgroup recovery, in disposable PID1."""
import fcntl
import json
import os
from pathlib import Path
import select
import signal
import socket
import subprocess
import sys
import tempfile
import termios
import time
assert os.geteuid()==0 and os.environ.get('ARCHY_RADIO_FIXTURE')=='1'
sentinel=Path('/tmp/radio-custody-sentinel');sentinel.write_bytes(b'private-fixture-preserved');sentinel.chmod(0o600)
repo=Path(__file__).resolve().parents[2];sys.path.insert(0,str(repo/'scripts'))
import c01_radio_owner as radio
import c01_runtime
base=Path('/sys/fs/cgroup')/Path('/proc/self/cgroup').read_text().strip().removeprefix('0::/')
group=base/'archipelago-runtime-worker.service';group.mkdir(mode=0o700)
directory=Path('/run/archipelago-radio');directory.mkdir(mode=0o700)
radio.REGISTRY=directory/'job.json'
c01_runtime.load_config=lambda: ({'user_cgroup':str(base).removeprefix('/sys/fs/cgroup')},'fixture-only')
with tempfile.TemporaryDirectory() as temporary:
root=Path(temporary);root.chmod(0o755)
home=root/'home';home.mkdir(mode=0o700);os.chown(home,53002,53002)
helper=root/'helper'
subprocess.run(['/usr/bin/cc',str(repo/'scripts/tests/radio_helper_fixture.c'),'-o',str(helper)],check=True)
tool=os.open(helper,os.O_RDONLY|os.O_CLOEXEC)
homefd=os.open(home,os.O_RDONLY|os.O_DIRECTORY|os.O_CLOEXEC)
master,slave=os.openpty();os.set_blocking(master,False)
path=os.ttyname(slave);os.chmod(path,0o666);fcntl.ioctl(slave,termios.TIOCEXCL)
ready_r,ready_w=os.pipe()
owner=os.fork()
if owner==0:
os.close(master);os.close(ready_r)
job=radio.Job(group,['/usr/local/bin/archy-rnodeconf','--archy-owned','install',path,'heltecv3','f'*32],
{3:slave,4:homefd},{'HOME':'/proc/self/fd/4','PATH':'/usr/bin:/bin'},53002,53002,tool,
admission_end=radio.now()+1_500_000_000)
end=time.monotonic()+3
while b'descendant=' not in job.output:
assert not job.tick() and time.monotonic()<end;time.sleep(.01)
os.write(ready_w,b'!')
while True:time.sleep(1)
os.close(slave);os.close(ready_w)
try:
assert select.select([ready_r],[],[],4)[0] and os.read(ready_r,1)==b'!'
endpoint=socket.socket(socket.AF_UNIX,socket.SOCK_SEQPACKET)
endpoint.bind(str(directory/'control.sock'));endpoint.listen(1)
try:
try:radio.retire_main()
except ValueError:pass
else:raise AssertionError('live owner retirement accepted')
finally:endpoint.close();(directory/'control.sock').unlink()
os.kill(owner,signal.SIGKILL);os.waitpid(owner,0)
record=json.loads(radio.REGISTRY.read_text())
changed={**record,'identity':[record['identity'][0],record['identity'][1]+1]}
radio.REGISTRY.write_text(json.dumps(changed))
try:radio.retire_main()
except ValueError:pass
else:raise AssertionError('foreign cgroup capability accepted')
radio.REGISTRY.write_text(json.dumps(record))
radio.retire_main()
assert not radio.REGISTRY.exists()
assert not any(p.is_dir() for p in group.iterdir()), 'writer group survived retirement'
time.sleep(2.1)
try:data=os.read(master,4096)
except OSError:data=b''
assert b'ORPHAN' not in data
assert sentinel.read_bytes()==b'private-fixture-preserved' and not list(radio.WORK.iterdir())
print('PASS abrupt owner SIGKILL: recorded lifetime identity rejects substitution; fixed retirement kills detached writer before reuse',flush=True)
finally:
os.close(ready_r);os.close(master);os.close(homefd);os.close(tool)
while True:
try:
pid,_=os.waitpid(-1,os.WNOHANG)
if not pid:break
except ChildProcessError:break
group.rmdir()