Fix FDA check: query TCC database instead of file test#5764
Conversation
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>
Greptile SummaryThis PR replaces an incorrect FDA (Full Disk Access) detection heuristic — checking readability of Key changes:
Issues found:
Confidence Score: 1/5
Important Files Changed
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
Last reviewed commit: "Fix Full Disk Access..." |
| try process.run() | ||
| process.waitUntilExit() |
There was a problem hiding this comment.
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 fromcheckAllPermissions()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;", |
There was a problem hiding this comment.
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| 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") | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
…#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)
Summary
~/Library/Mailis always readable by the file owner — not a valid FDA test~/Library/Application Support/com.apple.TCC/TCC.dbforkTCCServiceSystemPolicyAllFilesentries🤖 Generated with Claude Code