Skip to content

Add identity verification step to exec policy evaluation#927

Closed
mlw wants to merge 10 commits into
mainfrom
mlw/snt-377-verify-opened-file-matches-es-event-vnode-cdhashfstat
Closed

Add identity verification step to exec policy evaluation#927
mlw wants to merge 10 commits into
mainfrom
mlw/snt-377-verify-opened-file-matches-es-event-vnode-cdhashfstat

Conversation

@mlw

@mlw mlw commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Also includes a MOLCodesignChecker change: when a file descriptor is supplied to initWithBinaryPath:fileDescriptor:, route SecStaticCodeCreateWithPath through /dev/fd/N so the static code ref operates on the caller's vnode. This avoids a redundant path resolution and aligns disk-side identity with the bytes the caller already has open.

@mlw mlw added this to the 2026.4 milestone Apr 26, 2026
@github-actions github-actions Bot added comp/santad Issues or PRs related to the daemon comp/gui Issues or PRs related to the Santa GUI comp/santasyncservice Issues or PRs related to the sync protocol lang/objc++ PRs modifying files in ObjC++ comp/common size/l Size: large labels Apr 26, 2026
@mlw mlw force-pushed the mlw/snt-377-verify-opened-file-matches-es-event-vnode-cdhashfstat branch from f0e7396 to 6299971 Compare April 26, 2026 02:01
@coderabbitai

coderabbitai Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

This PR implements binary identity verification between Endpoint Security process metadata and on-disk code-signing information. It extends MOLCodesignChecker to support CPU type hints for per-slice validation in universal binaries, introduces file-descriptor-based initialization, and integrates identity comparison into SNTPolicyProcessor's decision flow, emitting a new SNTEventStateBlockBinaryMismatch decision on mismatch.

Changes

Cohort / File(s) Summary
MOLCodesignChecker enhancements
Source/common/MOLCodesignChecker.h, Source/common/MOLCodesignChecker.mm, Source/common/MOLCodesignCheckerTest.mm
Added CPU type/subtype hint parameters and fd-based initializer; per-slice signing dictionary lookup for fat binaries; suppressed whole-binary fallback when slice hints active; extended test coverage for fd vnode behavior, atomic renames, bundle binaries, and slice selection logic.
SNTFileInfo ES exec event support
Source/common/SNTFileInfo.h, Source/common/SNTFileInfo.mm, Source/common/SNTFileInfoTest.mm
New initWithEndpointSecurityExecEvent: initializer to capture CPU type/subtype from ES exec events; conditional fd+hint passing to MOLCodesignChecker; added cpuType/cpuSubtype properties; test coverage for CPU initialization and fd offset behavior during codesign validation.
ScopedFile temp file management
Source/common/ScopedFile.h, Source/common/ScopedFileTest.mm
Added size and keep_path parameters to CreateTemporary; path-retention logic via new path_ member; updated move semantics; added Path() accessor; test coverage for file sizing and path persistence.
SNTEventStateBlockBinaryMismatch enum and UI
Source/common/SNTCommonEnums.h, Source/common/SNTBlockMessage.mm, Source/common/santa.proto
New enum value SNTEventStateBlockBinaryMismatch (1ULL << 25); switch case in block reason mapper; new REASON_BINARY_MISMATCH proto enum value.
Localization strings
Source/gui/Resources/{en,de,es,fr,fr-CA,ja,ru,uk}.lproj/Localizable.strings
Added "Binary identity verification failed" translation entries across all supported language variants.
SNTPolicyProcessor identity verification
Source/santad/SNTPolicyProcessor.h, Source/santad/SNTPolicyProcessor.mm, Source/santad/SNTPolicyProcessorTest.mm
New IdentityVerifyResult enum and verifyIdentityForTargetProc:fd:csInfo: class method implementing ES vs on-disk identity comparison (signedness, signing ID, team ID, cdhash, unsigned stat fields); integrated into decisionForFileInfo flow; 510+ lines of comprehensive test coverage.
SNTExecutionController integration
Source/santad/SNTExecutionController.h, Source/santad/SNTExecutionController.mm, Source/santad/SNTExecutionControllerTest.mm
Added kBlockBinaryMismatch reason constant; extended event counter switch for new state; changed SNTFileInfo initialization to use ES exec event; updated test mocking for identity auto-mirroring and new counter test.
Logging serializers
Source/santad/Logs/EndpointSecurity/Serializers/{BasicString,Protobuf}.mm, ...Test.mm
Added SNTEventStateBlockBinaryMismatch case mappings to reason string and proto enum converters; test updates for new enum value.
Sync service integration
Source/santasyncservice/ProtoTraits.h, Source/santasyncservice/SNTSyncEventUpload.mm
Added BLOCK_BINARY_MISMATCH decision constant wiring to sync v1/v2 proto enums; conversion case in execution event upload.
Test infrastructure updates
Source/santad/SantadTest.mm, Source/santad/BUILD, Source/common/BUILD
SantadTest now derives ES identity from on-disk binary via MOLCodesignChecker; BUILD files updated with SDK dylib link, test dependencies, and testdata export.
Dependency update
MODULE.bazel
Updated protos git override to commit 54add5f.

Sequence Diagram

sequenceDiagram
    participant ES as Endpoint Security
    participant SEC as SNTExecutionController
    participant FI as SNTFileInfo
    participant MCS as MOLCodesignChecker
    participant SP as SNTPolicyProcessor

    ES->>SEC: exec event (cdhash, signing_id, team_id, etc.)
    SEC->>FI: initWithEndpointSecurityExecEvent:<br/>(pass exec event + cpu type hint)
    FI->>MCS: codesignCheckerWithError:<br/>(fd + cpuType hint)
    MCS->>MCS: extract per-slice signing dict<br/>(if cpuType != CPU_TYPE_ANY)
    MCS-->>FI: return checker with<br/>slice-specific signing info
    FI-->>SEC: return file info<br/>(cpuType, cpuSubtype, signing certs)
    SEC->>SP: decisionForFileInfo:<br/>(...with fd + csInfo)
    SP->>SP: verifyIdentityForTargetProc:<br/>compare ES vs on-disk identity
    alt identity mismatch
        SP-->>SEC: kMismatch
    else identity drift allowed
        SP-->>SEC: kDriftAllowed
    else identity match
        SP-->>SEC: kMatch (continue evaluation)
    end
    SEC-->>ES: decision<br/>(Block / Allow / SNTEventStateBlockBinaryMismatch)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding an identity verification step to exec policy evaluation.
Description check ✅ Passed The description relates directly to the changeset by explaining the MOLCodesignChecker modification using /dev/fd/N for file descriptor routing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mlw/snt-377-verify-opened-file-matches-es-event-vnode-cdhashfstat

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
Source/santad/SNTExecutionController.mm (1)

174-174: Make binary-mismatch conversion explicit in BlockToAllowDecision.

The new state is counted here, but conversion currently relies on default in BlockToAllowDecision. Adding an explicit case will avoid hidden fallback behavior and future drift.

♻️ Proposed follow-up
 static SNTEventState BlockToAllowDecision(SNTEventState blockDecision) {
   switch (blockDecision) {
     case SNTEventStateBlockUnknown: return SNTEventStateAllowUnknown;
     case SNTEventStateBlockBinary: return SNTEventStateAllowBinary;
     case SNTEventStateBlockCertificate: return SNTEventStateAllowCertificate;
     case SNTEventStateBlockScope: return SNTEventStateAllowScope;
     case SNTEventStateBlockTeamID: return SNTEventStateAllowTeamID;
     case SNTEventStateBlockSigningID: return SNTEventStateAllowSigningID;
     case SNTEventStateBlockCDHash: return SNTEventStateAllowCDHash;
     case SNTEventStateBlockCELFallback: return SNTEventStateAllowCELFallback;
     case SNTEventStateBlockLongPath: return SNTEventStateAllowUnknown;  // No direct equivalent
+    case SNTEventStateBlockBinaryMismatch:
+      return SNTEventStateAllowUnknown;  // No direct equivalent
     default: return SNTEventStateAllowUnknown;
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/santad/SNTExecutionController.mm` at line 174, The new enum value
SNTEventStateBlockBinaryMismatch is handled in the switch that maps eventTypeStr
but BlockToAllowDecision still relies on its default branch to translate states;
add an explicit case in BlockToAllowDecision that maps
SNTEventStateBlockBinaryMismatch to the correct allow/deny decision (the same
semantic intent as kBlockBinaryMismatch) instead of falling through to default,
updating the switch in BlockToAllowDecision to include a case for
SNTEventStateBlockBinaryMismatch so behavior is explicit and future-proof.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Source/common/MOLCodesignChecker.mm`:
- Around line 160-170: The per-architecture validation in
universalSigningInformationForBinaryPath:fileDescriptor: must use the same
fd-based path used for the primary SecStaticCode binding; update the per-arch
SecStaticCodeCreateWithPath calls to build their CFURL from the same secCodePath
(i.e. "/dev/fd/N" when fileDescriptor != -1) instead of the original path
variable so slices are validated against the descriptor-bound vnode; locate the
per-arch code ref creation in
universalSigningInformationForBinaryPath:fileDescriptor: and replace usage of
path with the fd-derived secCodePath (or a new local constructed string based on
fileDescriptor) when fileDescriptor != -1.

---

Nitpick comments:
In `@Source/santad/SNTExecutionController.mm`:
- Line 174: The new enum value SNTEventStateBlockBinaryMismatch is handled in
the switch that maps eventTypeStr but BlockToAllowDecision still relies on its
default branch to translate states; add an explicit case in BlockToAllowDecision
that maps SNTEventStateBlockBinaryMismatch to the correct allow/deny decision
(the same semantic intent as kBlockBinaryMismatch) instead of falling through to
default, updating the switch in BlockToAllowDecision to include a case for
SNTEventStateBlockBinaryMismatch so behavior is explicit and future-proof.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7fc2fab3-0ab3-4692-8003-60ef34f7b14c

📥 Commits

Reviewing files that changed from the base of the PR and between 6992e06 and 6299971.

📒 Files selected for processing (31)
  • MODULE.bazel
  • Source/common/MOLCodesignChecker.h
  • Source/common/MOLCodesignChecker.mm
  • Source/common/MOLCodesignCheckerTest.mm
  • Source/common/SNTBlockMessage.mm
  • Source/common/SNTCommonEnums.h
  • Source/common/SNTFileInfo.mm
  • Source/common/ScopedFile.h
  • Source/common/ScopedFileTest.mm
  • Source/common/santa.proto
  • Source/gui/Resources/de.lproj/Localizable.strings
  • Source/gui/Resources/en.lproj/Localizable.strings
  • Source/gui/Resources/es.lproj/Localizable.strings
  • Source/gui/Resources/fr-CA.lproj/Localizable.strings
  • Source/gui/Resources/fr.lproj/Localizable.strings
  • Source/gui/Resources/ja.lproj/Localizable.strings
  • Source/gui/Resources/ru.lproj/Localizable.strings
  • Source/gui/Resources/uk.lproj/Localizable.strings
  • Source/santad/BUILD
  • Source/santad/Logs/EndpointSecurity/Serializers/BasicString.mm
  • Source/santad/Logs/EndpointSecurity/Serializers/BasicStringTest.mm
  • Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm
  • Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm
  • Source/santad/SNTExecutionController.h
  • Source/santad/SNTExecutionController.mm
  • Source/santad/SNTExecutionControllerTest.mm
  • Source/santad/SNTPolicyProcessor.h
  • Source/santad/SNTPolicyProcessor.mm
  • Source/santad/SNTPolicyProcessorTest.mm
  • Source/santasyncservice/ProtoTraits.h
  • Source/santasyncservice/SNTSyncEventUpload.mm

Comment thread Source/common/MOLCodesignChecker.mm
mlw added a commit that referenced this pull request Apr 26, 2026
MOLCodesignChecker: route per-arch SecStaticCodeCreateWithPathAndAttributes
through /dev/fd/N when an fd is supplied, matching the primary code-ref
binding so universal-binary slice validation operates on the same vnode
(CodeRabbit on PR #927).

SantadTest: mirror the on-disk binary's identity (cdhash, teamID, signingID,
signedness) into the ES-side proc fields in checkBinaryExecution: so the new
identity verifier sees matching ES vs disk. Drops the kNoRuleMatchSigningID
override pattern — the disk binaries' actual signing_ids already aren't keyed
in rules.db, preserving the "no signing-id match" intent.

SNTCommandFileInfoTest: stub initWithBinaryPath:fileDescriptor:error: with
ignoringNonObjectArgs(); SNTFileInfo.codesignCheckerWithError: invokes the
3-arg form now, so the old 2-arg stubs no longer matched.
@github-actions github-actions Bot added the comp/santactl Issues or PRs related to santactl label Apr 26, 2026

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
Source/santactl/Commands/SNTCommandFileInfoTest.mm (1)

130-132: Consider extracting repeated OCMStub setup into a test helper.

The same stub chain is repeated many times; a helper would reduce noise and future churn when the initializer signature changes again.

♻️ Suggested refactor
+ - (void)stubCodesignCheckerInitReturningError:(NSError *)err {
+   OCMStub([self.cscMock initWithBinaryPath:OCMOCK_ANY
+                             fileDescriptor:0
+                                      error:[OCMArg setTo:err]])
+       .ignoringNonObjectArgs()
+       .andReturn(self.cscMock);
+ }

-  OCMStub([self.cscMock initWithBinaryPath:OCMOCK_ANY fileDescriptor:0 error:[OCMArg setTo:err]])
-      .ignoringNonObjectArgs()
-      .andReturn(self.cscMock);
+  [self stubCodesignCheckerInitReturningError:err];

Also applies to: 139-141, 148-150, 157-159, 166-168, 175-177, 184-186, 193-195, 202-204, 211-213, 220-222, 229-231, 238-240, 247-249, 256-258, 265-267

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/santactl/Commands/SNTCommandFileInfoTest.mm` around lines 130 - 132,
Tests repeat the same OCMStub setup for [self.cscMock
initWithBinaryPath:OCMOCK_ANY fileDescriptor:0 error:[OCMArg setTo:err]]
.ignoringNonObjectArgs().andReturn(self.cscMock); — extract that chain into a
single test helper (e.g., a private method on the test class like
stubCscInitWithError: or stubCscInitReturning:) and replace each repeated
OCMStub call with a single call to that helper; ensure the helper accepts the
error pointer or return mock as parameters so callers can vary behavior without
duplicating the whole stub chain, and update uses referenced in methods that
currently call initWithBinaryPath:... to call the new helper instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Source/santactl/Commands/SNTCommandFileInfoTest.mm`:
- Around line 130-132: Tests repeat the same OCMStub setup for [self.cscMock
initWithBinaryPath:OCMOCK_ANY fileDescriptor:0 error:[OCMArg setTo:err]]
.ignoringNonObjectArgs().andReturn(self.cscMock); — extract that chain into a
single test helper (e.g., a private method on the test class like
stubCscInitWithError: or stubCscInitReturning:) and replace each repeated
OCMStub call with a single call to that helper; ensure the helper accepts the
error pointer or return mock as parameters so callers can vary behavior without
duplicating the whole stub chain, and update uses referenced in methods that
currently call initWithBinaryPath:... to call the new helper instead.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4bd1f48d-241d-4bae-bb94-003650d7099d

📥 Commits

Reviewing files that changed from the base of the PR and between 6299971 and f43024d.

📒 Files selected for processing (3)
  • Source/common/MOLCodesignChecker.mm
  • Source/santactl/Commands/SNTCommandFileInfoTest.mm
  • Source/santad/SantadTest.mm
🚧 Files skipped from review as they are similar to previous changes (1)
  • Source/common/MOLCodesignChecker.mm

@github-actions github-actions Bot added the size/xl Size: extra large label Apr 27, 2026

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Source/santad/SNTPolicyProcessor.mm`:
- Around line 679-695: The code allows UpdateCachedDecisionSigningInfo to
overwrite ES-side identity with on-disk identity after verifyIdentity returns
IdentityVerifyResult::kDriftAllowed, causing CreateRuleIDs to evaluate rules
against a mixed identity; modify the kDriftAllowed branch so we do NOT apply
on-disk signing info to cd (either skip calling UpdateCachedDecisionSigningInfo
when verifyIdentity(...) == IdentityVerifyResult::kDriftAllowed, or set/clear a
flag on cd such as cd.identityDriftAllowed and prevent
UpdateCachedDecisionSigningInfo from overwriting cd.sha256 and related fields),
and ensure CreateRuleIDs checks that flag (or presence of original ES cdhash)
and performs rule lookups using the original ES identity only.
- Around line 807-813: The verifyIdentity block is only set when
existingDecision == nil, so cached decisions (including
SNTActionRespondAllowNoCache) skip the vnode identity re-check; move or create
the verifyIdentity block unconditionally in decisionForFileInfo:targetProcess:…
so it is available for mismatch protection regardless of existingDecision, e.g.
always assign verifyIdentity = ^IdentityVerifyResult(MOLCodesignChecker*
_Nullable csInfo) { return [SNTPolicyProcessor
verifyIdentityForTargetProc:targetProc fd:fileInfo.fileHandle.fileDescriptor
csInfo:csInfo]; }; and keep the later mismatch check (lines referencing
verifyIdentity) unchanged so cached responses still trigger re-verification via
verifyIdentityForTargetProc.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 581f9fa8-ffa4-4c2c-bbb9-1d2bde4b07ef

📥 Commits

Reviewing files that changed from the base of the PR and between f43024d and bbee9e3.

📒 Files selected for processing (3)
  • Source/common/MOLCodesignCheckerTest.mm
  • Source/santad/SNTPolicyProcessor.mm
  • Source/santad/SNTPolicyProcessorTest.mm
✅ Files skipped from review due to trivial changes (1)
  • Source/santad/SNTPolicyProcessorTest.mm

mlw added 7 commits April 27, 2026 14:09
…SNT-377)

At policy-evaluation time, compare the kernel's view of the about-to-execute
binary (targetProc from the ES event) against Santa's on-disk view
(MOLCodesignChecker for signed; fstat(fd) for unsigned). When the two views
disagree, deny with a new SNTEventStateBlockBinaryMismatch decision. CDHash
drift within a matching (TeamID, SigningID) is allowed to accommodate
concurrent package installs.

Plumbs the new event state through telemetry proto (REASON_BINARY_MISMATCH),
sync proto (BLOCK_BINARY_MISMATCH v1+v2, protos pin -> 54add5f7), BasicString
serializer, GUI block reasons, and the exec counter. ScopedFile gains a size
accessor and keep-path mode used by integration tests.
When a fd is supplied, construct SecStaticCode via /dev/fd/N instead of
re-resolving the path. Avoids a redundant path lookup and aligns disk-side
identity with the caller's open vnode — consumed by SNT-377's verifier.
- Tighten BinaryMismatch decisionExtra assertion to lock the contract.
- Document the critical-system-binaries short-circuit with the security
  property it relies on (CS_KILL/CS_HARD enforcement).
- Drop redundant per-test mockCodesignChecker.teamID stubs in
  testTeamID(Allow|Block)Rule (the validateExecEvent fixture already
  mirrors target identity onto the mock); tighten the fixture comment
  to discourage adding new ones.
MOLCodesignChecker: route per-arch SecStaticCodeCreateWithPathAndAttributes
through /dev/fd/N when an fd is supplied, matching the primary code-ref
binding so universal-binary slice validation operates on the same vnode
(CodeRabbit on PR #927).

SantadTest: mirror the on-disk binary's identity (cdhash, teamID, signingID,
signedness) into the ES-side proc fields in checkBinaryExecution: so the new
identity verifier sees matching ES vs disk. Drops the kNoRuleMatchSigningID
override pattern — the disk binaries' actual signing_ids already aren't keyed
in rules.db, preserving the "no signing-id match" intent.

SNTCommandFileInfoTest: stub initWithBinaryPath:fileDescriptor:error: with
ignoringNonObjectArgs(); SNTFileInfo.codesignCheckerWithError: invokes the
3-arg form now, so the old 2-arg stubs no longer matched.
Threads cputype/cpusubtype from es_event_exec_t through SNTFileInfo into
a new MOLCodesignChecker initializer so signing identity (cdhash, teamID,
signingID, certificates) reflects the slice the kernel actually loaded.
Without the hint, behavior is unchanged - the new path is opt-in via the
new SNTFileInfo exec-event initializer.

For per-arch-inconsistent universals, the active slice's identity now
populates regardless of cross-slice consistency. The cross-slice check
remains as an informational error signal.
@mlw mlw force-pushed the mlw/snt-377-verify-opened-file-matches-es-event-vnode-cdhashfstat branch from bbee9e3 to 6571d15 Compare April 27, 2026 19:07

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

♻️ Duplicate comments (2)
Source/santad/SNTPolicyProcessor.mm (2)

667-690: ⚠️ Potential issue | 🔴 Critical

Re-run identity verification on cache-hit paths too.

Line 807 only creates verifyIdentity for fresh evaluations, and Lines 667-690 only invoke it while repopulating signing info. Any re-eval path with cached signing material therefore skips the new ES-vs-disk mismatch gate entirely, so stale cached decisions can survive an in-place vnode mutation.

Suggested direction
-  NSError* csInfoError;
-  if (!cd.certSHA256.length) {
-    MOLCodesignChecker* csInfo = [fileInfo codesignCheckerWithError:&csInfoError];
+  NSError* csInfoError;
+  MOLCodesignChecker* csInfo = nil;
+  if (verifyIdentity || !cd.certSHA256.length) {
+    csInfo = [fileInfo codesignCheckerWithError:&csInfoError];
+  }
+
+  if (verifyIdentity) {
+    switch (verifyIdentity(csInfo)) {
+      case IdentityVerifyResult::kMismatch:
+        cd.decision = SNTEventStateBlockBinaryMismatch;
+        cd.decisionExtra = @"Binary identity mismatch between ES event and on-disk file";
+        return cd;
+      case IdentityVerifyResult::kDriftAllowed:
+        cd.decisionExtra = @"CDHash drift allowed by matching TeamID/SigningID";
+        break;
+      case IdentityVerifyResult::kMatch:
+        break;
+    }
+  }
+
+  if (!cd.certSHA256.length) {
-    if (verifyIdentity) {
-      switch (verifyIdentity(csInfo)) {
-        case IdentityVerifyResult::kMismatch:
-          cd.decision = SNTEventStateBlockBinaryMismatch;
-          cd.decisionExtra = @"Binary identity mismatch between ES event and on-disk file";
-          return cd;
-        case IdentityVerifyResult::kDriftAllowed:
-          cd.decisionExtra = @"CDHash drift allowed by matching TeamID/SigningID";
-          break;
-        case IdentityVerifyResult::kMatch: break;
-      }
-    }
-
     if (csInfo && csInfo.cdhash.length > 0) {
       UpdateCachedDecisionSigningInfo(cd, csInfo, platformBinaryState, entitlementsFilterCallback);
     } else if (csInfoError) {
       ...
     }
   }
-  VerifyIdentityBlock verifyIdentity = nil;
-  if (!existingDecision) {
-    int fd = fileInfo.fileHandle.fileDescriptor;
-    verifyIdentity = ^IdentityVerifyResult(MOLCodesignChecker* _Nullable csInfo) {
-      return [SNTPolicyProcessor verifyIdentityForTargetProc:targetProc fd:fd csInfo:csInfo];
-    };
-  }
+  int fd = fileInfo.fileHandle.fileDescriptor;
+  VerifyIdentityBlock verifyIdentity = ^IdentityVerifyResult(MOLCodesignChecker* _Nullable csInfo) {
+    return [SNTPolicyProcessor verifyIdentityForTargetProc:targetProc fd:fd csInfo:csInfo];
+  };

Also applies to: 807-813

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/santad/SNTPolicyProcessor.mm` around lines 667 - 690, The identity
re-check is only performed for fresh evaluations so cached-signing paths skip
the ES-vs-disk mismatch gate; ensure verifyIdentity is invoked for
cache-hit/repopulating signing-info flows as well by making the verifyIdentity
callable available before the cache-check (or by creating it in the cached-path)
and calling verifyIdentity(csInfo) in the block that handles
!cd.certSHA256.length, handling the three results exactly as the fresh-eval
branch (set cd.decision = SNTEventStateBlockBinaryMismatch and cd.decisionExtra
for kMismatch, set cd.decisionExtra for kDriftAllowed, and treat kMatch as
no-op) so stale cached decisions cannot survive an in-place vnode mutation.

685-695: ⚠️ Potential issue | 🟠 Major

Don't mix disk-only rule IDs into kDriftAllowed decisions.

Once Line 685 allows drift, Lines 692-695 still hydrate certificate data from the on-disk file while cdhash, teamID, and signingID continue to describe the executing image. CreateRuleIDs(cd) then evaluates binary/certificate rules against mixed identity.

Suggested direction
-    if (verifyIdentity) {
-      switch (verifyIdentity(csInfo)) {
+    IdentityVerifyResult identityResult = IdentityVerifyResult::kMatch;
+    if (verifyIdentity) {
+      identityResult = verifyIdentity(csInfo);
+      switch (identityResult) {
         case IdentityVerifyResult::kMismatch:
           cd.decision = SNTEventStateBlockBinaryMismatch;
           cd.decisionExtra = @"Binary identity mismatch between ES event and on-disk file";
           return cd;
         case IdentityVerifyResult::kDriftAllowed:
           cd.decisionExtra = @"CDHash drift allowed by matching TeamID/SigningID";
           break;
         case IdentityVerifyResult::kMatch: break;
       }
     }

-    if (csInfo && csInfo.cdhash.length > 0) {
+    if (identityResult == IdentityVerifyResult::kMatch && csInfo && csInfo.cdhash.length > 0) {
       // Identity was extracted — populate signing details and keep whatever
       // signingStatus the kernel reported, even if csInfoError is set.
       UpdateCachedDecisionSigningInfo(cd, csInfo, platformBinaryState, entitlementsFilterCallback);
+    } else if (identityResult == IdentityVerifyResult::kDriftAllowed) {
+      cd.sha256 = nil;
+      cd.certSHA256 = nil;
+      cd.certCommonName = nil;
+      cd.certChain = nil;
     } else if (csInfoError) {
       cd.decisionExtra = [NSString
           stringWithFormat:@"Signature ignored due to error: %ld", (long)csInfoError.code];
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Source/santad/SantadTest.mm`:
- Around line 198-205: Currently the code treats a nil MOLCodesignChecker
(csInfo) the same as an unsigned binary; instead, detect csInfo == nil as a
test/setup failure and fail fast (e.g., call the test failure helper or
return/abort the test) so real fixture errors are not masked, and only use the
unsigned path when csInfo is non-nil and csInfo.cdhash.length == 0 before
clearing proc.codesigning_flags; update the logic around MOLCodesignChecker
initialization (csInfo) to assert/fail on nil, then handle the empty cdhash case
separately.

---

Duplicate comments:
In `@Source/santad/SNTPolicyProcessor.mm`:
- Around line 667-690: The identity re-check is only performed for fresh
evaluations so cached-signing paths skip the ES-vs-disk mismatch gate; ensure
verifyIdentity is invoked for cache-hit/repopulating signing-info flows as well
by making the verifyIdentity callable available before the cache-check (or by
creating it in the cached-path) and calling verifyIdentity(csInfo) in the block
that handles !cd.certSHA256.length, handling the three results exactly as the
fresh-eval branch (set cd.decision = SNTEventStateBlockBinaryMismatch and
cd.decisionExtra for kMismatch, set cd.decisionExtra for kDriftAllowed, and
treat kMatch as no-op) so stale cached decisions cannot survive an in-place
vnode mutation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0c555433-3253-462a-b567-5a95040dd7e9

📥 Commits

Reviewing files that changed from the base of the PR and between bbee9e3 and 6571d15.

📒 Files selected for processing (36)
  • MODULE.bazel
  • Source/common/BUILD
  • Source/common/MOLCodesignChecker.h
  • Source/common/MOLCodesignChecker.mm
  • Source/common/MOLCodesignCheckerTest.mm
  • Source/common/SNTBlockMessage.mm
  • Source/common/SNTCommonEnums.h
  • Source/common/SNTFileInfo.h
  • Source/common/SNTFileInfo.mm
  • Source/common/SNTFileInfoTest.mm
  • Source/common/ScopedFile.h
  • Source/common/ScopedFileTest.mm
  • Source/common/santa.proto
  • Source/gui/Resources/de.lproj/Localizable.strings
  • Source/gui/Resources/en.lproj/Localizable.strings
  • Source/gui/Resources/es.lproj/Localizable.strings
  • Source/gui/Resources/fr-CA.lproj/Localizable.strings
  • Source/gui/Resources/fr.lproj/Localizable.strings
  • Source/gui/Resources/ja.lproj/Localizable.strings
  • Source/gui/Resources/ru.lproj/Localizable.strings
  • Source/gui/Resources/uk.lproj/Localizable.strings
  • Source/santactl/Commands/SNTCommandFileInfoTest.mm
  • Source/santad/BUILD
  • Source/santad/Logs/EndpointSecurity/Serializers/BasicString.mm
  • Source/santad/Logs/EndpointSecurity/Serializers/BasicStringTest.mm
  • Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm
  • Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm
  • Source/santad/SNTExecutionController.h
  • Source/santad/SNTExecutionController.mm
  • Source/santad/SNTExecutionControllerTest.mm
  • Source/santad/SNTPolicyProcessor.h
  • Source/santad/SNTPolicyProcessor.mm
  • Source/santad/SNTPolicyProcessorTest.mm
  • Source/santad/SantadTest.mm
  • Source/santasyncservice/ProtoTraits.h
  • Source/santasyncservice/SNTSyncEventUpload.mm
✅ Files skipped from review due to trivial changes (15)
  • Source/gui/Resources/ja.lproj/Localizable.strings
  • Source/gui/Resources/fr.lproj/Localizable.strings
  • Source/gui/Resources/de.lproj/Localizable.strings
  • Source/gui/Resources/ru.lproj/Localizable.strings
  • Source/santad/SNTExecutionController.h
  • Source/gui/Resources/fr-CA.lproj/Localizable.strings
  • Source/gui/Resources/es.lproj/Localizable.strings
  • Source/santasyncservice/SNTSyncEventUpload.mm
  • Source/santad/Logs/EndpointSecurity/Serializers/BasicString.mm
  • Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm
  • MODULE.bazel
  • Source/common/SNTCommonEnums.h
  • Source/santasyncservice/ProtoTraits.h
  • Source/gui/Resources/en.lproj/Localizable.strings
  • Source/gui/Resources/uk.lproj/Localizable.strings
🚧 Files skipped from review as they are similar to previous changes (5)
  • Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm
  • Source/common/SNTBlockMessage.mm
  • Source/common/ScopedFileTest.mm
  • Source/santad/BUILD
  • Source/santactl/Commands/SNTCommandFileInfoTest.mm

Comment thread Source/santad/SantadTest.mm
…th-based

codesignCheckerWithError: now uses the fd-bound MOLCodesignChecker
initializer only when the SNTFileInfo was constructed from an exec event
(cputype hint set). Diagnostic callers (santactl, compiler controller,
GUI, sync) fall back to the path-based init so SecStaticCode has full
bundle context, producing richer signing-status output.

Convert MOLCodesignChecker's fat-header read to pread so the caller's
file offset is preserved across the call. Document on SNTFileInfo's
fileHandle property that the offset is unspecified after
codesignCheckerWithError: (SecStaticCode reads through /dev/fd/N share
the descriptor's offset), and add a regression test pinning that
pread-based readers see stable bytes before and after.

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Source/common/MOLCodesignChecker.mm (1)

541-559: ⚠️ Potential issue | 🟠 Major

Retry interrupted pread() calls in the fd-offset reader.

Lines 546 and 559 treat any pread() failure as fatal. pread(2) can return -1 with errno == EINTR, so a transient signal will make architectureAndOffsetsForFileDescriptor: return nil spuriously and suppress the new exec-time fd/slice-bound path.

🔧 Proposed fix
   const uint8* headerBytes = (const uint8*)alloca(len);
-  if (pread(fd, (void*)headerBytes, len, 0) != (ssize_t)len) return nil;
+  ssize_t bytesRead;
+  do {
+    bytesRead = pread(fd, (void*)headerBytes, len, 0);
+  } while (bytesRead == -1 && errno == EINTR);
+  if (bytesRead != (ssize_t)len) return nil;
   struct fat_header* fh = (struct fat_header*)headerBytes;
@@
   len = use64 ? sizeof(struct fat_arch_64) * archCount : sizeof(struct fat_arch) * archCount;
   const uint8* archBytes = (const uint8*)alloca(len);
-  if (pread(fd, (void*)archBytes, len, sizeof(struct fat_header)) != (ssize_t)len) return nil;
+  do {
+    bytesRead = pread(fd, (void*)archBytes, len, sizeof(struct fat_header));
+  } while (bytesRead == -1 && errno == EINTR);
+  if (bytesRead != (ssize_t)len) return nil;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/common/MOLCodesignChecker.mm` around lines 541 - 559, The pread()
calls that read headerBytes and archBytes in
architectureAndOffsetsForFileDescriptor: treat any return != len as fatal;
change both to robust read loops that handle EINTR and partial reads by
repeatedly calling pread() until the requested len bytes are read or a
non-recoverable error occurs, accumulating bytes read in a ssize_t total and
advancing the buffer pointer and file offset argument accordingly; on failure
propagate nil only for non-EINTR errors or short reads after the loop. Ensure
you update the two sites that call pread(fd, headerBytes, len, 0) and pread(fd,
archBytes, len, sizeof(struct fat_header)) to use this retry/accumulate pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@Source/common/MOLCodesignChecker.mm`:
- Around line 541-559: The pread() calls that read headerBytes and archBytes in
architectureAndOffsetsForFileDescriptor: treat any return != len as fatal;
change both to robust read loops that handle EINTR and partial reads by
repeatedly calling pread() until the requested len bytes are read or a
non-recoverable error occurs, accumulating bytes read in a ssize_t total and
advancing the buffer pointer and file offset argument accordingly; on failure
propagate nil only for non-EINTR errors or short reads after the loop. Ensure
you update the two sites that call pread(fd, headerBytes, len, 0) and pread(fd,
archBytes, len, sizeof(struct fat_header)) to use this retry/accumulate pattern.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5e0eda02-3a18-48ce-bf91-ea0881cff7ff

📥 Commits

Reviewing files that changed from the base of the PR and between 6571d15 and d0dd816.

📒 Files selected for processing (5)
  • Source/common/BUILD
  • Source/common/MOLCodesignChecker.mm
  • Source/common/SNTFileInfo.h
  • Source/common/SNTFileInfo.mm
  • Source/common/SNTFileInfoTest.mm

@northpolesec northpolesec deleted a comment from coderabbitai Bot Apr 28, 2026
@northpolesec northpolesec deleted a comment from coderabbitai Bot Apr 28, 2026
- SNTExecutionController: explicit BlockBinaryMismatch arm in
  BlockToAllowDecision so the mapping is locked rather than relying on the
  default-fall-through.
- MOLCodesignChecker: factor a PreadFull helper that retries on EINTR and
  short reads, and route the fat-header / arch-table reads in
  architectureAndOffsetsForFileDescriptor: through it.
- SantadTest: distinguish a MOL init failure (XCTFail + return NO) from a
  genuine unsigned binary (non-nil checker with empty cdhash) so fixture
  problems surface instead of silently exercising the unsigned path.
@mlw mlw marked this pull request as ready for review April 28, 2026 04:54
@mlw mlw requested a review from a team as a code owner April 28, 2026 04:54
Comment thread Source/santad/SNTPolicyProcessor.mm Outdated

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.

Should we not add one more check here: && !identityExtracted? (Or call UpdateCachedDecisionSigningInfo like we do on line 695)

sharvilshah
sharvilshah previously approved these changes Apr 28, 2026

@sharvilshah sharvilshah left a comment

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.

Just a question, but lgtm

Comment thread Source/common/MOLCodesignChecker.mm
Comment on lines +595 to +597
if (CdhashEqual(targetProc->cdhash, csInfo.cdhash)) {
return IdentityVerifyResult::kMatch;
}

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.

Should we test for CDHash equality first thing inside of the if (esHasSID) branch?


@note The file offset will be set to the amount of bytes read while parsing the header.
The descriptor must refer to a regular-file Mach-O. SecStaticCode operates
on the descriptor's vnode (via /dev/fd/N) rather than re-resolving

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.

Are we sure that having SecStaticCode looking /dev/fd/N paths is not masking signature failing errors behind non-failing errors like errSecCSInfoPlistFailed?

@mlw mlw marked this pull request as draft May 1, 2026 15:07
@mlw

mlw commented May 11, 2026

Copy link
Copy Markdown
Contributor Author

This is being superseded by #942

@mlw mlw closed this May 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/common comp/gui Issues or PRs related to the Santa GUI comp/santactl Issues or PRs related to santactl comp/santad Issues or PRs related to the daemon comp/santasyncservice Issues or PRs related to the sync protocol lang/objc++ PRs modifying files in ObjC++ size/l Size: large size/xl Size: extra large

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants