Skip to content

fix(file-ops): stop read_file blocking forever on non-regular files - #82075

Open
Drexuxux wants to merge 1 commit into
NousResearch:mainfrom
Drexuxux:fix/read-file-blocks-on-non-regular-files
Open

fix(file-ops): stop read_file blocking forever on non-regular files#82075
Drexuxux wants to merge 1 commit into
NousResearch:mainfrom
Drexuxux:fix/read-file-blocks-on-non-regular-files

Conversation

@Drexuxux

@Drexuxux Drexuxux commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

What

Every read path in tools/file_operations.py starts with the same size probe:

wc -c < <path> 2>/dev/null

wc opens the path. On a FIFO with no writer, a socket, or a character device that never reaches EOF, that read never returns — and read_file, read_file_raw and read_file_bytes all call _exec without a timeout. There is no deadline anywhere in the call path, so the turn wedges until the process is killed.

The guard that exists for this today is a list of names. e3f8347 (2026-03-31, feat(file_tools): harden read_file with size guard, dedup, and device blocking) introduced _BLOCKED_DEVICE_PATHS with the /dev/* entries, and the list has been extended by name ever since — most recently 868fa95 (2026-07-01, fix(security): block /proc/*/auxv and /proc/*/pagemap read leaks). Each extension adds paths someone thought to enumerate, which is the right shape for /dev and /proc aliases.

It cannot reach this case. A FIFO is a file type, not a name: it can sit at any path in a workspace, created by a build system, a daemon, or a test fixture. No blocklist can enumerate it. Measured against a FIFO with no writer, read_file_tool was still running after 20 s with nothing in the call path able to interrupt it.

The fix

Gate the probe behind [ -f ], which is a stat rather than an open, and route all three readers through one helper:

if [ -f <path> ]; then wc -c < <path> 2>/dev/null;
elif [ -e <path> ]; then echo __hermes_not_regular__;
else exit 1; fi

[ -f ] answers exactly the question the size probe needs — regular file, symlinks followed — without touching the contents. A path that exists but is not a regular file reports the sentinel so the caller can say so instead of claiming the file is missing; a genuinely absent path still exits non-zero and keeps its existing not-found handling, including the similar-file suggestions.

This is independent of the name blocklist and does not change it. The two compose: the blocklist keeps rejecting known device aliases before any I/O, and this bounds everything it cannot enumerate.

Tests and results

New TestNonRegularFileReads in tests/tools/test_file_read_guards.py. Each read runs on a worker thread with a wall clock, so a blocking call fails as an assertion instead of hanging the suite:

  • FIFO — errors with not a regular file instead of blocking; skips where os.mkfifo is unavailable
  • directory — same, replacing a misleading "file not found"
  • regular file — still returns its content
  • missing file — still reports not-found, not the type error

All three readers were exercised directly against a FIFO and against a regular file: each returned a bounded error for the FIFO in under a second, and read the regular file unchanged.

tests/tools/test_file_operations.py::TestShellFileOpsHelpers — 8 passed. test_read_file_uses_bash_safe_windows_paths pinned the probe's exact command string, so it moves to the new one; what it protects — that every command the read path issues carries the bash-safe converted path — is unchanged.

The size probe every read path starts with — `wc -c < path` — opens the
path. On a FIFO with no writer, a socket, or a character device that never
reaches EOF, that read never returns, and read_file/read_file_raw/
read_file_bytes all pass no timeout to _exec. The turn wedges until the
process is killed.

The device blocklist in tools/file_tools.py cannot close this: it matches
literal /dev/* names, so it can only ever cover paths someone thought to
enumerate. A FIFO is a file type and can sit at any path.

Gate the probe behind `[ -f ]`, which stats instead of opening, and report
a path that exists but is not a regular file as such. A missing path keeps
its existing not-found handling.
@alt-glitch alt-glitch added type/bug Something isn't working tool/file File tools (read, write, patch, search) P2 Medium — degraded but workaround exists labels Aug 8, 2026
@spfcraze

spfcraze commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Summary:
The new sentinel error says the path 'could block indefinitely', but a directory read errors instantly and /dev/null — absent from _BLOCKED_DEVICE_PATHS — reaches EOF immediately, so read_file('/dev/null') regresses from an empty read to a hard error with a false justification.

Problems:

  • /dev/null is absent from _BLOCKED_DEVICE_PATHS in tools/file_tools.py:140, and read_file on main probes it with the plain wc -c < form (tools/file_operations.py:1240) — wc -c < /dev/null returns 0 in about a millisecond, so today read_file("/dev/null") succeeds as an empty read. The sentinel (__hermes_not_regular__) now rejects it as 'not a regular file … could block indefinitely', which /dev/null cannot do.
  • A directory is rejected by the same branch but never blocked either: the PR's own test_read_file_tool_on_directory_errors_instead_of_blocking asserts an error, not a hang, and the description itself frames the directory case as 'replacing a misleading file not found' — a message problem, not a blocking one — yet the new message applies the same 'could block indefinitely' claim to it.
  • The message enumerates all four classes ('directory, FIFO, socket, or device'). Reading it could block indefinitely — only the FIFO-with-no-writer and never-EOF device cases actually can; a directory, a socket, and /dev/null end immediately.

Solution:

  • State the type refusal as it is, without the blanket blocking claim: Cannot read '': not a regular file (directory, FIFO, socket, or device). Since /dev/null is absent from _BLOCKED_DEVICE_PATHS and reads as an empty file today, the sentinel branch could also exempt it explicitly.

Checked against 0b958df — the tip of fix/read-file-blocks-on-non-regular-files when this was written — and 6243136, main at the same moment.

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

Labels

P2 Medium — degraded but workaround exists tool/file File tools (read, write, patch, search) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants