Skip to content

fix(plugins): distinguish missing entry file from path-escape security violation#52491

Merged
steipete merged 2 commits intoopenclaw:mainfrom
hclsys:fix/plugin-discovery-enoent-not-security
Mar 22, 2026
Merged

fix(plugins): distinguish missing entry file from path-escape security violation#52491
steipete merged 2 commits intoopenclaw:mainfrom
hclsys:fix/plugin-discovery-enoent-not-security

Conversation

@hclsys
Copy link
Copy Markdown
Contributor

@hclsys hclsys commented Mar 22, 2026

lobster-biscuit

Closes #52445

Problem

Gateway enters a fatal crash loop during startup when any extension entry point file is missing. The error message says "extension entry escapes package directory" but the file is simply not found (ENOENT) — not a security violation.

This affects AUR/Arch Linux builds and any environment where built-in extensions may be partially installed.

Root cause

src/plugins/discovery.ts:478resolvePackageEntrySource() checks !opened.ok but never inspects opened.reason. openBoundaryFileSync returns SafeOpenSyncResult with reason: "path" for ENOENT and reason: "validation" for actual path-escape violations (safe-open-sync.ts:4).

All failures were treated as security violations, aborting the gateway.

User impact

Gateway cannot start at all. Crash loop on every startup attempt. No workaround except manually creating missing entry files.

Fix

Only emit the security diagnostic when opened.reason === "validation". For "path" (ENOENT) or "io" errors, return null to skip the entry silently.

1 file, +4 lines.

How to verify

  1. Remove or rename an extension entry point file (e.g. extensions/acpx/index.js)
  2. Start gateway
  3. Before: crash loop with "escapes package directory". After: extension skipped, gateway starts.

Tests

The SafeOpenSyncResult type and openBoundaryFileSync are covered by existing tests in safe-open-sync.test.ts. This change is a call-site guard — the function behavior is unchanged.

🤖 Generated with Claude Code

resolvePackageEntrySource() treats all openBoundaryFileSync failures
as path-escape security violations. When an extension entry file is
simply missing (ENOENT, reason="path"), the gateway emits "extension
entry escapes package directory" and aborts — crashing in a loop.

Root cause: src/plugins/discovery.ts:478 checks !opened.ok but never
inspects opened.reason. SafeOpenSyncResult already distinguishes
"path" (ENOENT) from "validation" (actual path escape).

Fix: only push the security diagnostic when opened.reason is
"validation". For "path" or "io" failures, return null to skip the
entry silently — a missing file is not a security violation.

Closes openclaw#52445

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: HCL <[email protected]>
@hclsys
Copy link
Copy Markdown
Contributor Author

hclsys commented Mar 22, 2026

@vincentkocdiscovery.ts:478 treats ENOENT as path-escape security violation → gateway crash loop when extension entry file is missing. Fix: check opened.reason === "validation" before emitting security diagnostic. SafeOpenSyncResult already encodes the distinction. 1 file, +4 lines. Closes #52445.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 22, 2026

Greptile Summary

This PR fixes a crash-loop on gateway startup by correctly distinguishing a missing plugin entry file (ENOENT → reason: "path") from a genuine path-escape security violation (reason: "validation") in resolvePackageEntrySource. The root cause analysis is accurate: openBoundaryFileSync propagates SafeOpenSyncFailureReason faithfully, and resolveBoundaryPathSync only throws (→ "validation") for actual boundary escapes, while openVerifiedFileSync maps ENOENT to "path" via isExpectedPathError. The one-liner guard is minimal, targeted, and correct.

  • Fix: Only emits the "extension entry escapes package directory" error diagnostic when opened.reason === "validation"; all other failure kinds return null silently, allowing the gateway to skip the missing extension and proceed.
  • Minor: Unexpected I/O errors (reason: "io") are now silently swallowed along with ENOENT. A "warn"-level diagnostic for "io" failures would help operators detect disk/permission problems without reintroducing the crash behaviour.

Confidence Score: 5/5

  • Safe to merge — the fix is minimal, targeted, and correctly narrows the security diagnostic to actual path-escape violations.
  • The change is a single 4-line guard that is verifiably correct given the full call chain: ENOENT produces reason: "path" via isExpectedPathError in openVerifiedFileSync, while path-escape errors throw in resolveBoundaryPathSync and are caught as reason: "validation" in resolveBoundaryFilePathGeneric. Security properties are preserved. The only non-blocking follow-up is optionally surfacing "io" failures as warnings rather than silently swallowing them.
  • No files require special attention.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/plugins/discovery.ts
Line: 479-482

Comment:
**Consider warning on unexpected I/O errors**

`"io"` covers unexpected errors (e.g. permission denied, disk errors) that are distinct from a simple missing file (`"path"`). Silently skipping them makes disk-level problems invisible to the operator. A `"warn"` diagnostic here would preserve the non-fatal startup behaviour while still surfacing the anomaly in logs:

```suggestion
    if (opened.reason === "path") {
      // File missing (ENOENT/ENOTDIR/ELOOP) — skip silently, not a security violation.
      return null;
    }
    if (opened.reason === "io") {
      params.diagnostics.push({
        level: "warn",
        message: `extension entry could not be read (I/O error): ${params.entryPath}`,
        source: params.sourceLabel,
      });
      return null;
    }
```

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

Reviews (1): Last reviewed commit: "fix(plugins): distinguish missing entry ..." | Re-trigger Greptile

Comment on lines +479 to +482
if (opened.reason !== "validation") {
// File missing (ENOENT) or I/O error — skip silently, not a security violation.
return null;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Consider warning on unexpected I/O errors

"io" covers unexpected errors (e.g. permission denied, disk errors) that are distinct from a simple missing file ("path"). Silently skipping them makes disk-level problems invisible to the operator. A "warn" diagnostic here would preserve the non-fatal startup behaviour while still surfacing the anomaly in logs:

Suggested change
if (opened.reason !== "validation") {
// File missing (ENOENT) or I/O error — skip silently, not a security violation.
return null;
}
if (opened.reason === "path") {
// File missing (ENOENT/ENOTDIR/ELOOP) — skip silently, not a security violation.
return null;
}
if (opened.reason === "io") {
params.diagnostics.push({
level: "warn",
message: `extension entry could not be read (I/O error): ${params.entryPath}`,
source: params.sourceLabel,
});
return null;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugins/discovery.ts
Line: 479-482

Comment:
**Consider warning on unexpected I/O errors**

`"io"` covers unexpected errors (e.g. permission denied, disk errors) that are distinct from a simple missing file (`"path"`). Silently skipping them makes disk-level problems invisible to the operator. A `"warn"` diagnostic here would preserve the non-fatal startup behaviour while still surfacing the anomaly in logs:

```suggestion
    if (opened.reason === "path") {
      // File missing (ENOENT/ENOTDIR/ELOOP) — skip silently, not a security violation.
      return null;
    }
    if (opened.reason === "io") {
      params.diagnostics.push({
        level: "warn",
        message: `extension entry could not be read (I/O error): ${params.entryPath}`,
        source: params.sourceLabel,
      });
      return null;
    }
```

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

Copy link
Copy Markdown

@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: 4535ba1352

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +479 to +481
if (opened.reason !== "validation") {
// File missing (ENOENT) or I/O error — skip silently, not a security violation.
return null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve diagnostics for I/O failures opening plugin entries

openVerifiedFileSync() intentionally distinguishes missing paths from real filesystem failures by returning reason: "io" for cases like EACCES/EMFILE (src/infra/safe-open-sync.ts:84-88), but this branch now collapses those into a silent null. Both discovery callers treat null as “skip this plugin” (src/plugins/discovery.ts:564-595 and 694-725), so a configured plugin package with an unreadable entry file now just disappears with no diagnostic at all. The ENOENT fix makes sense, but swallowing non-path errors regresses debuggability for genuine startup failures.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a87ccfa. Now handles all 3 reasons explicitly: path → silent null (ENOENT), io → warn diagnostic (EACCES/EMFILE surfaced without abort), validation → error diagnostic (security). I/O failures are no longer invisible.

Comment on lines +479 to +481
if (opened.reason !== "validation") {
// File missing (ENOENT) or I/O error — skip silently, not a security violation.
return null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restrict missing-entry suppression to partially installed bundles

This return null also applies when the user explicitly points OpenClaw at a package directory via extraPaths/config. In that path, if package.json declares openclaw.extensions but the built entry file is missing, discoverFromPath() now returns with no candidates and no error (src/plugins/discovery.ts:694-725), which turns a broken third-party package or typo into a silent no-op. The original Arch/AUR case is about partially installed bundled plugins, but applying the same ENOENT suppression to all package origins hides actionable misconfiguration elsewhere.

Useful? React with 👍 / 👎.

Address Codex P1 + Greptile P2: the previous commit collapsed both
"path" (ENOENT) and "io" (EACCES/EMFILE) into silent null returns.

Now:
- reason="path" (missing file): return null silently — not a security issue
- reason="io" (permission/disk): push warn diagnostic — surface anomaly
  without aborting gateway
- reason="validation" (path escape): push error diagnostic — security violation

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: HCL <[email protected]>
Copy link
Copy Markdown

@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: a87ccfaa3f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +479 to +481
if (opened.reason === "path") {
// File missing (ENOENT) — skip, not a security violation.
return null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restrict missing-entry suppression to ENOENT

opened.reason === "path" is broader than the missing-file case this fix is targeting: openVerifiedFileSync() also maps ENOTDIR and ELOOP into "path" (src/infra/safe-open-sync.ts:17-20,84-88). With this branch, a package entry like ./dist/index.js now disappears silently if dist is accidentally a file or a symlink loop, instead of producing any diagnostic. That regresses debuggability for malformed packages; only plain ENOENT should be skipped here.

Useful? React with 👍 / 👎.

@steipete steipete merged commit 4f11982 into openclaw:main Mar 22, 2026
37 of 41 checks passed
@steipete
Copy link
Copy Markdown
Contributor

Landed via temp rebase onto main.

  • Gate: pnpm check && pnpm build && pnpm test
  • Follow-up verify after final test-only rebase fix: pnpm build && pnpm test -- src/gateway/server.chat.gateway-server-chat.test.ts src/plugins/conversation-binding.test.ts
  • Land commits: d73852ff21, 63130118ba
  • Merge commit: 4f11982ae6

Thanks @hclsys!

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug]: Gateway crash loop due to missing internal extension entry points

2 participants