Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions contract/notes/cxc/CRW-601.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"issue": "CRW-601",
"pending": [
"cli__doctor__codex_bin_and_surface_env",
"cli__hooks__retrust_needs_bootstrap_then_writes",
"cli__hooks__retrust_safety_pin_refuses_all_drifted"
],
"identical": [],
"intentionally-changed": []
}
10 changes: 10 additions & 0 deletions docs/port-cxc/known-defects.md
Original file line number Diff line number Diff line change
Expand Up @@ -1047,3 +1047,13 @@ Source: `plugins/codexclaw/components/subagent-config/src/fallback-dispatch.ts`
- A record, or the directory itself, swapped for a named pipe between the check and the open blocks the open for good while the dispatch lock is held (the shared reader looked at no file type on its own descriptor; recorded as port: kept in the CRW-574 section, last line, which this line replaces for the pinned reads), and a record replaced by a relative link to itself passed an lstat taken before the open; port: fixed (the pinned reader opens with O_NONBLOCK and requires the opened descriptor to be the regular file that was looked at and the name to still lead to it, and a directory is opened through `name/.`, so a pipe or file fails at once; `TestDispatchPinnedSiblingTurnedIntoPipe`, `TestDispatchPinnedRecordSwappedForLinkToItself` and the pipe rows of `TestDispatchPinnedRefusesSwapAfterCheck`).
- `crwdir.EnsureDir` still creates `.crw` and its `.gitignore` by path, so a `.crw` swapped for a link between its mkdir and the exclusive `.gitignore` write puts that one file into the linked directory; only when `.crw` did not exist, and creating a file of fixed content (source `codexclaw-dir.ts`, `internal/pabcd/crwdir/crwdir.go:25-43`); port: kept (the package is shared by every state writer and is outside this issue's edit region).
- `ManagedSpawn`, the read-only selection that the managed-spawn hook runs before it issues, still reads the record by path (`dispatchRead`: an lstat of the file and a read through `.crw/dispatches/<session>`, whose components are not checked), so a link or a named pipe put there can redirect or block that read, which writes nothing (source `fallback-dispatch.ts:242`, `internal/role/managed_spawn.go:37`); port: kept (the issuance that follows takes the pinned directory and rereads the record under the lock; a follow-up would read the selection through the pinned directory too).
## Found by the hook trust entry listing port (CRW-601)

- `listHookEntries` skips a group whose matcher `new RegExp(matcher)` refuses, which is V8's grammar; Go has only RE2, so the port accepts a matcher that `regexp.Compile` accepts and one it refuses only for lookaround or a backreference (the lookaround opener is rewritten to a plain group and the backreference to one character, then the pattern is compiled again), and the two grammars still differ. A matcher JavaScript accepts that the port skips: `[]`, `[^]`, `\u0041`, `\u{41}`, `\xZ`, `\cJ`, `\e`, `\q`, `\Z`, `\k<`, a repeat count above 1000 (`a{1001}`), a group name with a non-ASCII letter (`(?<é>x)`), a lone surrogate, and a class range that ends in an escape the rewrite turns into a smaller character (`[0-\9]`, `[\x02-\7]`, `[a-\k<z>]`). A matcher JavaScript refuses that the port keeps: inline flags (`(?i)a`, `(?s)a`), `(?P<n>a)`, duplicate group names, a quantified anchor or lookbehind (`^*`, `$*`, `\b+`, `(?<=a)*`), an unknown `\k<x>` beside a named group, and a reversed octal range (`[\3-\1]`); measured against Node 24 over 96 patterns (source `plugins/codexclaw/components/cxc-ops/src/hook-trust.ts:191-197`; recorded as the `matcher_*` cases and the fourteen `matcher_residual_*` cases of `internal/runtime/doctor/testdata/hooktrust/entries-oracle.json`, where the oracle's answer is kept and the replay expects the port's); port: kept.
- `listHookEntries` writes the event into the key through a template literal of `EVENT_LABELS[eventName]`, so an event key that `Object.prototype` defines, which the oracle's own `in` guard lets through (see the hook trust identity entry above), is spelled as the source of the inherited function, `function toString() { [native code] }` (`function Object() { [native code] }` for `constructor`), or `[object Object]` for `__proto__`, in place of an event label (source `hook-trust.ts:181` and `:206`; recorded as the `proto_*` cases); port: kept.
- A hook document whose `hooks` member is a non-empty string or array is read through `Object.entries` as an object with index keys and fails with `unsupported hook event: 0`, a number or boolean `hooks` member is silently an empty list, and a document or manifest that is JSON `null` fails with V8's raw TypeError `Cannot read properties of null (reading 'hooks')` that names no file; `Object.entries` also lists integer-like event keys before the others, so a file with an index key and another defect reports the index key first (source `hook-trust.ts:165-166` and `:177-179`; recorded as the `doc_hooks_*`, `doc_null`, `manifest_top_level_null` and `int_key*` cases); port: kept.
- `listHookEntries` strips a leading `./` and `containedPluginFile` strips another before it resolves the path, so `././x.json` is keyed `./x.json` but read from `x.json`, and `././/x.json` resolves as the absolute path `/x.json` and is refused as escaping the plugin root with the message naming `.//x.json` (source `hook-trust.ts:140-156` and `:175-176`; recorded as `ref_double_dot_slash`, `ref_triple_dot_slash_then_slash` and `ref_triple_dot_slash_then_absolute`); port: kept.
- The plugin manifest was read without the containment check the hook files get (a `.codex-plugin/plugin.json`, or a `.codex-plugin` directory, that is a symlink was followed wherever it points), and a hook file was opened after its path was checked, so a symlink swapped in between was followed too. Devin reported both as security findings on this port, so the port applies the same containment to the manifest and, once a file is open, resolves its path again and refuses a file that is not the one it opened or no longer lies inside the plugin root (source `hook-trust.ts:165` and `:175-176`; the oracle's answer for a manifest that leaves the root is recorded as the `intentionally_changed_*` cases of `internal/runtime/doctor/testdata/hooktrust/entries-oracle.json`, and the swap is covered by `TestListHookTrustEntries_swapAfterTheCheckIsNotFollowed`); port: fixed.
- A FIFO in place of the manifest or of a hook file blocks the read, in the oracle and in the port (source `hook-trust.ts:165` and `:175-176`; not recorded, a case would hang); port: kept.
- A plugin root that is the file system root rejects every reference as escaping it, because the prefix test appends the separator to a root that already ends with one and so expects `//` where every path starts with `/` (source `hook-trust.ts:149-150` and `:153-154`; not recorded, the recorder cannot create files at `/`); port: kept.
- The Go JSON reader refuses a document that nests containers more than 10,000 deep, with an error, where V8's `JSON.parse` reads a million levels (source `hook-trust.ts:165` and `:177`; probed with Node 24 and not recorded, since the fixture would be megabytes); port: kept.
Loading
Loading