Skip to content

CRW-631: open hook trust manifest and hook files inside an os.Root - #585

Merged
thisisjun786 merged 3 commits into
devfrom
codex/crw-631-hooktrust-root-open
Oct 5, 2026
Merged

thisisjun786 merged 3 commits into
devfrom
codex/crw-631-hooktrust-root-open

Conversation

@thisisjun786

@thisisjun786 thisisjun786 commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

What this changes

hookTrustEntriesReadContained (the reader behind ListHookTrustEntries, #567) read a plugin's manifest and hook files after a path check. It opened the file by its resolved path, then resolved the path a second time and compared the two observations with os.SameFile. A link swapped to a file outside the plugin root at the open and again at the stat made both observations that outside file, so its content was read and listed.

The reader now resolves the plugin root once, opens it with os.OpenRoot, opens the manifest or hook file through the Root, checks the opened handle with fstat and reads from that handle. The second resolution is removed.

Expected behaviour (acceptance criteria)

  • A link that stays inside the plugin root is followed, as before. A link that leaves the root at the moment of the open is refused with the oracle's text plugin manifest symlink escapes plugin root: <ref>, however it is swapped around the check.
  • The oracle's other texts and their order are unchanged: an absolute reference plugin manifest path must be relative: <ref>, a lexical escape plugin manifest path escapes plugin root: <ref> (checked before the open, as before); all other failures are engine errors. The ListHookTrustEntries signature and texts are unchanged.
  • The opened file is checked with fstat: a directory is EISDIR, any other file that is not regular is refused; the content is read from the opened handle.
  • The 206 recorded oracle cases of entries-oracle.json, the existing swap test and every other existing test pass unedited. No Node, no CGO, no init() and no package-level initializer that does work.

Ported from

CXC v0.2.40 (commit 3c1459ac), plugins/codexclaw/components/cxc-ops/src/hook-trust.ts lines 140-156 (normalizeHookPath, containedPluginFile) and 162-216 (listHookEntries); parity is kept except for the security fix, which was already recorded as port: fixed for this function (an earlier review finding) and is strengthened here. docs/port-cxc/known-defects.md: the fix line is rewritten and a new last section records what is new.

Evidence

Red before, green after. The new table test (internal/runtime/doctor/hooktrust_entries_race_test.go, package doctor) drives hookTrustEntriesReadContained through an unexported trailing parameter observe func(stage string) that is called with "open" just before the open (nil in production). Row 1 replays the finding: a regular file at the first resolution, a link to the outside file at the open, the regular file at the re-resolution, the link again at the stat.

  • On the old function body with the seam only (four stage calls added: resolve, open, reresolve, stat), the test as first written (9 rows) failed: read the file outside the plugin root (error <nil>) for row 1, and the absolute-link row read "inside" where the escape is now expected; the named-pipe row was skipped there because the old body would block on it.
  • With the Root body all 14 rows pass. Five rows were added after reviews of the finished change: a link that leaves the root and returns, a dangling link outside the root, a chain of 8 links followed, a chain of 9 refused with ELOOP, and (Codex review, see below) a plugin root that can be searched but not read. A mutation that drops the observe call fails the swap rows (the seam never observed the open).

Commands, all with temporary HOME, CODEX_HOME and CRW_HOME, GOFLAGS=-p=4, run on the head 1dcca3e (exit codes 0):

  • go test -count=1 ./internal/runtime/doctor and, once, go test -race -count=1 ./internal/runtime/doctor
  • go test -count=1 -run TestHookTrustEntriesReadContained_rootOpen -v ./internal/runtime/doctor (14 of 14 rows)
  • go test -count=1 ./internal/contracttest -run 'TestDomain/cxc'
  • go vet ./..., GOOS=darwin go vet ./..., go run honnef.co/go/tools/cmd/staticcheck ./internal/runtime/doctor, CGO_ENABLED=0 go build ./...
  • find . -name '*.go' -not -path './.git/*' -exec gofmt -l {} + (no output), git diff --check and git diff --check origin/dev...HEAD
  • go run -tags dev ./cmd/crw-dev ci plugin (no --record-version): package digest 635105c68ea07f89, 216 files, unchanged from the base; ci validate and ci contracts pass
  • a merge-equivalent of this change on a later dev tip builds and its internal/runtime/doctor tests pass (the sibling change that calls ListHookTrustEntries is on that tip)

Hosted CI and the external reviews are listed at the end of this description.

Corpus fixtures (c3)

The fixtures that reach this unit, with their entry points. This PR alone drives none of them (the reader is an internal function; the commands are other changes' work), so it claims none: contract/notes/cxc/CRW-631.json has empty identical and intentionally-changed lists, like other notes files of this kind.

Fixture Entry point Passes with this PR alone?
cli__hooks__retrust_needs_bootstrap_then_writes crw hooks retrust no, stays pending
cli__hooks__retrust_safety_pin_refuses_all_drifted crw hooks retrust no, stays pending
cli__doctor__codex_bin_and_surface_env crw doctor no, stays pending

The unit's own replay is the recorded internal/runtime/doctor/testdata/hooktrust/entries-oracle.json (206 sub-tests), unedited.

Diff size

236 changed lines (212 added, 24 removed) against the base: implementation 60 (hooktrust_entries.go +38/-22), tests 166, documentation 9 (known-defects.md +7/-2), notes 1. No generated data, no verbatim copies, no moved files.

Defects and limits (also in known-defects.md)

  • port: kept: the plugin root path is the anchor of the containment. It is resolved once with EvalSymlinks and opened once with os.OpenRoot, which follows links in the root's own name, so a link swapped into a parent directory of the root between those two calls makes the Root hold another directory. Once the root is open no swap under it leads outside it. The oracle has the same window. Closing it needs a descriptor walk that follows no link (follow-up).
  • port: fixed, consequences of the security fix: the Root refuses an absolute link even when it points inside the root, a relative link that leaves the root and comes back, a chain of more than 8 links (the earlier resolution followed up to 255), a dangling link that points outside the root reports the escape where the oracle reports ENOENT, a file that is not regular is refused, and a plugin root that the process may search but not read (mode 0111) is refused with the permission error because os.OpenRoot opens the directory for reading (found by the Codex review). The recorded cases use relative in-root links only and pass unchanged.
  • port: kept: a FIFO that nothing holds open for writing still blocks the open itself (already recorded; the test holds the pipe open to reach the refusal).
  • A comment in the existing hooktrust_entries_test.go (above the swap test) still describes the removed re-resolution. Existing tests are not edited by this change; a follow-up can fix the comment.
  • A sibling note: known-defects.md takes appended sections from several changes, so this branch may show a conflict at the end of that file; it is an append-only conflict.

Out of scope

The activation surface (plugins/crw/wiring/hooks, plugins/crw/skills, every .codex-plugin directory) is untouched (the plugin digest above is unchanged). No other package, existing test or fixture, go.mod, and no installation, merge or release.

Review process disclosure

The plan was audited by an independent reviewer in two rounds and the finished change by a third review, all read-only. Round 1 returned FAIL (a non-regular-file path the plan had excluded, verifiers that did not observe the change, and a document that mixed research with the implementation design); all were folded. The follow-up of that reviewer then ended in a provider refusal (code cyber_policy, reported and left as recorded), the next managed dispatch started with a configured model that the host's spawn_agent rejected (no child created, reported and left as recorded), and the global role settings were not changed. Round 2 and the check review therefore ran as explicit model overrides outside the managed dispatch path, with the same model that had served round 1 (gpt-6-astra, xhigh); the architect consultation and round 1 ran through the managed dispatch.

Hosted CI and external reviews

  • Hosted CI, head 1dcca3efbe98e6ed50426b471a18971b4e7aae2f: workflow run 37285087092 (CRW CI, event workflow_dispatch, attempt 1; dispatched on the exact head because this branch conflicts with dev only at the end of known-defects.md, an append-only conflict, so GitHub starts no pull_request run). All ten jobs succeeded: validate, secrets, go-product (lint), go-product (test-1) to (test-4), go-product (test-rest), go-product (dist) and dev-gate. The first head fbb33900 had passed the same ten jobs in run 37283737480.
  • Devin Review: "No Issues Found" (status "Completed analysis in 1m 9s", 2026-10-05 08:28 UTC) on the first head fbb33900; it reviews a pull request once, so the later head has no Devin run.
  • Codex Code Review: completed on fbb33900 (08:37 UTC) with one P2 inline comment, a plugin root that can be searched but not read (verified; recorded as the sixth difference in known-defects.md, pinned by a test row in 1dcca3e, replied to and resolved). Codex Security Review: skipped, "You have reached your Codex usage limits for security reviews."

Devin Review

hookTrustEntriesReadContained checked a path, opened it by its resolved path,
then resolved the path a second time and compared the two observations with
os.SameFile. A link swapped to a file outside the plugin root at the open and
again at the stat made both observations the outside file, so it was read and
listed.

Resolve the plugin root once, open it with os.OpenRoot and open the manifest
or hook file through the Root: a link that stays inside the root is followed,
one that leaves it is refused at the open with the oracle's text. The opened
handle is checked with fstat (a directory is EISDIR, any other non-regular
file is refused) and read from that handle; the second resolution is gone.

The Root is narrower than the old resolution (an absolute link, a link that
leaves the root and returns, and a chain of more than 8 links are refused; a
dangling link that points outside reports the escape), which is recorded in
docs/port-cxc/known-defects.md with the one window that stays: the plugin
root's own path is resolved once and opened once.

An unexported observe parameter is the test seam of the new table test, which
also holds the double swap of the finding.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for security reviews. Please try again later.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-05T08:37:32.230870Z fbb3390 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

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

Copy link
Copy Markdown

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: fbb33900da

ℹ️ 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 thread internal/runtime/doctor/hooktrust_entries.go
A review of the Root-based reader found that os.OpenRoot opens the plugin root
directory for reading, so a root that the process may search but not read
(mode 0111) is refused with a permission error, where opening the file by its
path needs search permission only. This follows from using os.Root; record it as
the sixth difference in docs/port-cxc/known-defects.md and pin it with a row of
the table test (skipped when the tests run as root).
The coordinator refreshed the base. The only conflict was in docs/port-cxc/known-defects.md, where both sides
appended lines at the same place (1 block(s)); both sets are kept, dev's lines first, then this branch's.
@thisisjun786
thisisjun786 merged commit 25c80a9 into dev Oct 5, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant