fix(file): anchor device symlink guard to task cwd
The read_file device guard now walks symlink hops before the file operation layer, but that hop walk still interpreted relative paths against the Python process cwd. In sessions where TERMINAL_CWD points at the task workspace, a relative workspace symlink to a blocked alias such as /dev/../dev/stdin could therefore miss the intermediate device target before later task-cwd resolution. Anchor relative device checks to the task base before symlink-hop inspection so the pre-I/O guard sees the same workspace path that read_file would otherwise read. Absolute device paths and the existing final realpath fallback remain unchanged. Refs #10141 Refs #29158
This commit is contained in:
parent
e267237671
commit
def3f6388f
|
|
@ -170,6 +170,33 @@ class TestDevicePathBlocking(unittest.TestCase):
|
||||||
self.assertIn("device file", result["error"])
|
self.assertIn("device file", result["error"])
|
||||||
mock_ops.assert_not_called()
|
mock_ops.assert_not_called()
|
||||||
|
|
||||||
|
@patch("tools.file_tools._get_file_ops")
|
||||||
|
def test_read_file_tool_rejects_task_cwd_relative_device_alias_symlink(self, mock_ops):
|
||||||
|
if not os.path.exists("/dev/stdin"):
|
||||||
|
self.skipTest("/dev/stdin is not available on this platform")
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
workspace = os.path.join(tmpdir, "workspace")
|
||||||
|
process_cwd = os.path.join(tmpdir, "process")
|
||||||
|
os.mkdir(workspace)
|
||||||
|
os.mkdir(process_cwd)
|
||||||
|
link_path = os.path.join(workspace, "stdin-link")
|
||||||
|
try:
|
||||||
|
os.symlink("/dev/../dev/stdin", link_path)
|
||||||
|
except OSError as exc:
|
||||||
|
self.skipTest(f"symlink unavailable: {exc}")
|
||||||
|
|
||||||
|
old_cwd = os.getcwd()
|
||||||
|
try:
|
||||||
|
os.chdir(process_cwd)
|
||||||
|
with patch.dict(os.environ, {"TERMINAL_CWD": workspace}, clear=False):
|
||||||
|
result = json.loads(read_file_tool("stdin-link", task_id="dev_rel_link_test"))
|
||||||
|
finally:
|
||||||
|
os.chdir(old_cwd)
|
||||||
|
|
||||||
|
self.assertIn("error", result)
|
||||||
|
self.assertIn("device file", result["error"])
|
||||||
|
mock_ops.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Character-count limits
|
# Character-count limits
|
||||||
|
|
|
||||||
|
|
@ -302,14 +302,17 @@ def _is_blocked_device_path(path: str) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _is_blocked_device(filepath: str) -> bool:
|
def _is_blocked_device(filepath: str, base_dir: str | Path | None = None) -> bool:
|
||||||
"""Return True if the path would hang the process (infinite output or blocking input).
|
"""Return True if the path would hang the process (infinite output or blocking input).
|
||||||
|
|
||||||
Check the literal path first so aliases like /dev/stdin are caught before
|
Check the literal path first so aliases like /dev/stdin are caught before
|
||||||
they resolve to terminal-specific paths. Then check each symlink hop before
|
they resolve to terminal-specific paths. Then check each symlink hop before
|
||||||
the final resolved path so aliases to devices cannot bypass the guard.
|
the final resolved path so aliases to devices cannot bypass the guard.
|
||||||
"""
|
"""
|
||||||
normalized = os.path.normpath(os.path.expanduser(filepath))
|
expanded = os.path.expanduser(filepath)
|
||||||
|
if base_dir is not None and not os.path.isabs(expanded):
|
||||||
|
expanded = os.path.join(os.fspath(base_dir), expanded)
|
||||||
|
normalized = os.path.normpath(expanded)
|
||||||
if _is_blocked_device_path(normalized):
|
if _is_blocked_device_path(normalized):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -850,7 +853,8 @@ def read_file_tool(path: str, offset: int = 1, limit: int = 500, task_id: str =
|
||||||
# ── Device path guard ─────────────────────────────────────────
|
# ── Device path guard ─────────────────────────────────────────
|
||||||
# Block paths that would hang the process (infinite output,
|
# Block paths that would hang the process (infinite output,
|
||||||
# blocking on input). Pure path check — no I/O.
|
# blocking on input). Pure path check — no I/O.
|
||||||
if _is_blocked_device(path):
|
device_base = None if Path(path).expanduser().is_absolute() else _resolve_base_dir(task_id)
|
||||||
|
if _is_blocked_device(path, base_dir=device_base):
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
"error": (
|
"error": (
|
||||||
f"Cannot read '{path}': this is a device file that would "
|
f"Cannot read '{path}': this is a device file that would "
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue