Skip to content

Harden local skill scan against ancestor-directory ABA-swap TOCTOU (openat/read-sandbox) #517

Description

@Poytr1

Context

Follow-up to #506 (task #36). The local skill inventory (GET /agents/:id/skills/local, daemon/src/skills/local-skill-inventory.ts) scans the agent's workspace skill roots for SKILL.md metadata. #506 already hardened it: symlink confinement via realpath, O_NOFOLLOW + before/opened/after fstat identity + hard-link rejection (readBoundedFile), a leaf skill-directory inode before/after check, a scanned-entry cap, and a serialized-byte budget under the control-frame limit.

Residual (owner-accepted P2)

An ancestor-directory ABA swap remains: a same-UID racing process can rename an ancestor (e.g. .claude or the skills root) after the realpath() containment check, temporarily replace it with a symlink to a peer workspace while the manifest is read, then restore the original ancestor before the leaf before/after check. The leaf directory moves with its ancestor, so its device/inode/ctime/mtime are unchanged, and the outside manifest is returned. Closing this fully needs directory-descriptor / openat-style traversal or a read sandbox confined to the workspace realpath — neither expressible with Node's fs path API (no openat/fstatat).

Why deferred (P2)

  • Exposure is low sensitivity: skill name + description metadata only.
  • The attacker must already be a same-UID process racing the daemon read.
  • The existing shipped GET /agents/:id/workspace/file byte-read (cp/workspace-reader.ts) uses the same lexical + realpath containment and carries the same ABA residual — the skill endpoint is already more hardened than that precedent. A general fix likely belongs at the shared workspace-read layer.

Options

  1. A shared TOCTOU-safe traversal primitive (native openat/fstatat binding or a vetted dependency), used by both the workspace-file reader and the skill inventory.
  2. Run untrusted workspace reads inside the OS sandbox (SRT/bwrap) confined to the workspace realpath.

Acceptance

  • Ancestor-swap during a local-skill read cannot surface another workspace's manifest.
  • Applies consistently to the existing workspace-file byte read.

Refs: #506, packages/daemon/src/skills/local-skill-inventory.ts, packages/daemon/src/cp/workspace-reader.ts.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions