Skip to content

fix(sandbox): handle existing directories in mkdirp boundary checks#31547

Merged
steipete merged 3 commits intoopenclaw:mainfrom
stakeswky:fix/sandbox-mkdirp-boundary-31438
Mar 2, 2026
Merged

fix(sandbox): handle existing directories in mkdirp boundary checks#31547
steipete merged 3 commits intoopenclaw:mainfrom
stakeswky:fix/sandbox-mkdirp-boundary-31438

Conversation

@stakeswky
Copy link

@stakeswky stakeswky commented Mar 2, 2026

Summary

  • fix sandbox mkdirp boundary validation to handle pre-existing directory targets
  • keep strict boundary failures for non-path errors unless target is an existing directory
  • preserve canonical mount and writability checks after boundary validation

Root cause

mkdirp uses openBoundaryFile for boundary checks. openBoundaryFile is optimized for open-verified paths and can return non-path validation failures for directory targets in some mounted workspace flows. The bridge treated these as hard failures, so mkdir -p failed even when the target directory already existed and was in-bounds.

Fix

When allowedType === "directory" and boundary open returns a non-path failure, fallback to a host-side statSync check for an existing directory. If it is a directory, continue; otherwise preserve the original failure behavior.

Testing

  • pnpm vitest run src/agents/sandbox/fs-bridge.test.ts -t "allows mkdirp for existing in-boundary subdirectories|rejects mkdirp when target exists as a file|rejects pre-existing host symlink escapes before docker exec"

Closes #31438

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 2, 2026

Greptile Summary

Fixed mkdirp boundary validation to handle pre-existing directories by adding a fallback when openBoundaryFile returns non-path failures.

How it works:

  • When allowedType === "directory" and boundary open fails with a non-path error (validation/io), the code now falls back to checking if the target is an existing directory on the host
  • If it is a directory, the operation continues; otherwise the original error is thrown
  • Canonical mount and writability checks still execute after the fallback, maintaining security

Key changes:

  • Added directory-specific fallback in assertPathSafety (lines 270-294) that uses fs.statSync to verify existing directories
  • Preserves strict boundary checking for files while avoiding false negatives for directory operations

The fix correctly addresses the issue where mkdirp failed on existing in-boundary directories due to openVerifiedFileSync being optimized for file operations. Tests verify the fix works for existing subdirectories while still rejecting files.

Confidence Score: 4/5

  • This PR is safe to merge with minimal risk - it fixes a real bug while maintaining security boundaries
  • The fix correctly addresses the mkdirp issue for existing directories while preserving security checks (lexical and canonical boundary validation still execute). Tests verify the behavior. One minor style issue with error handling logic could be clearer but doesn't affect correctness.
  • No files require special attention - the changes are focused and well-tested

Last reviewed commit: 0e455d9

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +274 to +287
try {
const st = fs.statSync(target.hostPath);
if (!st.isDirectory()) {
throw new Error(
`Sandbox boundary checks failed; cannot ${options.action}: ${target.containerPath}`,
);
}
} catch {
throw guarded.error instanceof Error
? guarded.error
: new Error(
`Sandbox boundary checks failed; cannot ${options.action}: ${target.containerPath}`,
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

confusing error handling - the thrown error on line 277 is immediately caught and replaced

The current implementation throws an error when !st.isDirectory() but that error is immediately caught by the catch block and replaced with guarded.error. Consider simplifying:

Suggested change
try {
const st = fs.statSync(target.hostPath);
if (!st.isDirectory()) {
throw new Error(
`Sandbox boundary checks failed; cannot ${options.action}: ${target.containerPath}`,
);
}
} catch {
throw guarded.error instanceof Error
? guarded.error
: new Error(
`Sandbox boundary checks failed; cannot ${options.action}: ${target.containerPath}`,
);
}
let isDirectory = false;
try {
const st = fs.statSync(target.hostPath);
isDirectory = st.isDirectory();
} catch {
// statSync failed, treat as not a directory
}
if (!isDirectory) {
throw guarded.error instanceof Error
? guarded.error
: new Error(
`Sandbox boundary checks failed; cannot ${options.action}: ${target.containerPath}`,
);
}

This makes the intent clearer: check if the path is an existing directory, and if not, throw the original boundary check error.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/sandbox/fs-bridge.ts
Line: 274-287

Comment:
confusing error handling - the thrown error on line 277 is immediately caught and replaced

The current implementation throws an error when `!st.isDirectory()` but that error is immediately caught by the catch block and replaced with `guarded.error`. Consider simplifying:

```suggestion
          let isDirectory = false;
          try {
            const st = fs.statSync(target.hostPath);
            isDirectory = st.isDirectory();
          } catch {
            // statSync failed, treat as not a directory
          }
          if (!isDirectory) {
            throw guarded.error instanceof Error
              ? guarded.error
              : new Error(
                  `Sandbox boundary checks failed; cannot ${options.action}: ${target.containerPath}`,
                );
          }
```

This makes the intent clearer: check if the path is an existing directory, and if not, throw the original boundary check error.

How can I resolve this? If you propose a fix, please make it concise.

@steipete steipete force-pushed the fix/sandbox-mkdirp-boundary-31438 branch from 0e455d9 to 061121f Compare March 2, 2026 15:54
@aisle-research-bot
Copy link

aisle-research-bot bot commented Mar 2, 2026

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

# Severity Title
1 🔵 Low Sandbox boundary validation bypass via directory stat() fallback in assertPathSafety

1. 🔵 Sandbox boundary validation bypass via directory stat() fallback in assertPathSafety

Property Value
Severity Low
CWE CWE-862
Location src/agents/sandbox/fs-bridge.ts:268-281

Description

SandboxFsBridgeImpl.assertPathSafety uses openBoundaryFile(...) as a boundary/alias guard before executing filesystem-affecting commands inside the sandbox container.

A new fallback treats certain openBoundaryFile failures as non-fatal when allowedType === "directory" by checking only fs.statSync(hostPath).isDirectory().

Problem:

  • The fallback triggers for any openBoundaryFile failure whose reason !== "path" (i.e., it includes "validation" failures, not just IO failures).
  • "validation" failures can represent security-relevant boundary rejections (e.g., path not within the mount root, or alias/symlink canonicalization escaping the boundary).
  • fs.statSync() does not enforce the boundary root constraint and follows symlinks.

As a result, an attacker can potentially bypass boundary validation for directory operations (currently mkdirp) whenever hostPath happens to be an existing directory, even if openBoundaryFile rejected it.

Vulnerable code (introduced in this diff):

if (!guarded.ok) {
  if (guarded.reason !== "path") {
    const canFallbackToDirectoryStat =
      options.allowedType === "directory" && this.pathIsExistingDirectory(target.hostPath);
    if (!canFallbackToDirectoryStat) {
      throw guarded.error instanceof Error
        ? guarded.error
        : new Error(
            `Sandbox boundary checks failed; cannot ${options.action}: ${target.containerPath}`,
          );
    }
  }
}

Security impact:

  • inputs: filePath provided to mkdirp()
  • guard bypass: openBoundaryFile failure with reason: "validation" becomes non-fatal if statSync() says directory
  • sink: runCommand('... mkdir -p -- "$1"') executes after the weakened check

This broad fallback should be limited to the specific platform-IO case it was meant to address; otherwise it undermines boundary enforcement for directories.

Recommendation

Limit the fallback to the intended IO-only directory-open incompatibility case, and (optionally) to mkdirp explicitly.

Suggested change:

if (!guarded.ok) {
  if (guarded.reason === "io" && options.action === "create directories" && options.allowedType === "directory") {
    if (!this.pathIsExistingDirectory(target.hostPath)) {
      throw guarded.error instanceof Error ? guarded.error : new Error(...);
    }
  } else if (guarded.reason !== "path") {
    throw guarded.error instanceof Error ? guarded.error : new Error(...);
  }
}

This preserves support for platforms that cannot openSync() directories while ensuring that boundary validation failures are never bypassed.


Analyzed PR: #31547 at commit 061121f

Last updated on: 2026-03-02T17:10:30Z

@steipete steipete merged commit 44270c5 into openclaw:main Mar 2, 2026
10 checks passed
@steipete
Copy link
Contributor

steipete commented Mar 2, 2026

Landed via temp rebase onto main.

  • Gate: pnpm test src/agents/sandbox/fs-bridge.test.ts (full gate blocked by pre-existing main failure in extensions/zalouser/src/monitor.ts:141)
  • Land commit: 061121f
  • Merge commit: 44270c5

Thanks @stakeswky!

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 061121f2b1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +273 to +274
const canFallbackToDirectoryStat =
options.allowedType === "directory" && this.pathIsExistingDirectory(target.hostPath);

Choose a reason for hiding this comment

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

P1 Badge Restrict mkdirp fallback to I/O-only boundary failures

The new fallback now runs for every non-path failure, but openBoundaryFile also reports symlink/hardlink boundary violations as reason: "validation"; with options.allowedType === "directory", pathIsExistingDirectory uses statSync (follows symlinks) and can turn those validation failures into success. This means mkdirp can bypass host-side alias boundary rejection and proceed to Docker execution for paths that were explicitly rejected by boundary validation, which weakens the sandbox safety model for pre-existing directory aliases. The fallback should be limited to the platform I/O case (reason === "io") that this fix is targeting.

Useful? React with 👍 / 👎.

Linux2010 pushed a commit to Linux2010/openclaw that referenced this pull request Mar 2, 2026
mrosmarin added a commit to mrosmarin/openclaw that referenced this pull request Mar 2, 2026
* main: (174 commits)
  refactor(gateway): unify control-ui and plugin webhook routing
  fix(exec): resolve PATH key case-insensitively for Windows pathPrepend (openclaw#25399) (openclaw#31879)
  fix(tsgo): unblock baseline type errors (openclaw#31873)
  fix(security): harden sms.send dangerous-node defaults
  fix(gateway): let POST requests pass through root-mounted Control UI to plugin handlers
  fix(browser): fail closed navigation guard with env proxy
  test(perf): reduce timer teardown overhead in cron issue regressions
  refactor: split browser context/actions and unify CDP timeout policy
  test(perf): cache redact hints and tune guardrail scan concurrency
  docs(changelog): credit sessions_spawn agentId validation fix (openclaw#31381)
  fix(agents): validate sessions_spawn agentId format (openclaw#31381)
  fix(agents): add strict format validation to sessions_spawn for agentId
  fix(logging): log timestamps use local time instead of UTC (openclaw#28434)
  test(perf): remove redundant module reset in system presence version tests
  test(perf): avoid module reload churn in config guard tests
  fix(gateway): fail closed plugin auth path canonicalization
  docs(changelog): credit sandbox mkdirp boundary fix (openclaw#31547)
  fix(sandbox): allow mkdirp boundary checks on existing directories (openclaw#31547)
  fix(sandbox): allow mkdirp boundary check on existing directories
  fix: preserve dns pinning for strict web SSRF fetches
  ...
execute008 pushed a commit to execute008/openclaw that referenced this pull request Mar 2, 2026
execute008 pushed a commit to execute008/openclaw that referenced this pull request Mar 2, 2026
dawi369 pushed a commit to dawi369/davis that referenced this pull request Mar 3, 2026
OWALabuy pushed a commit to kcinzgg/openclaw that referenced this pull request Mar 4, 2026
zooqueen pushed a commit to hanzoai/bot that referenced this pull request Mar 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Sandbox boundary checks fail on mkdirp: "cannot create directories: /workspace"

2 participants