Skip to content

Fix FDA check: query TCC database instead of file test#5764

Merged
kodjima33 merged 1 commit intomainfrom
fix/fda-check-tcc-database
Mar 17, 2026
Merged

Fix FDA check: query TCC database instead of file test#5764
kodjima33 merged 1 commit intomainfrom
fix/fda-check-tcc-database

Conversation

@kodjima33
Copy link
Copy Markdown
Collaborator

Summary

  • ~/Library/Mail is always readable by the file owner — not a valid FDA test
  • Now queries ~/Library/Application Support/com.apple.TCC/TCC.db for kTCCServiceSystemPolicyAllFiles entries
  • Correctly detects FDA as not granted when user hasn't enabled it

🤖 Generated with Claude Code

The previous check (isReadableFile on ~/Library/Mail) always returned true
because the file owner can always read their own directories regardless of
FDA status. Fix: query the user's TCC.db for kTCCServiceSystemPolicyAllFiles
entries matching the app's bundle ID.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@kodjima33 kodjima33 merged commit c99177c into main Mar 17, 2026
@kodjima33 kodjima33 deleted the fix/fda-check-tcc-database branch March 17, 2026 23:04
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 17, 2026

Greptile Summary

This PR replaces an incorrect FDA (Full Disk Access) detection heuristic — checking readability of ~/Library/Mail, which is always readable by the file owner — with a new approach that spawns an sqlite3 subprocess to query the user-level TCC database directly. While the motivation is correct, the implementation has three significant problems that affect both correctness and UI responsiveness.

Key changes:

  • checkFullDiskAccess() now spawns /usr/bin/sqlite3 to query ~/Library/Application Support/com.apple.TCC/TCC.db for a kTCCServiceSystemPolicyAllFiles row with auth_value = 2
  • An unrelated addition clears the local knowledge graph (KnowledgeGraphStorage) during the onboarding reset flow

Issues found:

  • Main-thread blocking: AppState is @MainActor, and checkFullDiskAccess() is called from SwiftUI .onReceive(permissionCheckTimer) timer callbacks (in OnboardingChatView) and from checkAllPermissions() at startup — both on the main thread. process.waitUntilExit() is a synchronous blocking call that will freeze the UI for the duration of the sqlite3 query with no timeout guard.
  • Likely always-false result: The sqlite3 subprocess is identified by its own bundle ID (com.apple.sqlite3), not the calling app's. macOS evaluates TCC FDA grants per-process audit token, so sqlite3 almost certainly cannot open TCC.db even when the app itself has FDA — causing the check to permanently return "not granted" after FDA is enabled, a false negative.
  • Unsafe SQL construction: The bundle identifier is interpolated directly into the SQL query string without escaping, making the query vulnerable to SQL injection if the bundle ID contains a single-quote character.

Confidence Score: 1/5

  • Not safe to merge — the new FDA check is likely broken by design (child process cannot inherit TCC grant) and blocks the main thread on every timer tick.
  • The core correctness of the fix is in question: the sqlite3 subprocess evaluated under its own TCC identity almost certainly cannot read TCC.db when spawned from a non-FDA app, and may also fail when the app does have FDA — producing a permanent false negative. Additionally, the synchronous waitUntilExit() call on the @MainActor main thread causes UI-blocking on every permission-check timer tick. These two issues together mean the PR likely does not achieve its stated goal and regresses UI responsiveness.
  • desktop/Desktop/Sources/AppState.swift — specifically the new checkFullDiskAccess() implementation

Important Files Changed

Filename Overview
desktop/Desktop/Sources/AppState.swift Replaces ~/Library/Mail readability check with a spawned sqlite3 subprocess to query TCC.db, but introduces three issues: main-thread blocking via waitUntilExit() in a @MainActor class called from UI timers, unsafe bundle-ID interpolation in the SQL string, and a likely false-negative because the sqlite3 child process does not inherit the parent app's TCC/FDA grant. Also includes an unrelated addition to clear the local knowledge graph during onboarding reset.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[checkFullDiskAccess called\non @MainActor / main thread] --> B[Spawn /usr/bin/sqlite3\nas child Process]
    B --> C[process.waitUntilExit\nBLOCKS main thread]
    C --> D{Swift throws?}
    D -- Yes --> E[catch: set hasFullDiskAccess = false]
    D -- No --> F[Read stdout output]
    F --> G{output == '2'?}
    G -- Yes --> H[hasFullDiskAccess = true]
    G -- No --> I[hasFullDiskAccess = false]

    subgraph TCC Check in Child Process
        B --> J[sqlite3 queries\n~/Library/...TCC.db]
        J --> K{sqlite3 has its OWN\nTCC FDA grant?}
        K -- No FDA grant --> L[sqlite3 cannot open TCC.db\nOutput: empty / error on stderr]
        K -- Has FDA grant --> M[Returns auth_value row]
    end

    L --> I
    M --> G

    style C fill:#f99,stroke:#c00
    style K fill:#f99,stroke:#c00
    style L fill:#fdd,stroke:#c00
Loading

Last reviewed commit: "Fix Full Disk Access..."

Comment on lines +1002 to +1003
try process.run()
process.waitUntilExit()
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.

P1 Blocking main thread with waitUntilExit()

AppState is declared @MainActor (line 24), so checkFullDiskAccess() runs on the main thread. process.waitUntilExit() is a synchronous, blocking call — it will freeze the UI until the sqlite3 subprocess finishes.

This is made worse by the three call sites:

  • OnboardingChatView.swift:418 — inside a SwiftUI .onReceive(permissionCheckTimer) closure, which fires on the main actor on every timer tick.
  • AppState.swift:631 — called from checkAllPermissions() at startup, also on the main actor.
  • ChatToolExecutor.swift:568 — called inside an async Task (this one is safer, but the underlying function still blocks its executing thread).

If sqlite3 takes even 200–300 ms to open and query the database (e.g., due to I/O), this will drop frames and cause observable UI jank. If for any reason sqlite3 hangs, the main thread blocks indefinitely.

The fix is to dispatch the entire Process block onto a background queue/thread and publish the result back on the main actor, or to use process.terminationHandler for an async completion callback.

process.executableURL = URL(fileURLWithPath: "/usr/bin/sqlite3")
process.arguments = [
tccPath,
"SELECT auth_value FROM access WHERE service='kTCCServiceSystemPolicyAllFiles' AND client='\(bundleId)' LIMIT 1;",
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.

P1 SQL injection via bundle ID string interpolation

The bundleId value is interpolated directly into the SQL string:

"SELECT auth_value FROM access WHERE service='kTCCServiceSystemPolicyAllFiles' AND client='\(bundleId)' LIMIT 1;"

While bundle identifiers are developer-controlled at build time and normally safe (e.g. com.example.omi), they are not sanitized. A bundle ID containing a single-quote character (e.g. com.example.o'mi) would break the query syntax or allow injection. The sqlite3 CLI does not support parameterized queries via arguments, but the value should at minimum be validated or escaped:

let safeBundleId = bundleId.replacingOccurrences(of: "'", with: "''")
// then use safeBundleId in the SQL string

Comment on lines +992 to 1016
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/sqlite3")
process.arguments = [
tccPath,
"SELECT auth_value FROM access WHERE service='kTCCServiceSystemPolicyAllFiles' AND client='\(bundleId)' LIMIT 1;",
]
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = Pipe()
do {
try process.run()
process.waitUntilExit()
let output = String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let granted = output == "2"
if granted != hasFullDiskAccess {
hasFullDiskAccess = granted
log("Full Disk Access: \(granted ? "granted" : "not granted")")
}
} catch {
if hasFullDiskAccess {
hasFullDiskAccess = false
log("Full Disk Access: check failed, assuming not granted")
}
}
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.

P1 sqlite3 subprocess may not inherit the app's TCC grant, causing FDA to always appear not granted

TCC grants are evaluated per-process using the process's audit token and responsible-party bundle ID. When the app spawns /usr/bin/sqlite3 via Process, sqlite3 runs as its own process and is identified by its own bundle identifier (com.apple.sqlite3) — not the app's. On macOS 11+, the kernel requires FDA in the spawning process's TCC record specifically, and sandboxed/TCC-checked subprocesses do not automatically inherit FDA from their parent.

In practice this means:

  • If FDA is not granted: sqlite3 cannot read TCC.db → empty output → granted = false ✅ (correct)
  • If FDA is granted to the app: sqlite3 still has no FDA of its own → sqlite3 also cannot open TCC.db → empty output → granted = false ❌ (wrong — false negative)

This would cause the check to permanently report FDA as not granted even after the user enables it.

A more reliable approach that stays within the process itself is to attempt opening the database via SQLite3 directly (using the C API from Swift, which inherits the app's TCC grant), or to fall back to checking a known FDA-protected system path (such as /Library/Application Support/com.apple.TCC/TCC.db, the system-level TCC database) using FileManager.isReadableFile(atPath:) — which evaluates TCC in the calling process's context rather than spawning a child.

Glucksberg pushed a commit to Glucksberg/omi-local that referenced this pull request Apr 28, 2026
…#5764)

## Summary
- `~/Library/Mail` is always readable by the file owner — not a valid
FDA test
- Now queries `~/Library/Application Support/com.apple.TCC/TCC.db` for
`kTCCServiceSystemPolicyAllFiles` entries
- Correctly detects FDA as not granted when user hasn't enabled it

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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