fix(subprocess): block /proc/environ env-blocklist bypass (#4427) - #19568
fix(subprocess): block /proc/environ env-blocklist bypass (#4427)#19568EthanGuo-coder wants to merge 4 commits into
Conversation
Fixes NousResearch#4427 Same-UID children could read the parent's stripped env vars by opening /proc/<ppid>/environ when dumpable=1. Clear PR_SET_DUMPABLE on Linux via a prctl ctypes call at module load and inside the env-sanitizing helpers, plus an explicit call before code_execution_tool's subprocess.Popen since it builds child_env inline. argtypes/restype are declared on prctl, the errno path emits a one-shot logger.warning so a silent fallback to the vulnerable state stays observable, and the call is a no-op on non-Linux. Co-Authored-By: Claude <noreply@anthropic.com>
Adds three Linux-only unit tests verifying _sanitize_subprocess_env(), _make_run_env(), and the one-shot _harden_against_proc_environ_leak() gate plus an integration test that spawns a child and asserts /proc/<ppid>/environ is unreadable after hardening (skipped on root, where dumpable=0 is bypassed). Co-Authored-By: Claude <noreply@anthropic.com>
…arch#4427 Independent codex review surfaced one blocker and three important findings on the original /proc/<ppid>/environ hardening. This follow-up addresses all four: 1. MCP stdio servers spawned by mcp_tool._build_safe_env() were not covered by the chokepoint — the helper strips secrets via _SAFE_ENV_KEYS but never invoked the prctl harden, and it can run before tools.environments.local is imported via CLI MCP discovery. Same /proc/<ppid>/environ leak as the issue describes, just at a different spawn site. Add an explicit _harden_against_proc_environ_leak() call at the top of _build_safe_env(). 2. libc.so.6 was hardcoded; on musl-based distros (Alpine) and systems without that soname, CDLL raised OSError and the helper silently returned, leaving the leak open. Resolve via ctypes.util.find_library with a CDLL(None) fallback so the bound symbols come from whatever libc the interpreter is already using. 3. _PROC_ENVIRON_HARDENED was a write-once boolean cache. If the dumpable flag was reset externally (e.g., via prctl from another extension or by a fork that re-enabled it), subsequent sanitizer calls would skip the prctl and leave the parent's /proc/<pid>/environ readable again. Switch to state-based: read PR_GET_DUMPABLE on every call and reapply PR_SET_DUMPABLE=0 when needed. The boolean is kept as informational "have we ever successfully hardened" telemetry. 4. Test fixture set dumpable=1 before importing tools.environments.local; the first import would then re-fire module-load harden and clobber the test setup. Tests passed only because conftest's import chain pre-loaded the module. Reorder so the import (and any module-load side effect) happens before _set_dumpable(1). Replace test_hardening_runs_only_once with test_hardening_reapplies_when_dumpable_resets reflecting the new state-based contract. Add test_mcp_build_safe_env_clears_dumpable as direct regression coverage for finding 1. Co-Authored-By: Claude <noreply@anthropic.com>
325854c to
4a8391a
Compare
There was a problem hiding this comment.
Pull request overview
This PR hardens Linux subprocess launches against a /proc/<ppid>/environ bypass where same-UID child processes can recover secrets stripped from the child’s env= by reading the parent’s proc environ. It does this by clearing PR_SET_DUMPABLE (via prctl(2) through ctypes) before secret-scrubbed subprocess spawns, and adds pytest coverage to validate the hardening.
Changes:
- Add a Linux-only
prctl(PR_SET_DUMPABLE, 0)hardening helper to prevent/proc/<ppid>/environreads from recovering stripped secrets. - Invoke the hardening helper in multiple subprocess env construction/spawn paths (local env helpers, MCP stdio safe env, code execution tool).
- Add a new test suite validating dumpable flag behavior and the
/proc/<ppid>/environaccess denial.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
tools/mcp_tool.py |
Clears dumpability when building MCP stdio subprocess environments to prevent /proc/<ppid>/environ recovery. |
tools/environments/local.py |
Introduces the hardening helper (and wires it into env construction helpers) plus warning-once logging. |
tools/code_execution_tool.py |
Applies the hardening helper before spawning the execute_code sandbox subprocess. |
tests/test_subprocess_proc_environ_hardening.py |
Adds Linux-only tests asserting dumpable is cleared and /proc/<ppid>/environ reads fail post-hardening. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Cache libc.prctl after first resolution, remove module-level harden call so import is side-effect-free, drop duplicate logger, and route the test libc resolution through the production helper so musl-based distros work.
|
@Copilot thanks for the review — all four points addressed in
|
|
obsolete The issue this PR closes appears to be resolved already. Please reopen with a fresh target if this still covers a distinct gap. Signed: GPT-5.5-low in Codex |
|
Closing — issue #4427 was resolved NOT_PLANNED by maintainers, so this subprocess-layer hardening will not be accepted upstream. |
What
Closes #4427. On Linux a same-UID child can recover the parent's stripped
env vars by reading
/proc/<ppid>/environeven when the spawn used afiltered
env=dict, becausedumpable=1leaves/proc/<self>/environworld-readable to same-UID processes. Clear
PR_SET_DUMPABLEon the parentacross the secret-stripping spawn sites so the kernel marks the file
0400 root:root. Hardening is best-effort: a one-shotlogger.warningfires if
prctlis unavailable.Related Issue
Fixes #4427
Related work
PR #4609 fixes a different layer (the
read_filetool's own guard againstreading other processes' environ); the two are complementary and neither
subsumes the other.
Type of Change
Changes Made
PR_SET_DUMPABLEvia actypesprctl(2)call, wired into everysecret-stripping spawn path: the helpers in
tools/environments/local.py,the inline
child_envbuilder intools/code_execution_tool.py, and_build_safe_env()intools/mcp_tool.py(MCP stdio servers).ctypes.util.find_library("c")with aCDLL(None)fallback so musl-based distros (Alpine) don't silently skip the
protection.
PR_GET_DUMPABLEbefore every set so the helper self-corrects whenthe flag is reset by a fork / extension / multiprocessing path.
logger.warningifprctlis unavailable; the spawn stillproceeds.
How to Test
/proc/<pid>/environdoesn't exist.python -m pytest tests/test_subprocess_proc_environ_hardening.py -vtest_child_cannot_read_parent_environ_after_hardeningis skipped underroot (root bypasses
dumpable=0); the upstream CI runner is non-root sothis test executes there.
tools/environments/local.pycould read/proc/<ppid>/environandrecover stripped secrets; after the fix, the same
open()raisesPermissionError.Checklist
Code
pytest tests/test_subprocess_proc_environ_hardening.py -qand the new tests passDocumentation & Housekeeping
sys.platform == "linux"; on other platforms the function is a no-op.