IronClaw
IronClaw moves local filesystem access onto descriptor-rooted traversal
An open IronClaw patch replaces path-check-then-open behavior with descriptor-rooted traversal for the local filesystem backend. The change is intended to close time-of-check/time-of-use escapes in which a path component changes after validation but before access.
nearai/ironclaw PR #6817 is the inspected primary source: “fix(filesystem): close local-backend TOCTOU escapes with fd-rooted traversal.” The two related records below were inspected as supporting context, not treated as independent confirmation.
The facts
- nearai/ironclaw PR #6817 was created 2026-07-28T18:23:58Z and was open at inspection; its title is “fix(filesystem): close local-backend TOCTOU escapes with fd-rooted traversal.” - nearai/ironclaw PR #6832 was created 2026-07-28T21:45:48Z and was open at inspection; its title is “fix(agent-loop): bound recovery per RUN, not just per stage (#6284 WS9).” - nearai/ironclaw PR #6746 was created 2026-07-28T04:23:50Z and was open at inspection; its title is “feat(sandbox): unwired Docker-connect retry, egress allowlist, shell limits.” - The repository reported 12571 stars, 1452 open issues, default branch main, and last push 2026-07-29T00:48:35Z. - The latest tagged-release baseline checked was ironclaw-v1.0.0 published 2026-07-27T19:32:01Z.
What changed
nearai/ironclaw PR #6817 reports: What this fixes crates/ironclaw_filesystem/src/local.rs (DiskFilesystem) had **four TOCTOU containment escapes**, all one family: a pathname check (canonicalize + starts_with) followed by a *separate* syscall that re-resolves the same path string. Between the two, the path can change meaning. | # | Function | Race | |---|---|---| | 1 | resolve_existing | check, then caller separately does read/open/read_dir/remove_* | | 2 | resolve_for_write | validates the **parent** only; the leaf is never re-checked | | 3 | resolve_for_create_dir_all | create_dir_all runs, *then* re-canonicalizes — the mkdir already happened | | 4 | ensure_existing_ancestor_contained | checks the nearest *existing* ancestor; never-yet-existing tail components are unchecked by definition | The code already admitted the gap (local.rs:172, lib.rs:9-15 naming openat2/RESOLVE_BENEATH/cap-std as the production fix). All three RED tests reproduced the escape on the first race iteration** — outside bytes read, outside file mutated on append, outside/leaf created. Not a theoretical window. While closing this, a **second, independent escape was found, confirmed, and fixed**: /projects is mounted wide (containment root
nearai/ironclaw PR #6832 reports: Summary 6284 WS9. The workstream's eleven boxes are two kinds of work: boxes 1–3 are a concrete defect, boxes 4–11 are an **audit**. This implements the fix and reports the audit. The defect: recovery was bounded per stage, never per run.** cleared_attempts() returned Self::default(), wiping every retry counter on any successful model call. That bounds a single stage and leaves the run unbounded — a run alternating success and failure re-earns its full budget indefinitely. So the worst case was never the ~41 model calls one stage permits. **It was unlimited**, with RetryProvider tripling each underneath. run_recovery_attempts is a monotonic per-run counter that cleared_attempts() deliberately preserves. That preservation *is* the fix — the counter is useless without it. The run bound is checked **before** the per-class bound, so a class with remaining per-stage budget cannot retry past an exhausted run. The per-stage reset keeps doing its own job: an unrelated later error still does not inherit a previous stage's attempts. Change Type [x] Bug fix
nearai/ironclaw PR #6746 reports: Why The sandbox container transport has never been sliced to main. Six modules (~5,455 lines) live only on sandbox/shell-integration, and they block the sandbox-docker-tests CI job, the W6 credential swap, and three finished branches queued behind them. This is **slice 1 of 4** — the three leaf modules that depend on nothing else unlanded. Chosen and ordered by actual cargo build probes off origin/main, not by reading imports: this project has already been burned once by picking a slice whose *files* looked separable and whose *symbols* were not. What's here | Module | Lines | Depends on | |---|---|---| | sandbox_process/connect.rs | 311 | bollard, new ironclaw_common dep | | sandbox_process/network_allowlist.rs | 155 | ironclaw_host_api only | | sandbox_process/shell_limits.rs | 120 | std only | **connect** — bounded-retry Docker connect (4 attempts, 250 ms doubling backoff), an IRONCLAW_REBORN_DOCKER_HOST override for CI/remote daemons, and a cheap readiness probe. Retry exhaustion propagates as a hard RuntimeProcessError; there is deliberately no unsandboxed-host fallback, and the module doc says
Why it matters
String normalization cannot hold a filesystem still. Capability-scoped agents need each traversal step anchored to an already-open directory, or a concurrent rename can turn an approved path into access outside the root. This is an operator boundary: persisted state, execution ownership and visible controls must describe the same event. A workaround that broadens access, deletes state or hides the symptom would trade a visible defect for an unmeasured one.
Current
The primary record was open when captured on 2026-07-29. The release baseline was ironclaw-v1.0.0 published 2026-07-27T19:32:01Z. Current repository metadata, pull requests, issues, release records, Hacker News discovery results and the rendered ClawCharts row were checked. ClawCharts selected the subject; it did not prove the claim.
Evidence
The primary URL is https://github.com/nearai/ironclaw/pull/6817. Supporting records are https://github.com/nearai/ironclaw/pull/6832 and https://github.com/nearai/ironclaw/pull/6746. Their source bodies, timestamps and states were captured in the daily evidence bundle. Test counts, reproductions and deployment observations remain attributed to their authors unless explicitly identified as independently rerun.
Source boundary
Open work is described as open, closed work as closed, and operator reports as reports. A pull request is evidence of proposed or reviewed direction, not proof of a shipped release. Issue closure would not by itself prove a deployed fix. Search residue, package mirrors and historically published source spines were excluded.
Operator take
Run adversarial rename and symlink races against every read, write and metadata operation; preserve fail-closed behavior when a platform lacks the required descriptor primitives. Preserve a before-state receipt, make the smallest reversible change, and verify the original failure independently after intervention.
Caveat
Public project records are mutable. Status, scope and evidence can change after publication. This brief records the inspected state and does not authorize changes to a reader’s deployment.
Source inspected; source state, environment and release boundary remain explicit.