Files
framebridge/source/tests/test_credential_helper_pipe.py
T

101 lines
4.4 KiB
Python

"""A stalled unprivileged stdout reader must not retain the registry lock.
The subprocess disables installation/caller/crypto operations and uses only its
own temporary directory. No sudo, host files, accounts or credentials are used.
"""
import array
import fcntl
import os
from pathlib import Path
import select
import subprocess
import sys
import tempfile
import termios
import time
import unittest
from framebridge_expanded import credential_helper
CHILD = r"""
import importlib.util
import sys
from pathlib import Path
from types import SimpleNamespace
spec = importlib.util.spec_from_file_location("pipe_canary_helper", sys.argv[1])
helper = importlib.util.module_from_spec(spec)
spec.loader.exec_module(helper)
helper.ROOT = Path(sys.argv[2])
helper._installation = lambda: None
helper._prepare_root = lambda: None
helper._caller = lambda: SimpleNamespace(pw_uid=12345, pw_name="synthetic", pw_dir="/synthetic-only")
helper.decrypt = lambda *args: b"X" * 16384
# The only file is our own temporary lock; simulate its expected root metadata.
actual_fstat = helper.os.fstat
helper.os.fstat = lambda fd: SimpleNamespace(st_mode=actual_fstat(fd).st_mode,
st_uid=0, st_nlink=actual_fstat(fd).st_nlink)
raise SystemExit(helper.main(["decrypt", "a" * 64, "b" * 64]))
"""
class CredentialHelperPipeTests(unittest.TestCase):
@unittest.skipUnless(hasattr(fcntl, "F_SETPIPE_SZ"), "Linux pipe-size controls required")
def test_stalled_stdout_reader_does_not_hold_global_registry_lock(self):
with tempfile.TemporaryDirectory(prefix="framebridge-pipe-canary-") as directory:
read_fd, write_fd = os.pipe()
child = None
lock_fd = None
try:
capacity = fcntl.fcntl(write_fd, fcntl.F_SETPIPE_SZ, 4096)
child = subprocess.Popen([sys.executable, "-I", "-S", "-c", CHILD,
str(Path(credential_helper.__file__).resolve()), directory],
stdin=subprocess.DEVNULL, stdout=write_fd,
stderr=subprocess.PIPE, close_fds=True)
os.close(write_fd)
write_fd = None
deadline = time.monotonic() + 5
buffered = array.array("i", [0])
while time.monotonic() < deadline:
fcntl.ioctl(read_fd, termios.FIONREAD, buffered, True)
if buffered[0] == capacity:
break
if child.poll() is not None:
self.fail("Synthetic helper exited before filling the canary pipe: " + child.stderr.read().decode())
time.sleep(0.01)
self.assertEqual(capacity, buffered[0], "Synthetic pipe did not fill")
self.assertIsNone(child.poll(), "Helper should be waiting for the synthetic stdout reader")
lock_fd = os.open(Path(directory) / ".lock", os.O_RDWR)
try:
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
self.fail("Helper retained the registry lock while stdout was blocked")
finally:
fcntl.flock(lock_fd, fcntl.LOCK_UN)
received = bytearray()
deadline = time.monotonic() + 5
while True:
remaining = deadline - time.monotonic()
self.assertGreater(remaining, 0, "Synthetic helper pipe did not finish")
ready, _, _ = select.select([read_fd], [], [], remaining)
self.assertTrue(ready, "Synthetic helper pipe timed out")
block = os.read(read_fd, 4096)
if not block:
break
received.extend(block)
self.assertEqual(b"X" * 16384, bytes(received))
self.assertEqual(0, child.wait(timeout=3), child.stderr.read().decode())
finally:
if child is not None:
if child.poll() is None:
child.kill()
child.wait(timeout=3)
child.stderr.close()
os.close(read_fd)
if write_fd is not None:
os.close(write_fd)
if lock_fd is not None:
os.close(lock_fd)
if __name__ == "__main__":
unittest.main()