Skip to content

fix(exec): allow symlinked OPENCLAW_STATE_DIR via terminal stat#96037

Closed
t2wei wants to merge 1 commit into
openclaw:mainfrom
t2wei:fix/exec-approvals-allow-symlinked-state-dir
Closed

fix(exec): allow symlinked OPENCLAW_STATE_DIR via terminal stat#96037
t2wei wants to merge 1 commit into
openclaw:mainfrom
t2wei:fix/exec-approvals-allow-symlinked-state-dir

Conversation

@t2wei

@t2wei t2wei commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

src/infra/exec-approvals.ts's ensureDir validated the approvals directory with fs.lstatSync and rejected any path where isSymbolicLink() was true:

```ts
const dirStat = fs.lstatSync(dir);
if (!dirStat.isDirectory() || dirStat.isSymbolicLink()) {
throw new Error(`Refusing to use unsafe exec approvals directory: ${dir}`);
}
```

This breaks on common deployment topologies where the state directory itself is a top-level symlink — e.g. `/opt/openclaw` → `/mnt/efs/openclaw` for EFS-backed hosts on AWS, or any deployment that wants a stable canonical path while the actual storage is mounted under a different name.

Why The Companion Check Misses It

The function calls `assertNoExecApprovalsSymlinkParents(dir, resolveRequiredHomeDir())` first, which walks segments BETWEEN the trusted root and the terminal target. When `OPENCLAW_STATE_DIR` equals `OPENCLAW_HOME` (a common deployment shape where the symlink is the canonical state root), there are zero segments to walk, so the terminal symlink slips past that check entirely. The follow-up `lstatSync` check is then the only gate — and it rejects the entire deployment.

User Impact

Before the fix, every `exec` tool call from every agent failed with the same error, regardless of the `workdir` parameter the agent passed:

```text
[tools] exec failed: Refusing to use unsafe exec approvals directory: /opt/openclaw
raw_params={"workdir":"/opt/openclaw/workspace/docs/tmp/job-1","timeout":20}
```

The error message is misleading because the rejection happens during approvals-state resolution (server-side) before `workdir` is even consulted. Agents that retried with different `workdir` values still failed identically.

Fix

Switch the terminal directory check from `fs.lstatSync` to `fs.statSync` (follows symlinks). A terminal symlink whose target is a real directory now passes, while:

  • Intermediate path symlinks remain rejected by `assertNoExecApprovalsSymlinkParents` (unchanged).
  • Non-directory terminals (regular files, FIFOs, etc.) remain rejected via `dirStat.isDirectory()` (unchanged).

Behavior is unchanged for the no-symlink path: `statSync` returns the same stat as `lstatSync` when the target is a normal directory.

Evidence

  • `pnpm tsgo` passes.
  • `pnpm vitest run src/infra/exec-approvals.test.ts src/infra/exec-approvals-store.test.ts` → 46/46 tests pass.
  • New regression test `accepts a symlinked state directory whose target is a real directory` mirrors the production topology: `OPENCLAW_HOME` and `OPENCLAW_STATE_DIR` both set to a symlink whose target is a fresh directory. Pre-fix: throws `Refusing to use unsafe exec approvals directory`. Post-fix: `ensureExecApprovals()` resolves cleanly and creates the approvals file at the symlink's resolved target.
  • End-to-end verified on a real EFS-backed EC2 deploy: after deploying the fix (and replacing the symlink workaround with bind mount), `exec` tool now succeeds with any reasonable `workdir`.

Test plan

  • `pnpm tsgo`
  • Targeted unit tests pass
  • End-to-end on EC2 systemd deploy
  • (reviewer) Verify symlink-in-parent path is still rejected (already covered by existing tests around `assertNoExecApprovalsSymlinkParents`)

🤖 AI-assisted PR — diagnosis + patch produced with Claude Code; verified against production deploy that exhibited the bug.

`ensureDir` validated the approvals directory with `fs.lstatSync`, then
rejected any path where `isSymbolicLink()` was true. This breaks on
common deployment topologies where the state directory itself is a
top-level symlink (e.g. `/opt/openclaw` -> `/mnt/efs/openclaw` for
EFS-backed hosts on AWS).

The companion `assertNoExecApprovalsSymlinkParents` check walks the
segments BETWEEN the trusted root and the terminal target — when
`OPENCLAW_STATE_DIR` equals `OPENCLAW_HOME` (the trusted root), no
segments are walked and the terminal symlink slips past that walk
without inspection. The follow-up lstat check is therefore the only
gate, and it rejects the entire deployment.

