Improve base guidance and qualify bounded aircraft route and hidden test lifecycle
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Narrow post-run cleanup of adopted device helpers in the one disposable prefix.
|
||||
|
||||
Library only: importing or executing this file sends no signals. Call only while
|
||||
holding .erri-workbench.lock, after normal wineserver/Gamescope shutdown. This is
|
||||
not a general Wine cleanup tool. Environment values are never returned/logged.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
import os
|
||||
import posixpath
|
||||
import select
|
||||
import signal
|
||||
import stat
|
||||
import time
|
||||
|
||||
COMPAT = Path('/tmp/erri-headless-compat')
|
||||
INSTALLED_PROTON = Path.home()/'.local/share/Steam/steamapps/common/Proton - Experimental/proton'
|
||||
PREFIX = b'/tmp/erri-headless-compat/pfx'
|
||||
DEVICE_ARGV = (b'C:\\windows\\system32\\winedevice.exe',)
|
||||
SYSTEMD_ARGV = (b'/usr/lib/systemd/systemd', b'--user')
|
||||
|
||||
|
||||
class CleanupRefused(ValueError):
|
||||
def __init__(self, message, report=None):
|
||||
super().__init__(message)
|
||||
self.report = report or {}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Process:
|
||||
pid: int
|
||||
uid: int
|
||||
ppid: int
|
||||
start: int
|
||||
state: str
|
||||
comm: str
|
||||
argv: tuple
|
||||
exe: str | None
|
||||
executable_id: tuple | None
|
||||
prefix: bytes | None = None
|
||||
|
||||
def identity(self):
|
||||
# State may legitimately change between running and sleeping.
|
||||
return (self.pid, self.uid, self.ppid, self.start, self.comm, self.argv,
|
||||
self.exe, self.executable_id)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Device:
|
||||
process: Process
|
||||
parent: Process
|
||||
|
||||
def identity(self):
|
||||
return self.process.identity(), self.parent.identity()
|
||||
|
||||
def public(self):
|
||||
p = self.process
|
||||
return {'pid': p.pid, 'ppid': p.ppid, 'state': p.state,
|
||||
'exebasename': 'wine64-preloader', 'start_ticks': p.start,
|
||||
'role': 'adopted-winedevice'}
|
||||
|
||||
|
||||
def prefix_kind(value):
|
||||
if value is None:
|
||||
return 'other'
|
||||
if value.rstrip(b'/') == PREFIX:
|
||||
return 'exact'
|
||||
# Never resolve untrusted environment paths or follow their symlinks.
|
||||
# A lexical spelling of this prefix is ambiguous and cannot be killed.
|
||||
if posixpath.normpath(value) == PREFIX:
|
||||
return 'alias'
|
||||
return 'other'
|
||||
|
||||
|
||||
def native_metadata_proves_unrelated(p):
|
||||
names = [p.comm.lower(), *(os.fsdecode(a).replace('\\', '/').rsplit('/', 1)[-1].lower() for a in p.argv)]
|
||||
if p.exe:
|
||||
names.append(Path(p.exe).name.lower())
|
||||
if any(n.endswith('.exe') or n.startswith(('wine', 'proton', 'gamescope', 'armareforger')) or n in {'reaper', 'pressure-vessel-wrap', 'pv-bwrap'} for n in names):
|
||||
return False
|
||||
if p.exe and p.exe.startswith('/'):
|
||||
return True
|
||||
if p.comm == 'systemd' and p.argv == SYSTEMD_ARGV and p.ppid == 1:
|
||||
return True
|
||||
if p.comm == '(sd-pam)' and p.argv == (b'(sd-pam)',):
|
||||
return True
|
||||
# Same narrow native foreground signature as headless_game's preflight.
|
||||
if p.comm == 'apache2' and p.argv == (b'apache2', b'-DFOREGROUND'):
|
||||
return True
|
||||
# Nondumpable desktop services still expose argv and kernel comm. Match
|
||||
# both (Linux comm truncates at 15 bytes), after refusing runtime markers
|
||||
# in every argument above. This does not authorize any signal: it only
|
||||
# skips an unreadable environment for positively native metadata.
|
||||
if p.argv and p.argv[0].startswith(b'/'):
|
||||
basename = os.fsdecode(p.argv[0]).rsplit('/', 1)[-1]
|
||||
if p.comm and p.comm in {basename, basename[:15]}:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def prove_device(p, parent, uid, expected_exe, executable_id):
|
||||
if (p.uid != uid or p.state in {'Z', 'X'} or prefix_kind(p.prefix) != 'exact'
|
||||
or p.comm != 'winedevice.exe' or p.argv != DEVICE_ARGV
|
||||
or p.exe != expected_exe or p.executable_id != executable_id):
|
||||
raise CleanupRefused(f'PID {p.pid} is not the exact adopted device helper; left untouched')
|
||||
if (parent.pid != p.ppid or parent.uid != uid or parent.ppid != 1
|
||||
or parent.state in {'Z', 'X'} or parent.comm != 'systemd'
|
||||
or parent.argv != SYSTEMD_ARGV
|
||||
or parent.exe not in {None, '/usr/lib/systemd/systemd'}):
|
||||
raise CleanupRefused(f'PID {p.pid} does not have the proven user-systemd parent; left untouched')
|
||||
# The observed user-systemd is nondumpable: an unreadable exe is accepted
|
||||
# only with all exact parent metadata above. A readable wrong exe is refused.
|
||||
return Device(p, parent)
|
||||
|
||||
|
||||
class ProcFS:
|
||||
def __init__(self, expected_exe, executable_id, root=Path('/proc'), uid=None, self_pid=None):
|
||||
self.root = root
|
||||
self.uid = os.getuid() if uid is None else uid
|
||||
self.self_pid = os.getpid() if self_pid is None else self_pid
|
||||
self.expected_exe = str(expected_exe)
|
||||
self.executable_id = executable_id
|
||||
|
||||
def metadata(self, pid):
|
||||
p = self.root / str(pid)
|
||||
uid = p.stat().st_uid
|
||||
text = (p/'stat').read_text()
|
||||
fields = text[text.rfind(')') + 2:].split()
|
||||
state, ppid, start = fields[0], int(fields[1]), int(fields[19])
|
||||
comm = (p/'comm').read_text().strip()
|
||||
argv = tuple(a for a in (p/'cmdline').read_bytes().split(b'\0') if a)
|
||||
try:
|
||||
exe = os.readlink(p/'exe')
|
||||
info = (p/'exe').stat()
|
||||
executable_id = (info.st_dev, info.st_ino)
|
||||
except PermissionError:
|
||||
exe, executable_id = None, None
|
||||
# Detect PID reuse while reading the separate proc records.
|
||||
final = (p/'stat').read_text()
|
||||
after = final[final.rfind(')') + 2:].split()
|
||||
if int(after[19]) != start or int(after[1]) != ppid:
|
||||
raise CleanupRefused(f'PID {pid} changed while reading metadata; left untouched')
|
||||
return Process(pid, uid, ppid, start, state, comm, argv, exe, executable_id)
|
||||
|
||||
def devices(self):
|
||||
found = []
|
||||
for entry in self.root.iterdir():
|
||||
if not entry.name.isdigit() or int(entry.name) == self.self_pid:
|
||||
continue # library caller is this process, never another PID
|
||||
pid = int(entry.name)
|
||||
try:
|
||||
if entry.stat().st_uid != self.uid:
|
||||
continue
|
||||
p = self.metadata(pid)
|
||||
if p.state in {'Z', 'X'}:
|
||||
continue # already exited, not a signal target
|
||||
try:
|
||||
values = [v[11:] for v in (entry/'environ').read_bytes().split(b'\0') if v.startswith(b'WINEPREFIX=')]
|
||||
except PermissionError:
|
||||
if native_metadata_proves_unrelated(p):
|
||||
continue
|
||||
raise CleanupRefused(f'Cannot establish prefix ownership for PID {pid}; left untouched')
|
||||
kinds = [prefix_kind(value) for value in values]
|
||||
if not any(k in {'exact', 'alias'} for k in kinds):
|
||||
continue
|
||||
if len(values) != 1 or kinds[0] != 'exact':
|
||||
raise CleanupRefused(f'PID {pid} has ambiguous private-prefix spelling; left untouched')
|
||||
again = self.metadata(pid)
|
||||
if again.identity() != p.identity():
|
||||
raise CleanupRefused(f'PID {pid} changed during prefix inspection; left untouched')
|
||||
p = Process(**{**p.__dict__, 'prefix': values[0]})
|
||||
parent = self.metadata(p.ppid)
|
||||
found.append(prove_device(p, parent, self.uid, self.expected_exe, self.executable_id))
|
||||
except (FileNotFoundError, ProcessLookupError):
|
||||
continue
|
||||
except (PermissionError, OSError) as error:
|
||||
raise CleanupRefused(f'Cannot verify private-prefix PID {pid}: {type(error).__name__}; left untouched') from None
|
||||
return sorted(found, key=lambda d: d.process.pid)
|
||||
|
||||
|
||||
class Pidfds:
|
||||
def open(self, pid):
|
||||
return os.pidfd_open(pid)
|
||||
|
||||
def close(self, fd):
|
||||
os.close(fd)
|
||||
|
||||
def send(self, fd, sig):
|
||||
signal.pidfd_send_signal(fd, sig)
|
||||
|
||||
def wait(self, handles, timeout):
|
||||
poller = select.poll()
|
||||
by_fd = {fd: pid for pid, fd in handles.items()}
|
||||
for fd in by_fd:
|
||||
poller.register(fd, select.POLLIN)
|
||||
finished = set()
|
||||
until = time.monotonic() + timeout
|
||||
while len(finished) < len(handles):
|
||||
remaining = max(0, until-time.monotonic())
|
||||
events = poller.poll(int(remaining*1000))
|
||||
for fd, flags in events:
|
||||
if flags & (select.POLLIN | select.POLLHUP):
|
||||
finished.add(by_fd[fd])
|
||||
poller.unregister(fd)
|
||||
elif flags & (select.POLLERR | select.POLLNVAL):
|
||||
raise CleanupRefused('A pinned process handle became unverifiable')
|
||||
if remaining <= 0 or time.monotonic() >= until:
|
||||
break
|
||||
return finished
|
||||
|
||||
|
||||
def _cleanup(inspector, signals):
|
||||
"""Testable algorithm; production entry below always supplies real guards."""
|
||||
initial = {d.process.pid: d for d in inspector.devices()}
|
||||
report = {'scope': str(COMPAT/'pfx'), 'candidates': [d.public() for d in initial.values()],
|
||||
'term_sent': [], 'kill_sent': [], 'cleared': False}
|
||||
handles = {}
|
||||
|
||||
def census():
|
||||
current = {d.process.pid: d for d in inspector.devices()}
|
||||
for pid, candidate in current.items():
|
||||
if pid not in initial or candidate.identity() != initial[pid].identity():
|
||||
raise CleanupRefused(f'Private-prefix process identity changed (PID {pid}); remaining processes left untouched')
|
||||
return current
|
||||
|
||||
try:
|
||||
# Open every handle only after the complete prefix census proves there
|
||||
# are no active/unknown processes. Before signalling, verify it again.
|
||||
for pid in initial:
|
||||
try:
|
||||
handles[pid] = signals.open(pid)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
for pid, fd in handles.items():
|
||||
current = census()
|
||||
if pid in signals.wait({pid: fd}, 0):
|
||||
continue
|
||||
if pid not in current:
|
||||
raise CleanupRefused(f'Pinned PID {pid} lost its proven prefix identity; left untouched')
|
||||
try:
|
||||
signals.send(fd, signal.SIGTERM)
|
||||
report['term_sent'].append(pid)
|
||||
except ProcessLookupError:
|
||||
pass # pinned original exited, never retry via its numeric PID
|
||||
exited = signals.wait(handles, 3.0)
|
||||
for pid, fd in handles.items():
|
||||
if pid in exited:
|
||||
continue
|
||||
current = census()
|
||||
if pid in signals.wait({pid: fd}, 0):
|
||||
continue
|
||||
if pid not in current:
|
||||
raise CleanupRefused(f'Pinned PID {pid} lost its proven prefix identity; left untouched')
|
||||
try:
|
||||
signals.send(fd, signal.SIGKILL)
|
||||
report['kill_sent'].append(pid)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
signals.wait(handles, 1.0)
|
||||
if census():
|
||||
raise CleanupRefused('Some pinned private-prefix helpers remain alive; no broader cleanup attempted')
|
||||
report['cleared'] = True
|
||||
return report
|
||||
except (CleanupRefused, OSError) as error:
|
||||
raise CleanupRefused(str(error), report) from None
|
||||
finally:
|
||||
for fd in handles.values():
|
||||
signals.close(fd)
|
||||
|
||||
|
||||
def _inspector(compat, proton):
|
||||
compat, proton = Path(compat), Path(proton)
|
||||
if compat != COMPAT or compat.resolve() != COMPAT or (compat/'pfx').resolve() != COMPAT/'pfx':
|
||||
raise CleanupRefused('Cleanup is restricted to the canonical disposable /tmp/erri-headless-compat/pfx')
|
||||
for path in (compat, compat/'pfx'):
|
||||
if not path.is_dir() or path.stat().st_uid != os.getuid():
|
||||
raise CleanupRefused('Private prefix must be an existing directory owned by this user')
|
||||
expected = proton.parent/'files/lib/wine/i386-unix/wine64-preloader'
|
||||
if proton != INSTALLED_PROTON or not proton.is_file() or not expected.is_file() or expected.resolve() != expected:
|
||||
raise CleanupRefused('Verified installed Proton device-helper executable required')
|
||||
info = expected.stat()
|
||||
if not stat.S_ISREG(info.st_mode):
|
||||
raise CleanupRefused('Proton device-helper executable is not a regular file')
|
||||
if not hasattr(os, 'pidfd_open') or not hasattr(signal, 'pidfd_send_signal'):
|
||||
raise CleanupRefused('PID-pinned signalling is unavailable; no PID-only fallback')
|
||||
return ProcFS(expected, (info.st_dev, info.st_ino))
|
||||
|
||||
|
||||
def inspect_private_prefix(compat, proton):
|
||||
"""Read-only proof; refuses if anything except exact adopted helpers remains."""
|
||||
return [d.public() for d in _inspector(compat, proton).devices()]
|
||||
|
||||
|
||||
def cleanup_private_prefix(compat, proton):
|
||||
"""After ordinary shutdown, with the shared prefix lock still held.
|
||||
|
||||
Never call as startup recovery for a busy prefix. The caller owns the
|
||||
disposed test session; any other live process makes this fail closed.
|
||||
"""
|
||||
return _cleanup(_inspector(compat, proton), Pidfds())
|
||||
Reference in New Issue
Block a user