From 221126cb77ea41ec283598a23d628f760a51bdea Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 22 Sep 2026 02:34:08 +0200 Subject: [PATCH] Read the whole bwrap info object before parsing the child pid bwrap writes --info-fd JSON in several writes. A single os.read could return only '{"child-pid": N' and fail with a JSON error, observed reproducibly on railiance01 for the rebuilt owner runtime. Read until a complete object or EOF within the existing 10 s bound; a truncated object at EOF is still refused. 207 tests pass. Co-Authored-By: Claude Opus 5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 272244@bnt-lap001 Assistant-Session: c8962fa7-b290-47df-865f-403ddb6c77e9 --- src/sandboxer/extensions/bwrap.py | 31 ++++++++++++++----- tests/test_bwrap.py | 49 +++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/src/sandboxer/extensions/bwrap.py b/src/sandboxer/extensions/bwrap.py index ad1949e..5756a11 100644 --- a/src/sandboxer/extensions/bwrap.py +++ b/src/sandboxer/extensions/bwrap.py @@ -113,14 +113,31 @@ class BwrapExtension(SandboxExtension): @staticmethod def _read_child_pid(proc: subprocess.Popen, info_fd: int) -> int: - ready, _, _ = select.select([info_fd], [], [], 10) - if not ready: - proc.kill() - raise RuntimeError("timed out waiting for bwrap namespace child pid") - raw = os.read(info_fd, 16_384) + # bwrap writes the info object in several writes; one read can return + # only its first line. Read until EOF or a complete object, bounded. + deadline = time.monotonic() + 10 + raw = b"" + info = None + while info is None: + remaining = deadline - time.monotonic() + ready, _, _ = select.select([info_fd], [], [], max(remaining, 0)) + if not ready: + proc.kill() + raise RuntimeError("timed out waiting for bwrap namespace child pid") + chunk = os.read(info_fd, 16_384) + raw += chunk + if len(raw) > 16_384: + proc.kill() + raise RuntimeError("bwrap did not report a valid namespace child pid") + try: + info = json.loads(raw) + except json.JSONDecodeError as exc: + if not chunk: + proc.kill() + raise RuntimeError("bwrap did not report a valid namespace child pid") from exc try: - child_pid = int(json.loads(raw)["child-pid"]) - except (json.JSONDecodeError, KeyError, TypeError, ValueError) as exc: + child_pid = int(info["child-pid"]) + except (KeyError, TypeError, ValueError) as exc: proc.kill() raise RuntimeError("bwrap did not report a valid namespace child pid") from exc if child_pid <= 0: diff --git a/tests/test_bwrap.py b/tests/test_bwrap.py index 3ce0dee..592d3a5 100644 --- a/tests/test_bwrap.py +++ b/tests/test_bwrap.py @@ -362,3 +362,52 @@ def test_runner_bounds_output_and_normalizes_timeout(tmp_path) -> None: assert result["stdout"] == "123" assert result["stderr"] == "abc" assert result["output_truncated"] is True + + +class _FakeProc: + def __init__(self) -> None: + self.killed = False + + def kill(self) -> None: + self.killed = True + + +def test_read_child_pid_accepts_info_split_across_writes() -> None: + import os + import threading + import time + + read_fd, write_fd = os.pipe() + + def writer() -> None: + os.write(write_fd, b'{\n "child-pid": 1234567') + time.sleep(0.05) + os.write(write_fd, b',\n "cgroup-namespace": 4026531835\n}\n') + os.close(write_fd) + + thread = threading.Thread(target=writer) + thread.start() + try: + proc = _FakeProc() + assert BwrapExtension._read_child_pid(proc, read_fd) == 1234567 + assert not proc.killed + finally: + thread.join() + os.close(read_fd) + + +def test_read_child_pid_refuses_truncated_info_at_eof() -> None: + import os + + import pytest + + read_fd, write_fd = os.pipe() + os.write(write_fd, b'{\n "child-pid": 1234567') + os.close(write_fd) + proc = _FakeProc() + try: + with pytest.raises(RuntimeError, match="valid namespace child pid"): + BwrapExtension._read_child_pid(proc, read_fd) + assert proc.killed + finally: + os.close(read_fd)