Switch to `fs.statSync` so a terminal symlink that resolves to a real
directory is accepted while:

- intermediate path symlinks are still rejected by the parents check
- non-directory terminals (regular files, FIFOs, etc.) are still
  rejected via `dirStat.isDirectory()`

Symptom before fix:

```text
[tools] exec failed: Refusing to use unsafe exec approvals directory:
  /opt/openclaw raw_params={"workdir":"...","timeout":20}
```

Every exec tool call from any agent failed with the same error
regardless of `workdir`, because `OPENCLAW_STATE_DIR` resolution
happens server-side before the workdir is consulted.

Adds a regression test that mirrors the deployment shape: `HOME` and
`STATE_DIR` both pointing at a symlink whose target is a real directory.
@clawsweeper

clawsweeper Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed June 30, 2026, 5:34 PM ET / 21:34 UTC.

Summary
The PR changes exec approvals directory validation from terminal lstatSync symlink rejection to statSync symlink following and adds a POSIX regression test for symlinked OPENCLAW_HOME/OPENCLAW_STATE_DIR.

PR surface: Source +8, Tests +24. Total +32 across 2 files.

Reproducibility: yes. from source inspection. Current main resolves OPENCLAW_STATE_DIR into the exec approvals path and then rejects a terminal symlink via lstatSync, matching the PR's regression test scenario.

Review metrics: 1 noteworthy metric.

  • Exec approvals symlink check: 1 terminal directory check relaxed. This changes the filesystem trust boundary for command-approval storage, so maintainers should review more than the small LOC size.

Merge readiness
Overall: 🧂 unranked krab
Proof: 🧂 unranked krab
Patch quality: 🧂 unranked krab
Result: blocked until real behavior proof is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P1] Add redacted after-fix terminal output, logs, screenshot, recording, or linked artifact showing an exec call succeeds in the symlinked deployment.
  • Revise the symlink handling so terminal state-dir symlinks are rejected unless they pass an explicit trusted-root and target-safety policy with regression tests.

Proof guidance:

  • [P1] Needs real behavior proof before merge: The PR body claims EC2/EFS verification but includes no redacted after-fix terminal output, logs, screenshot, recording, or linked artifact; proof should redact private details and updating the PR body should trigger re-review. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Risk before merge

  • [P1] Merging as submitted would change command-approval storage from rejecting terminal directory symlinks to accepting any terminal symlink that resolves to a directory, including outside-root OPENCLAW_STATE_DIR paths the parent guard does not inspect.
  • [P1] The PR body claims EC2/EFS validation, but it does not include redacted after-fix terminal output, logs, screenshots, recordings, or a linked artifact showing an exec call succeeding.

Maintainer options:

  1. Narrow And Validate The Terminal Symlink (recommended)
    Allow only the explicitly trusted state-root/home terminal symlink shape after checking target safety and add rejected-case coverage for outside-root or unsafe targets.
  2. Accept The Expanded Boundary Explicitly
    Maintainers may intentionally allow arbitrary terminal state-dir symlinks, but that should be a visible security decision with documentation and real behavior proof.
  3. Fold Into Broader Symlink Design
    Pause this small patch and resolve terminal state-dir support with the broader exec approvals symlink work already being discussed in related items.

Next step before merge

  • [P1] Human security review and contributor revision are needed because the diff relaxes exec approval symlink validation and lacks real behavior proof.

Security
Needs attention: The patch changes a command-approval filesystem trust boundary and needs a narrower validated symlink policy before merge.

Review findings

  • [P1] Keep exec approvals symlinks validated — src/infra/exec-approvals.ts:429-430
Review details

Best possible solution:

Support symlinked state-dir deployments through a narrowly validated terminal-symlink policy that preserves parent, destination-file, hardlink, and migration safeguards, with redacted real exec proof before merge.

Do we have a high-confidence way to reproduce the issue?

Yes, from source inspection. Current main resolves OPENCLAW_STATE_DIR into the exec approvals path and then rejects a terminal symlink via lstatSync, matching the PR's regression test scenario.

Is this the best way to solve the issue?

No, not as submitted. Switching to statSync fixes the reported topology but also accepts unvalidated terminal symlinks outside the trusted root; the safer path is a narrower validated symlink allowance with explicit rejected-case coverage.

Full review comments:

  • [P1] Keep exec approvals symlinks validated — src/infra/exec-approvals.ts:429-430
    Switching the final check to statSync follows any terminal OPENCLAW_STATE_DIR symlink. When the state dir is outside OPENCLAW_HOME, the parent guard returns without walking it because allowOutsideRoot is enabled, so this accepts an unvalidated symlink target for approval storage. Please either keep terminal symlink rejection outside the trusted-root case or add explicit symlink/target validation plus rejected-case coverage.
    Confidence: 0.86

