Skip to content

D062 — close ACL propagation and descriptor lifecycle gaps - #96

Closed
LogicDuke wants to merge 6 commits into
feature/d062-post-start-control-channelfrom
repair/pr84-acl-protection
Closed

D062 — close ACL propagation and descriptor lifecycle gaps#96
LogicDuke wants to merge 6 commits into
feature/d062-post-start-control-channelfrom
repair/pr84-acl-protection

Conversation

@LogicDuke

@LogicDuke LogicDuke commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Repairs the exact CURRENT Windows ACL/descriptor findings identified on PR #84.

Base / protected parent:

Repair head:

Findings closed:

  • P1: require file-inheritable ACE semantics before relying on anchor ACL inheritance
  • P1: require SE_DACL_PROTECTED on the control anchor
  • P1: prevent stale weak-DACL runtime-descriptor overwrite when unlink fails

Terminal invariants:

  • accepted anchor DACL is present, non-NULL, protected, direct-only, canonical-principal-bounded, and file-inheritable
  • a fresh runtime token is never written into an existing descriptor pathname
  • descriptor creation is exclusive
  • actual created descriptor ACL is verified before pipe construction/listen
  • runtime gains no ACL provisioning/mutation authority
  • no shell / PowerShell / cmd / generic Windows runner added

Validated patch:

  • SHA256: 98F0C8C34CB6DD337B6A484F9FEEBB3E0A13938A3A1CBAEB391D3F8A6FFBAE80
  • bytes: 65605
  • files: 7

Validation:

  • exact-final full suite: 2112 passed / 1 skipped
  • typecheck: PASS
  • lint: PASS
  • build: PASS
  • git diff --check: PASS
  • fresh independent focused validation: 80 passed
  • real Windows stale-descriptor ACL reproducer: PASS
  • fresh validator classification: all three P1s FIXED
  • combined ACL propagation/protection + descriptor-lifecycle defect class: closed

Authority:

  • no runtime ACL mutation or provisioning
  • no runtime icacls
  • no shell
  • no PowerShell/cmd
  • no generic execution authority
  • helper remains read-only and provenance-gated

This PR is intentionally Draft for exact-head CI and independent review.
Do not merge automatically.
Human merge authority remains required.

Summary by CodeRabbit

  • Security Enhancements

    • Control-channel startup now fails closed when stale descriptor cleanup, secure creation, or access-control verification fails.
    • Runtime descriptors are created exclusively with protected permissions for the operator and SYSTEM accounts.
    • Access-control validation now detects unprotected descriptors, missing inheritance, and unexpected principals.
    • Descriptor contents and permissions are independently verified before the control channel is established.
  • Reliability

    • Existing descriptor files are never overwritten, reducing the risk of stale or tampered runtime state.
  • Validation

    • Expanded coverage verifies secure creation, cleanup, ownership, permissions, provenance, and failure handling across supported Windows workflows.

@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 224bf2ca-c9d7-4e74-a4d7-90dbe64723df

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: d2f7bc00-d301-469f-b003-9c0d830a2c29

📥 Commits

Reviewing files that changed from the base of the PR and between 30fc85e and 0be6e56.

📒 Files selected for processing (15)
  • src/control/control-runtime.ts
  • src/control/control-store.ts
  • tests/control/control-runtime.test.ts
  • tests/control/control-store.test.ts
  • tests/control/launch-lifecycle.test.ts
  • tests/control/owner-helper.win.test.ts
  • tests/control/support.ts
  • tools/control-owner/agentbridge-win-descriptor-create.c
  • tools/control-owner/agentbridge-win-owner.c
  • tools/control-owner/build.mjs
  • tools/control-owner/ensure-helper.mjs
  • tools/control-owner/helper-pair.d.mts
  • tools/control-owner/helper-pair.mjs
  • tools/control-owner/msvc-toolchain.mjs
  • tools/control-owner/provenance-format.mjs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The control channel now uses a V2 ACL snapshot protocol, a separately provenanced native descriptor creator, exclusive descriptor creation, and post-creation ACL verification before channel startup.

Changes

Control descriptor security

Layer / File(s) Summary
ACL snapshot and descriptor verification
src/control/control-store.ts, tools/control-owner/agentbridge-win-owner.c, tests/control/control-store.test.ts, tests/control/owner-helper.win.test.ts
ACL snapshots now report DACL state, protection, and numeric ACE flags. Anchor and descriptor evaluation enforce protected DACLs, required principals, direct ACEs, and file inheritance.
Native creator and provenance
tools/control-owner/agentbridge-win-descriptor-create.c, tools/control-owner/build.mjs, tools/control-owner/ensure-helper.mjs, tools/control-owner/helper-pair.*, tools/control-owner/provenance-format.mjs, tools/control-owner/msvc-toolchain.mjs, tests/control/launch-lifecycle.test.ts, tests/control/control-store.test.ts, tests/control/owner-helper.win.test.ts
A native creator uses CREATE_NEW, explicit ownership, a protected operator/SYSTEM DACL, bounded stdin, and provenance validation. Build and provisioning checks manage independent owner and creator artifact pairs.
Fail-closed startup orchestration
src/control/control-runtime.ts, src/control/control-store.ts, tests/control/control-runtime.test.ts, tests/control/support.ts
Startup removes stale descriptors, creates the replacement exclusively, verifies the actual ACL, cleans rejected descriptors, and delays pipe construction until verification succeeds.

Priority: ➖ Normal

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant startControlChannel
  participant descriptorCreator
  participant descriptorFile
  participant descriptorVerifier
  participant controlPipe
  startControlChannel->>descriptorCreator: Remove stale file and create descriptor
  descriptorCreator->>descriptorFile: Write descriptor with protected ACL
  startControlChannel->>descriptorVerifier: Verify descriptor ACL
  descriptorVerifier-->>startControlChannel: Return verification result
  startControlChannel->>controlPipe: Build and listen after success
Loading

Merge Risk: ⚪ Minimal · up to 0be6e

The descriptor lifecycle and ACL hardening are consistently implemented across creation, verification, startup, provenance, and tests; no merge-blocking issue is identified.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.92% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 52 functions across 15 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: strengthening ACL propagation and descriptor lifecycle handling.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch repair/pr84-acl-protection

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit reads each line,
The patch grows clear beneath the moon,
Small changes hop in place,
Tests guard the garden path,
Reviews bloom before the dawn.

Comment @coderabbitai help to get the list of available commands.

LogicDuke and others added 3 commits September 8, 2026 20:36
Windows chooses a newly created file's OWNER from the creating token's
DEFAULT owner and its DACL from the parent's inheritable ACEs, so a
`writeFileSync` descriptor is owned by BUILTIN\Administrators under an
elevated token and carries an unprotected, inherited DACL. The exact-owner
descriptor gate correctly rejected it, leaving the control channel
unavailable on a valid anchor.

Add a second build-provenanced native artifact with create-only authority,
agentbridge-win-descriptor-create.exe, which creates exactly
<anchor>\runtime-descriptor.json with CREATE_NEW and an explicit security
descriptor: owner = its own process TokenUser SID, and a PROTECTED DACL
whose principals are exactly that operator plus SYSTEM. It derives the
operator itself, accepts no owner or filename input, receives the secret
descriptor bytes only on stdin, and is SHA-256 verified against its own
generated provenance before execution. The creator and the read-only owner
helper have separate filenames, separate provenance modules, and separate
exported bindings, so neither artifact's trust root can satisfy the other's.

Creation is never trusted on its own word: the existing READ-ONLY helper
then inspects the file that actually exists, and evaluateDescriptorSnapshot
must accept it before any pipe is created. That evaluator now also requires
a PROTECTED DACL and rejects any INHERITED_ACE, so a descriptor that could
later widen through parent inheritance is refused at verification time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxNRavn8EqdoiimAKQrcnT
The D062 createDescriptorFileNative writeFile-seam test hardcoded a
Windows separator when forming the expected descriptor path, while
production `descriptorPathFor()` uses the platform-aware `join()` from
`node:path`. On ubuntu-latest the expectation demanded
`C:\Anchor\runtime-descriptor.json` while production correctly produced
`C:\Anchor/runtime-descriptor.json`, failing the Ubuntu verify job.

Build the expectation with the same `join()` the production path helper
uses, so expected and actual are separator-identical by construction on
every platform. Test-only: production path semantics are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016SnRhGj1dW4KiYsviKo9KL
@LogicDuke

Copy link
Copy Markdown
Owner Author

@codex review

@LogicDuke

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 9, 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-09-10T07:58:03.436946Z 64845de Draft marked ready
ℹ️ 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.

@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: 0be6e5661a

ℹ️ 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 +123 to 127
if (before === null) {
// Already provisioned with VALID pairs: do not recompile on every launch. The
// runtime still hashes each binary against its generated provenance before use.
note('valid helper/provenance pairs already present; skipping build.');
process.exit(0);

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 Rebuild helpers when the snapshot protocol is stale

When dist survives a rollback or mixed-cache restore, a canonical V1 owner-helper pair can coexist with the new canonical creator pair, causing firstInvalidPair() to return null here. The runtime now accepts only AGENTBRIDGE-ACL-V2, so the old helper's output is rejected as SNAPSHOT_MALFORMED, while every subsequent control:provision invocation reports success and skips the rebuild. Include a helper protocol/source version in pair validity, or retain parser compatibility, so provisioning can repair this state.

Useful? React with 👍 / 👎.

Comment thread src/control/control-runtime.ts Outdated
descriptorAcl = await verifyDescriptor(descriptorPathFor(anchorPath), { env });
} catch {
log('AgentBridge control channel: disabled (descriptor write failed).');
removeDescriptorFile(anchorPath, deps.descriptorDeps);

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 Protect successor descriptors during ACL failure cleanup

When two runtimes overlap, a successor can remove and recreate the fixed descriptor while the first runtime's ACL query is failing; this unconditional unlink can then delete the successor's newly created descriptor and make its live channel undiscoverable. The later listen-failure and close paths already avoid this by calling removeOwnDescriptorFile with the minted pipe name, and both descriptor-verification failure paths need the same ownership check.

Useful? React with 👍 / 👎.

Comment on lines +491 to +492
if (!snapshot.daclProtected) {
return { ok: false, reason: CONTROL_ANCHOR_REJECTION.DACL_UNPROTECTED };

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 Update provisioning guidance for the stronger ACL contract

This newly rejects an unprotected anchor DACL (and below also rejects every ACE lacking OBJECT_INHERIT_ACE), but docs/architecture/D3-cockpit-dashboard-host.md still tells operators that an operator+SYSTEM, present, non-inherited DACL is sufficient. Because the repository deliberately has no anchor provisioner, an operator can follow the only documented setup procedure and still have control startup fail closed; document the protected and file-inheritable requirements alongside the external prerequisite.

Useful? React with 👍 / 👎.

Three CURRENT/P2 findings on the exact PR #96 head, repaired coherently.

F1 — provisioning could skip a stale helper forever. Lifecycle pair validity
tested only self-consistency: on-disk provenance == encode(sha256(binary)). A
canonical-but-older owner-helper surviving a rollback or mixed-cache restore
therefore satisfied the gate, which reported success on every run, while the
runtime rejected that helper's output as SNAPSHOT_MALFORMED because the parser
requires AGENTBRIDGE-ACL-V2.

Bind the reviewed build source into the canonical identity: each provenance
module now names both the binary's SHA-256 and sourceId, the SHA-256 of the exact
C source the build compiled. The accepted identity is the pair (binary, source),
so lifecycle validity implies compatibility with the runtime's current snapshot
protocol — whose only definition is that source. Nothing is read out of the
on-disk provenance to decide this; the source digest is recomputed from the
repository, so a stale helper never votes on its own currency. No regex, no
version-text parser, no execution of the on-disk binary.

provenance-format.mjs stays pure (sourceId is a parameter); helper-pair.mjs owns
the one filesystem read and the one artifact->source mapping; build.mjs imports
that same function, so producer and acceptor cannot drift. The runtime reads only
filename and sha256 and ignores other fields, so this adds no runtime authority
and leaves hash-before-exec unchanged. Termination is one rebuild: the builder
publishes exactly what the gate recomputes.

F2 — a successor runtime's descriptor could be deleted. Both descriptor-ACL
verification failure paths unlinked the fixed pathname unconditionally, so a
predecessor whose verification failed removed a successor's freshly created
descriptor and made its live channel undiscoverable. Both paths now use the
ownership-checked removeOwnDescriptorFile with the minted pipeName, exactly as
the listen-failure and close() paths already did. Startup still fails closed.

F3 — the documented anchor contract understated the code. The D3 host document
described the DACL as operator + SYSTEM, present and non-inherited, but the
evaluator also rejects an unprotected DACL and any ACE lacking OBJECT_INHERIT_ACE,
so an operator could follow the only documented procedure and still fail closed.
Replace that sentence with the full requirement/rejection table, verified against
evaluateAnchorSnapshot, including that SYSTEM is permitted rather than required
and that allow/deny type and access mask do not gate authorization.

Reviewed C sources, msvc-toolchain.mjs and control-store.ts are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016SnRhGj1dW4KiYsviKo9KL
@LogicDuke

Copy link
Copy Markdown
Owner Author

@codex 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: 0b73b4a6fd

ℹ️ 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".

let descriptorAcl: DescriptorAclVerification;
try {
writeDescriptorFile(anchorPath, descriptor, deps.descriptorDeps);
descriptorAcl = await verifyDescriptor(descriptorPathFor(anchorPath), { env });

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 Recheck descriptor ownership after ACL verification

When runtimes overlap, a successor can replace the fixed descriptor while this asynchronous verification is in flight; the helper then validates the successor's descriptor and returns ok, but the predecessor proceeds to listen on its own pipe and reports successful startup even though its token and pipe are no longer discoverable. If the successor subsequently closes or fails to listen, no descriptor remains for the predecessor's still-running channel. After a successful ACL result, re-read the descriptor and fail startup unless it still contains this runtime's minted pipeName.

Useful? React with 👍 / 👎.

Descriptor ACL verification is asynchronous and path-based: the helper re-opens
whatever file sits at the fixed descriptor path when it runs. A successor runtime
removes that pathname unconditionally before creating its own, so during the
await the helper could validate the SUCCESSOR's descriptor and return ok. The
predecessor then listened on its own pipe and returned a handle reporting a
successful startup, while the stored descriptor named the successor: the channel
was live but undiscoverable, and once the successor closed, no descriptor
remained for it at all.

Prove ownership before listening. After a successful ACL result the runtime
re-reads the current descriptor and proceeds only while it still carries this
instance's minted pipeName; otherwise startup fails closed with no handle and no
token. The ownership test is factored out of removeOwnDescriptorFile into a
single descriptorIsOurs predicate, so the cleanup paths and this pre-listen proof
decide through exactly one function and cannot drift. A missing, malformed or
unreadable descriptor is not ours: ownership is proven, never assumed.

Nothing is removed on the new path, deliberately. It is reachable only when the
descriptor is absent, unprovable, or a successor's, and deleting any of those is
precisely the successor-clobbering this ownership rule exists to prevent; a later
runtime's stale-descriptor removal reclaims an orphaned pathname.

This is distinct from the failure-path repair already on this branch: that
governed cleanup after verification failed, this governs proceeding after it
succeeded. Removing only the new check fails six of the eight added tests, which
cover the successor rotation, a missing, malformed, unreadable or foreign
descriptor, successor preservation, and the absence of any token in the failure
log. No locks, retries, polling or global state; the descriptor store is
unchanged and fail-closed startup is not weakened.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016SnRhGj1dW4KiYsviKo9KL
@LogicDuke

Copy link
Copy Markdown
Owner Author

@codex 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: 64845dedb8

ℹ️ 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 +199 to 202
if (!descriptorIsOurs(anchorPath, descriptor.pipeName, deps.descriptorDeps)) {
log('AgentBridge control channel: disabled (descriptor no longer identifies this runtime).');
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 Recheck descriptor ownership after the listen await

When a successor rotates the descriptor after this check but before the asynchronous server.listen callback resolves, this runtime still returns a successful handle even though the fixed descriptor already names the successor, leaving this channel undiscoverable. The fresh evidence beyond the prior review is that the new ownership check remains before the separate listen await at lines 212–222; recheck after listening and close/fail startup if ownership was lost.

Useful? React with 👍 / 👎.

@LogicDuke
LogicDuke marked this pull request as ready for review September 10, 2026 07:52
@LogicDuke
LogicDuke marked this pull request as draft September 10, 2026 08:01
@LogicDuke LogicDuke closed this Sep 10, 2026
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