83 lines
3.8 KiB
Python
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()
|