fix(tui): prevent RPC stdin inheritance

This commit is contained in:
KoNit-K authored and Teknium committed 2026-09-16 17:53:28 -07:00
1 parent 7ad51527d4
commit ec1f26f476
2 files changed
+73

No files matched your search

@@ -0,0 +1,51 @@
"""Regression coverage for the stdio gateway's child-process boundary."""
import os
import sys
import pytest
from tui_gateway import entry
@pytest.mark.skipif(os.name != "posix", reason="POSIX close-on-exec semantics")
def test_rpc_stdin_is_closed_before_a_bare_child_executes(monkeypatch):
"""A child with no explicit stdin must not be able to consume RPC bytes."""
original_stdin = os.dup(0)
original_inheritable = os.get_inheritable(0)
reader, writer = os.pipe()
try:
os.dup2(reader, 0)
os.write(writer, b'{"method":"prompt.submit"}\n')
monkeypatch.setattr(entry.sys, "stdin", type("RpcStdin", (), {"fileno": lambda _: 0})())
entry._close_rpc_stdin_on_exec()
output_reader, output_writer = os.pipe()
pid = os.fork()
if pid == 0:
os.dup2(output_writer, 1)
os.close(output_reader)
os.close(output_writer)
os.execv(
sys.executable,
[
sys.executable,
"-c",
"import os; "
"\ntry: os.read(0, 1)\n"
"except OSError: print('stdin-closed')\n"
"else: print('stdin-inherited')",
],
)
os.close(output_writer)
_, status = os.waitpid(pid, 0)
assert os.waitstatus_to_exitcode(status) == 0
assert os.read(output_reader, 100) == b"stdin-closed\n"
os.close(output_reader)
finally:
os.dup2(original_stdin, 0, inheritable=original_inheritable)
os.close(original_stdin)
os.close(reader)
os.close(writer)
+22
View File
@@ -33,6 +33,27 @@ _mcp_discovery_thread = None
_mcp_discovery_enabled = False
def _close_rpc_stdin_on_exec() -> None:
"""Keep stdio RPC requests out of children launched by the gateway.
The TUI's stdin is the Node-to-Python JSON-RPC socketpair. Standard
descriptors survive subprocess execution unless they are explicitly
close-on-exec, so a dependency that launches a child without ``stdin=``
could otherwise consume a request before this process reads it.
Windows does not provide the POSIX FD_CLOEXEC contract used here; retain
its existing descriptor behavior rather than changing its launch paths.
"""
if os.name != "posix":
return
try:
os.set_inheritable(sys.stdin.fileno(), False)
except (OSError, ValueError):
# Embedded launchers can supply a stream without an inheritable file
# descriptor. Keep gateway startup available when no guard is possible.
logger.debug("could not mark TUI RPC stdin close-on-exec", exc_info=True)
def _install_sidecar_publisher() -> None:
"""Mirror every dispatcher emit to the dashboard sidebar via WS when set (best-effort)."""
url = os.environ.get("HERMES_TUI_SIDECAR_URL")
@@ -237,6 +258,7 @@ def _write_or_exit(payload: dict, reason: str) -> None:
def main():
_close_rpc_stdin_on_exec()
_install_sidecar_publisher()
# The heartbeat row lets the orphan sweep tell "live but idle" from "truly orphaned",