Bound credential input reads and verify registry lock recovery
This commit is contained in:
1 parent
12a03a1f97
commit
bf79a79342
4 files changed
+80
-3
No files matched your search
@@ -216,12 +216,20 @@ class CredentialHelperTests(unittest.TestCase):
|
||||
def test_broken_receipt_pipe_rolls_back_published_protect_grant(self):
|
||||
helper.authorize(self.owner, PROFILE, "host")
|
||||
stdout = mock.Mock(); stdout.buffer.write.side_effect = BrokenPipeError("synthetic closed pipe")
|
||||
with mock.patch.object(helper, "_installation"), mock.patch.object(helper, "_caller", return_value=self.owner), mock.patch.object(helper, "_prepare_root"), mock.patch.object(helper.resource, "setrlimit"), mock.patch.object(helper.os, "umask"), mock.patch("sys.stdin", SimpleNamespace(buffer=io.BytesIO(KEY))), mock.patch("sys.stdout", stdout), mock.patch("sys.stderr", new_callable=io.StringIO) as stderr:
|
||||
with mock.patch.object(helper, "_installation"), mock.patch.object(helper, "_caller", return_value=self.owner), mock.patch.object(helper, "_prepare_root"), mock.patch.object(helper, "_read_key", return_value=KEY), mock.patch.object(helper.resource, "setrlimit"), mock.patch.object(helper.os, "umask"), mock.patch("sys.stdout", stdout), mock.patch("sys.stderr", new_callable=io.StringIO) as stderr:
|
||||
self.assertEqual(1, helper.main(["protect", PROFILE]))
|
||||
self.assertNotIn(KEY.decode(), stderr.getvalue())
|
||||
self.assertEqual([".lock"], [p.name for p in self.registry.iterdir()])
|
||||
self.assertEqual([], list(self.policies.iterdir()))
|
||||
|
||||
def test_open_stdin_has_a_deadline_and_excess_input_is_bounded(self):
|
||||
with mock.patch("sys.stdin", SimpleNamespace(fileno=lambda: 17)), mock.patch.object(helper.select, "select", return_value=([], [], [])), mock.patch.object(helper.os, "read") as read:
|
||||
with self.assertRaises(RuntimeError): helper._read_key()
|
||||
read.assert_not_called()
|
||||
with mock.patch("sys.stdin", SimpleNamespace(fileno=lambda: 17)), mock.patch.object(helper.select, "select", return_value=([17], [], [])), mock.patch.object(helper.os, "read", return_value=b"x" * (helper.MAX_KEY + 1)) as read:
|
||||
with self.assertRaises(RuntimeError): helper._read_key()
|
||||
read.assert_called_once_with(17, helper.MAX_KEY + 1)
|
||||
|
||||
def test_ids_reject_paths_flags_and_extra_arguments(self):
|
||||
for value in ("../secret", "--uid=0", "A" * 64, "a" * 63, PROFILE + "\n"):
|
||||
with self.subTest(value=value), self.assertRaises(RuntimeError):
|
||||
|
||||
@@ -27,6 +27,7 @@ spec.loader.exec_module(helper)
|
||||
helper.ROOT = Path(sys.argv[2])
|
||||
helper._installation = lambda: None
|
||||
helper._prepare_root = lambda: None
|
||||
helper._cleanup_temporaries = 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.
|
||||
@@ -37,7 +38,52 @@ raise SystemExit(helper.main(["decrypt", "a" * 64, "b" * 64]))
|
||||
"""
|
||||
|
||||
|
||||
PROTECT_CHILD = CHILD.replace(
|
||||
'raise SystemExit(helper.main(["decrypt", "a" * 64, "b" * 64]))',
|
||||
'\n'.join([
|
||||
'helper.INPUT_TIMEOUT = 0.1',
|
||||
'helper.unauthorize = lambda *args, **kwargs: None',
|
||||
'helper.protect = lambda *args: b"SYNTHETIC-CIPHER-ONLY" * 8',
|
||||
'raise SystemExit(helper.main(["protect", "a" * 64]))',
|
||||
]),
|
||||
)
|
||||
|
||||
|
||||
class CredentialHelperPipeTests(unittest.TestCase):
|
||||
def test_open_stdin_without_eof_times_out_and_releases_registry_lock(self):
|
||||
with tempfile.TemporaryDirectory(prefix="framebridge-input-canary-") as directory:
|
||||
child = None
|
||||
lock_fd = None
|
||||
try:
|
||||
started = time.monotonic()
|
||||
child = subprocess.Popen([sys.executable, "-I", "-S", "-c", PROTECT_CHILD,
|
||||
str(Path(credential_helper.__file__).resolve()), directory],
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, close_fds=True)
|
||||
self.assertEqual(1, child.wait(timeout=1), "Protect must reject an input stream without EOF")
|
||||
self.assertLess(time.monotonic() - started, 1, "Synthetic input timeout exceeded its bound")
|
||||
# Do not close stdin or communicate(): both would supply EOF and
|
||||
# could hide an unbounded read or a leaked registry lock.
|
||||
self.assertFalse(child.stdin.closed)
|
||||
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("Protect retained the registry lock after its input deadline")
|
||||
finally:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
||||
self.assertEqual(b"", child.stdout.read())
|
||||
self.assertIn(b"protected credential operation failed", child.stderr.read())
|
||||
finally:
|
||||
if child is not None:
|
||||
if child.poll() is None:
|
||||
child.kill()
|
||||
child.wait(timeout=3)
|
||||
for stream in (child.stdin, child.stdout, child.stderr):
|
||||
stream.close()
|
||||
if lock_fd is not None:
|
||||
os.close(lock_fd)
|
||||
|
||||
@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:
|
||||
|
||||
Reference in new issue
Block a user