noqt commited on
Commit
2341488
·
1 Parent(s): ee6b2e4

fix: prevent FIFO descriptors from blocking discovery

Browse files
src/lumi_eggcracker/procfd.py CHANGED
@@ -306,7 +306,16 @@ def open_process_fd(
306
  """
307
  if isinstance(number, bool) or not isinstance(number, int) or number < 0:
308
  raise JsonInputError("target descriptor number is invalid")
309
- flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0)
 
 
 
 
 
 
 
 
 
310
  path = proc / str(value.pid) / "fd" / str(number)
311
  try:
312
  descriptor = os.open(path, flags)
 
306
  """
307
  if isinstance(number, bool) or not isinstance(number, int) or number < 0:
308
  raise JsonInputError("target descriptor number is invalid")
309
+ # A target can deliberately expose a FIFO, device, or another descriptor
310
+ # whose open would wait for external activity. Discovery must never let
311
+ # an untrusted workload block the supervisor before the descriptor has
312
+ # been validated as a regular file. O_NONBLOCK is inert for ordinary
313
+ # files and lets the caller reject non-regular descriptors after fstat.
314
+ flags = (
315
+ os.O_RDONLY
316
+ | getattr(os, "O_CLOEXEC", 0)
317
+ | getattr(os, "O_NONBLOCK", 0)
318
+ )
319
  path = proc / str(value.pid) / "fd" / str(number)
320
  try:
321
  descriptor = os.open(path, flags)
tests/test_procfd.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import tempfile
5
+ import threading
6
+ import unittest
7
+ from pathlib import Path
8
+
9
+ from lumi_eggcracker.discovery import identity
10
+ from lumi_eggcracker.jsonio import JsonInputError
11
+ from lumi_eggcracker.procfd import open_process_fd
12
+
13
+
14
+ @unittest.skipUnless(os.name == "posix" and hasattr(os, "mkfifo"), "requires POSIX FIFOs")
15
+ class ProcessDescriptorTests(unittest.TestCase):
16
+ def test_fifo_descriptor_reopen_cannot_block_discovery(self) -> None:
17
+ """A hostile FIFO descriptor must not stop autonomous scan progress."""
18
+ with tempfile.TemporaryDirectory() as raw:
19
+ fifo = Path(raw) / "hostile-fd"
20
+ os.mkfifo(fifo, mode=0o600)
21
+ keeper = os.open(fifo, os.O_RDWR | os.O_NONBLOCK | os.O_CLOEXEC)
22
+ target_fd = os.open(fifo, os.O_RDONLY | os.O_NONBLOCK | os.O_CLOEXEC)
23
+ os.close(keeper)
24
+ process = identity(os.getpid())
25
+ self.assertIsNotNone(process)
26
+ assert process is not None
27
+
28
+ outcome: dict[str, object] = {}
29
+
30
+ def probe() -> None:
31
+ try:
32
+ outcome["descriptor"] = open_process_fd(process, target_fd)[0]
33
+ except (JsonInputError, OSError) as error:
34
+ outcome["error"] = error
35
+
36
+ worker = threading.Thread(target=probe, daemon=True)
37
+ writer = -1
38
+ worker.start()
39
+ worker.join(timeout=0.5)
40
+ blocked = worker.is_alive()
41
+ if blocked:
42
+ # Unblock an unfixed implementation so this regression cannot
43
+ # strand the test process after reporting the failure.
44
+ writer = os.open(fifo, os.O_WRONLY | os.O_NONBLOCK | os.O_CLOEXEC)
45
+ worker.join(timeout=1.0)
46
+ try:
47
+ self.assertFalse(blocked, "reopening a FIFO descriptor blocked discovery")
48
+ self.assertNotIn("error", outcome)
49
+ finally:
50
+ descriptor = outcome.get("descriptor")
51
+ if isinstance(descriptor, int):
52
+ os.close(descriptor)
53
+ if writer >= 0:
54
+ os.close(writer)
55
+ os.close(target_fd)
56
+
57
+
58
+ if __name__ == "__main__":
59
+ unittest.main()