Overall correctness: patch is incorrect
Overall confidence: 0.86

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 44ec7580e2f0.

Label changes

Label justifications:

  • P1: The PR targets a real exec-tool availability failure while changing a command-approval security boundary.
  • merge-risk: 🚨 security-boundary: The diff relaxes symlink rejection for exec approval storage, which can affect command authorization state and socket placement.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🧂 unranked krab.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: The PR body claims EC2/EFS verification but includes no redacted after-fix terminal output, logs, screenshot, recording, or linked artifact; proof should redact private details and updating the PR body should trigger re-review. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed

PR surface:

Source +8, Tests +24. Total +32 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 10 2 +8
Tests 1 24 0 +24
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 34 2 +32

Security concerns:

  • [medium] Unvalidated terminal state-dir symlink — src/infra/exec-approvals.ts:429
    Following the terminal approvals directory symlink allows approval state and socket files to be written through a symlink target without checking the symlink location, target ownership, permissions, or outside-root parent chain.
    Confidence: 0.86

What I checked:

  • Repository policy read and applied: The full root AGENTS.md was read; its review policy applies because this PR touches exec approvals, a command-execution filesystem trust boundary. (AGENTS.md:1, 44ec7580e2f0)
  • Current main rejects terminal symlink directories: ensureDir() calls the symlink-parent guard, creates the directory, then uses fs.lstatSync(dir) and rejects symbolic-link terminal directories before chmod/write. (src/infra/exec-approvals.ts:417, 44ec7580e2f0)
  • PR follows terminal symlinks: The diff replaces the terminal lstatSync/isSymbolicLink() rejection with fs.statSync(dir) and a directory-only check, accepting any terminal symlink whose target is a directory. (src/infra/exec-approvals.ts:429, 229add945150)
  • Parent guard does not validate outside-root terminal symlinks: @openclaw/[email protected] returns without walking when allowOutsideRoot is true and the target is outside the root; it only skips a root-child symlink when allowRootChildSymlink is set. (@openclaw/[email protected]/package/dist/symlink-parents.js:9)
  • Sibling migration path keeps terminal symlink rejection: The exec approvals migration path still checks symlink parents and then rejects a symlinked target directory with lstatSync before writing migrated approval state. (src/infra/state-migrations.ts:4847, 44ec7580e2f0)
  • Latest release still has current rejection: Tag v2026.6.11 contains the same lstatSync/isSymbolicLink() terminal directory rejection, so the requested behavior is not implemented in the latest release. (src/infra/exec-approvals.ts:417, e085fa1a3ffd)

Likely related people:

  • vincentkoc: Recent merged history includes state-dir-aware exec approvals and trusted OPENCLAW_HOME symlink work in the same files. (role: recent area contributor; confidence: high; commits: adad27d7448e, 364d49889e67, 4d6593642e5a; files: src/infra/exec-approvals.ts, src/infra/exec-approvals-store.test.ts, src/infra/state-migrations.ts)
  • Takhoffman: Merged history shows the local exec-policy CLI and hardened approvals write path were added in the feature that shares this approvals store. (role: introduced affected CLI surface; confidence: high; commits: 4bf94aa0d669; files: src/cli/exec-policy-cli.ts, src/infra/exec-approvals.ts, src/infra/exec-approvals-store.test.ts)
  • steipete: Path history shows fs-safe extraction, exec approvals test hardening, and adjacent filesystem safety work around this boundary. (role: adjacent filesystem-safety contributor; confidence: medium; commits: 538605ff44d2, 45409a3ea1f7, 6851dc9505f1; files: src/infra/fs-safe-advanced.ts, src/infra/exec-approvals.ts, src/infra/exec-approvals-store.test.ts)
  • jesse-merhi: Recent merged history rewired command authorization through the exec approvals path and refreshed related coverage. (role: recent adjacent exec approvals contributor; confidence: medium; commits: c9707ab635b9; files: src/infra/exec-approvals.ts, src/infra/exec-approvals-store.test.ts, src/node-host/invoke-system-run.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P1 High-priority user-facing bug, regression, or broken workflow. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. labels Jun 23, 2026
@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Jul 16, 2026
@openclaw-barnacle

Copy link
Copy Markdown

Closing due to inactivity.
If you believe this PR should be revived, post in #clawtributors on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.

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

Labels

merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. P1 High-priority user-facing bug, regression, or broken workflow. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: XS stale Marked as stale due to inactivity status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant