From 508cd4b0b50fb205f1fa56f554d5dc63a3e01a5a Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 13 Nov 2026 20:21:00 -0500 Subject: [PATCH] fix(daemon): report a helper's denial over a later allowed result Overlapping tccd requests can resolve out of order; when any result for the denied folder class is a denial, report it. --- .../permissions/tcc-denial-log-verdict.ts | 5 +++++ .../testing/tcc-denial-log-verdict.test.ts | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/main/daemon/permissions/tcc-denial-log-verdict.ts b/src/main/daemon/permissions/tcc-denial-log-verdict.ts index 682b1789fbd..8ea9f65354f 100644 --- a/src/main/daemon/permissions/tcc-denial-log-verdict.ts +++ b/src/main/daemon/permissions/tcc-denial-log-verdict.ts @@ -173,7 +173,12 @@ export function parseTccLogVerdict( const relevantGroups = [...groups.values()].filter((group) => group.relevant) const decided = relevantGroups.filter((group) => group.result !== null) + // Overlapping requests can resolve out of order; a later "allowed" must not mask a denial. + const denied = decided.filter((group) => group.result?.authValue === '0') const chosen = + denied.findLast( + (group) => group.service !== null && SERVICE_BY_NAME[group.service] === subject.cwdClass + ) ?? decided.findLast( (group) => group.service !== null && SERVICE_BY_NAME[group.service] === subject.cwdClass ) ?? diff --git a/src/main/daemon/permissions/testing/tcc-denial-log-verdict.test.ts b/src/main/daemon/permissions/testing/tcc-denial-log-verdict.test.ts index 930c152acff..d9c39865ba0 100644 --- a/src/main/daemon/permissions/testing/tcc-denial-log-verdict.test.ts +++ b/src/main/daemon/permissions/testing/tcc-denial-log-verdict.test.ts @@ -149,6 +149,23 @@ describe('parseTccLogVerdict', () => { expect(documents).toMatchObject({ service: 'documents', authValue: 'allowed' }) }) + it('keeps a denial that resolves after a later request was allowed', () => { + const [send, ctx, attribution, deniedResult] = HELPER_DOWNLOADS_REFUSED + const later = [send, ctx, attribution].map((entry) => + entry.replaceAll('msgID=153.1126', 'msgID=153.1127') + ) + const allowedResult = deniedResult + .replace('msgID=153.1126', 'msgID=153.1127') + .replace('authValue=0', 'authValue=2') + // Request 1126 starts first, 1127 is allowed, then 1126 is refused. + const verdict = parseTccLogVerdict( + ndjson([send, ctx, attribution, ...later, allowedResult, deniedResult]), + SUBJECT, + 'ok' + ) + expect(verdict).toMatchObject({ authValue: 'denied', service: 'downloads' }) + }) + it('flags a failed stored requirement but not the routine "anchor apple" check', () => { const routine = line( '20:06:45.144194',