Skip to content

fix(security): extend /proc read block to maps siblings + auxv + pagemap - #226

Merged
hashbender merged 1 commit into
mainfrom
mirror/pr-56219
Jul 1, 2026
Merged

fix(security): extend /proc read block to maps siblings + auxv + pagemap#226
hashbender merged 1 commit into
mainfrom
mirror/pr-56219

Conversation

@hashbender

Copy link
Copy Markdown
Owner

Summary

read_file now blocks six more /proc/* pseudo-files that leaked the same ASLR layout /proc/*/maps was hidden to protect (PR NousResearch#4609). Previously _is_blocked_device_path() only matched endswith(("/environ", "/cmdline", "/maps")), so every sibling below slipped through.

Path Leak Was blocked?
/proc/*/smaps Superset of maps: full VMA list + addresses
/proc/*/smaps_rollup Aggregated VMA addresses
/proc/*/numa_maps VMA dump + NUMA annotations
/proc/*/mem Raw RW process memory
/proc/*/auxv AT_RANDOM canary seed + AT_BASE/AT_PHDR load addrs
/proc/*/pagemap Virtual→physical translation

read_file("/proc/self/smaps") or /proc/self/auxv recovered the exact address layout NousResearch#4609 set out to hide.

Changes

  • tools/file_tools.py: extend the endswith tuple in _is_blocked_device_path() to cover smaps / smaps_rollup / numa_maps / mem (@AhmetArif0's commit) plus auxv / pagemap. endswith matches both /proc/<pid>/X and the per-thread /proc/<pid>/task/<tid>/X alias.
  • tests/tools/test_file_read_guards.py: regression assertions for all six new paths + a new test for the /proc/<pid>/task/<tid>/* thread-alias form.

Same suffix-tuple pattern as NousResearch#4609. A regex/set refactor across the full /proc leak surface (stack, syscall, wchan, kallsyms, …) is a worthwhile follow-up but out of scope for closing this immediate gap.

Validation

Before After
/proc/self/{smaps,smaps_rollup,numa_maps,mem,auxv,pagemap} readable blocked
/proc/self/task/<tid>/{maps,auxv,pagemap,…} readable blocked
/proc/{cpuinfo,meminfo,version,uptime,status} readable readable (no false positive)
  • tests/tools/test_file_read_guards.py: 43/43 pass.
  • E2E with real imports against a temp HERMES_HOME: all 10 sensitive paths refused at both the _is_blocked_device guard and the read_file surface; all 5 legit /proc paths still readable.

Salvage of NousResearch#32238 (@AhmetArif0, commit cherry-picked with authorship preserved). Closes NousResearch#34430, closes NousResearch#32238.

Infographic

proc-read-guard-hardened


Mirror-of: NousResearch#56219
NousResearch#56219

@tenki-reviewer

tenki-reviewer Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Complete

Files Reviewed: 2
Findings: 3

By Severity:

  • 🟠 High: 2
  • 🟡 Medium: 1

PR adds /proc pseudo-file read guards and search result filtering but introduces three issues: an incomplete tenki backend removal that breaks file operations, a /proc/*/stat omission in the ASLR blocklist, and a search result filter that doesn't apply the /proc device blocklist.

Files Reviewed (2 files)
tests/tools/test_file_read_guards.py
tools/file_tools.py

@hashbender
hashbender merged commit 14a9d6c into main Jul 1, 2026
3 checks passed

@tenki-reviewer tenki-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: 🟠 High (72/100) — 2 high findings, 1 medium · 126 LOC across 2 files


High Severity

  1. Incomplete tenki removal (tools/file_tools.py:987): The PR removes tenki backend support from file_tools.py only, while terminal_tool.py and code_execution_tool.py still carry full tenki support (image branches, container_config keys, _CONTAINER_BACKENDS membership). Any tenki user triggering file operations will get image="" and container_config=None, causing TenkiEnvironment to fail at connection time. The five other callers are unaffected because their code paths are unchanged.

  2. ASLR bypass via /proc/*/stat (tools/file_tools.py:374-386): The blocklist expansion covers maps, smaps, smaps_rollup, numa_maps, mem, auxv, and pagemap but omits /proc/<pid>/stat, which exposes start_code, end_code, and start_stack (fields 26-28) per proc(5) — the same ASLR-sensitive addresses as maps. An attacker/model blocked from reading /maps can read /stat instead.

Medium Severity

  1. search_tool /proc filter asymmetry (tools/file_tools.py:446-477): read_file_tool applies dual guards (_is_blocked_device + get_read_block_error), but the new search filter applies only get_read_block_error. /proc pseudo-file path metadata leaks through search results even though read_file_tool blocks those same paths. Content is redacted but file path metadata (including line numbers) persists in results.

Impact

This is a security-hardening PR with three gaps: one functional regression (tenki), one genuine bypass (stat omission), and one defense-in-depth inconsistency (search asymmetry). The tenki issue will break existing tenki-backed deployments; the other two reduce the effectiveness of the intended hardening.

Comment thread tools/file_tools.py

container_config = None
if env_type in _CONTAINER_BACKENDS:
if env_type in {"docker", "singularity", "modal", "daytona"}:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Incomplete tenki removal: file_tools.py strips tenki config while terminal_tool.py and code_execution_tool.py still expect it (bug)

The PR removes tenki backend support from file_tools.py only: it deletes the elif env_type == "tenki": image-resolution branch, changes line 987 from if env_type in _CONTAINER_BACKENDS to a hardcoded set {"docker", "singularity", "modal", "daytona"} that excludes tenki, and removes all tenki_* keys from container_config. However, two other files in the same codebase still carry full tenki support: terminal_tool.py (line 1218: _CONTAINER_BACKENDS includes "tenki"; lines 1522-1544: _create_environment() reads tenki_api_endpoint, tenki_workspace_id, tenki_project_id, etc. from container_config and passes them to TenkiEnvironment) and code_execution_tool.py (lines 655-681: has the env_type == "tenki" image branch, puts tenki in the container set, and includes all tenki_* keys in container_config). As a result, any user with env_type = "tenki" who triggers file operations (read_file, write_file, search_files — via any of the 6 call sites of _get_file_ops at lines 1080, 1198, 1517, 1538, 1671, 1809) will get image="" and container_config=None, causing TenkiEnvironment to be constructed with blank credentials that will fail at connection time. Terminal operations and code execution remain unaffected because their respective code paths are unchanged.

💡 Suggestion: The tenki removal must be applied consistently across all three files. Either (a) restore tenki support in file_tools.py to match terminal_tool.py and code_execution_tool.py, including the image branch, container_config keys, and the _CONTAINER_BACKENDS guard; or (b) complete the tenki removal by also removing tenki from terminal_tool.py _CONTAINER_BACKENDS (line 1218), the TenkiEnvironment creation block (lines 1522-1544), and code_execution_tool.py (lines 655-656, 663, 671-680). The current partial removal leaves the code in an inconsistent state.

📋 Prompt for AI Agents

To complete the tenki removal consistently: (1) In tools/terminal_tool.py line 1218, remove "tenki" from the CONTAINER_BACKENDS frozenset. (2) In tools/terminal_tool.py, remove lines 1522-1544 (the elif env_type == "tenki": block that creates TenkiEnvironment). (3) In tools/code_execution_tool.py line 655-656, remove the elif env_type == "tenki": image branch. (4) In tools/code_execution_tool.py line 663, remove "tenki" from the hardcoded set. (5) In tools/code_execution_tool.py lines 671-680, remove the tenki* config keys from container_config. (6) In tools/file_tools.py line 987, restore _CONTAINER_BACKENDS instead of the hardcoded set (so it stays consistent after tenki is removed from the frozenset).

Comment thread tools/file_tools.py
Comment on lines 374 to 386
if normalized.startswith("/proc/") and normalized.endswith(
("/environ", "/cmdline", "/maps")
(
"/environ",
"/cmdline",
"/maps",
"/smaps",
"/smaps_rollup",
"/numa_maps",
"/mem",
"/auxv",
"/pagemap",
)
):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 /proc/*/stat missing from blocklist enables ASLR oracle bypass of maps/smaps/auxv blocks (security)

The _is_blocked_device_path function blocks /proc/*/maps, /smaps, /smaps_rollup, /numa_maps, /mem, /auxv, and /pagemap to prevent ASLR bypass and memory disclosure. However, /proc/<pid>/stat is NOT blocked, despite exposing start_code (text-segment base), end_code (text-segment end), and start_stack (stack base) — fields 26–28 per the Linux proc(5) man page. These are the same ASLR-sensitive addresses that maps would reveal. An attacker or model that cannot read /proc/self/maps can read /proc/self/stat and extract equivalent address information, defeating the block. The endswith-based check would naturally cover both /proc/<pid>/stat and /proc/<pid>/task/<tid>/stat if /stat were added to the blocked suffix tuple.

💡 Suggestion: Add /stat to the blocked suffix tuple in _is_blocked_device_path so the ASLR oracle is blocked alongside maps/smaps/auxv/pagemap. The endswith check already covers /proc/<pid>/stat and /proc/<pid>/task/<tid>/stat automatically.

📋 Prompt for AI Agents

In tools/file_tools.py, in function _is_blocked_device_path around lines 374-386, add /stat to the endswith tuple alongside the existing blocked /proc suffixes. The tuple currently contains ('/environ', '/cmdline', '/maps', '/smaps', '/smaps_rollup', '/numa_maps', '/mem', '/auxv', '/pagemap'). Insert '/stat' into the tuple. Also update the test fixtures in tests/tools/test_file_read_guards.py to add test cases for /proc/self/stat, /proc/12345/stat, and /proc/self/task/1234/stat in the existing test_proc_sensitive_pseudo_files_blocked and test_proc_task_thread_sensitive_files_blocked tests.

Comment thread tools/file_tools.py
Comment on lines +446 to +477
def _filter_read_blocked_search_results(result, task_id: str = "default") -> int:
"""Remove credential/cache/env paths from a SearchResult in-place."""
omitted = 0

if hasattr(result, "matches") and result.matches:
allowed_matches = []
for match in result.matches:
if _search_result_read_block_error(match.path, task_id):
omitted += 1
continue
allowed_matches.append(match)
result.matches = allowed_matches

if hasattr(result, "files") and result.files:
allowed_files = []
for file_path in result.files:
if _search_result_read_block_error(file_path, task_id):
omitted += 1
continue
allowed_files.append(file_path)
result.files = allowed_files

if hasattr(result, "counts") and result.counts:
allowed_counts = {}
for file_path, count in result.counts.items():
if _search_result_read_block_error(file_path, task_id):
omitted += 1
continue
allowed_counts[file_path] = count
result.counts = allowed_counts

return omitted

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 search_tool results bypass /proc pseudo-file blocklist applied by read_file_tool (security)

read_file_tool blocks /proc pseudo-files (maps, mem, environ, auxv, pagemap, stat, etc.) via _is_blocked_device at line 1059. However, the new search result filtering in _filter_read_blocked_search_results (lines 446-477) and the search directory guard (line 1805) apply only get_read_block_error — which blocks credential stores, .env files, and Hermes cache paths, but NOT /proc pseudo-files. This means: (1) a search rooted in /proc/self/ is not blocked by the directory guard, and (2) search results containing paths like /proc/self/maps or /proc/12345/environ pass through the result filter. While content matches are redacted via redact_sensitive_text (line 1818), the file path metadata (including line numbers) remains in results. This creates an asymmetry between read_file_tool (dual guards) and search_tool (single guard), potentially leaking blocked-path metadata through search enumeration.

💡 Suggestion: Extend _filter_read_blocked_search_results to also call _is_blocked_device (or _is_blocked_device_path) for each search result path, mirroring the dual-guard pattern used in read_file_tool. Additionally, apply _is_blocked_device to the search directory in search_tool (alongside the existing get_read_block_error guard at line 1805) to block searches rooted in /proc/ directories.

📋 Prompt for AI Agents

In tools/file_tools.py: (1) In _search_result_read_block_error (lines 431-443), add a call to _is_blocked_device_path(str(resolved)) alongside the existing get_read_block_error(str(resolved)) call — return the first non-None error from either guard, so /proc pseudo-files are filtered from search results. (2) In search_tool around line 1805, add a call to _is_blocked_device(str(resolved_path)) for the search directory path alongside the existing get_read_block_error check, so searches rooted in /proc/self/ or similar sensitive directories are blocked before performing I/O.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant