iOS Photos full access: establishing and verifying PHAccessLevelReadWrite¶
Status: non-fatal unverified success, typed observed failure and an injectable verifier are implemented
(#12317). Simulator verification with the Playground probe is still owed; nothing here is
device-verified. Related: #12316 (readback classification), #12368 (Photos grants return
verified:false, readback unknown).
Problem¶
setAppPermissions with photos on a simulator runs simctl privacy <udid> grant photos <bundle>
(src/utils/ios-cmdline-tools/IosPermissionsBackend.ts) and stores a kTCCServicePhotos row. The
issue’s controlled matrix (iOS 26.5, Xcode 26.6) shows the app’s own
PHPhotoLibrary.authorizationStatus(for: .readWrite) still notDetermined after that grant, and
.addOnly becomes authorized only after photos-add. No simulator/runtime combination is verified to
give effective full access through simctl privacy grant. Physical-device limits are unchanged.
Host outcomes (src/features/action/IosSimulatorPermissions.ts, classifyIosSimulatorPermission,
verifyPhotosGrant; PhotosAuthorizationVerifier is an injectable seam for target-app
instrumentation):
| Observation | Result |
|---|---|
| Default (dispatched, unobservable), or verifier/TCC-readback throws | success: true, applied: true, verified: false, warning, code: "PHOTOS_FULL_ACCESS_UNVERIFIED"; counts as changed; warnings aggregate in warnings; exceptions logged at warn; a readback exception returns this before consulting the verifier |
authorized read/write |
Verified success, no code or warning |
Positive limited, denied, notDetermined, addOnly |
Typed failure PHOTOS_FULL_ACCESS_NOT_ESTABLISHED, applied: true, verified: false, accessLevel: "readWrite", actionable error |
For all, a confirmed Photos failure keeps the successful all dispatch (applied: true) and adds a
separate failed photos result; non-Photos services are not marked failed, only Photos increments
failedCount, and the top-level error is the first failed result’s. The facade maps each result to its
own operation with a permission-specific stepId and exposes its code. getAppPermissions still
classifies a stored Photos grant as unknown, verified: false, evidence: "storedTccGrant"; stored
TCC grants are evidence only.
What the host can and cannot observe¶
Apple API facts:
PHPhotoLibrary.authorizationStatus(for:)(iOS 14+) reports the calling process’s own identity. Raw values:notDetermined0,restricted1,denied2,authorized3,limited4.- Querying never prompts; only
requestAuthorization(for:handler:)does. A request needsNSPhotoLibraryUsageDescription(read/write) /NSPhotoLibraryAddUsageDescription(add-only); the status query needs neither. XCUIApplication.resetAuthorizationStatus(for:)can reset a protected resource; the runner uses it (ios/control-proxy/Sources/CtrlProxy/GesturePerformer.swift,.photosfor bothphotosandphotos-add, sinceXCUIProtectedResourcehas only.photos, so it can’t distinguish the levels). XCUITest has no public API to read another app’s authorization status.
Repo evidence: stored TCC rows are host-readable (SimulatorTccSqliteClient.ts maps photos to
kTCCServicePhotos, photos-add to kTCCServicePhotosAdd), but a stored auth_value=2 row is not the
effective PhotoKit status, so TCC is not a verification channel.
Options¶
- A. Query from the CtrlProxy/XCTest runner (rejected). The runner is a separate process
(
XCTRunner) with its own bundle identity; its status describes the runner, not the target, so a runner command would return an authoritative-looking number about the wrong app. Unverified whether the runner’s status tracks the target’s on a simulator; don’t rely on it. - B. Instrumented probe app (recommended first step). An app without our SDK (or the Playground)
calls
authorizationStatus(for:)for both levels and writes raw values to its ownDocuments/as JSON; the host reads it viasimctl get_app_container <udid> <bundle> data(the channel used bysrc/server/appFileService.tsand the SDK-free probe patternios/UserDefaultsProbe/Probe.swift+scripts/ios/userdefaults-no-sdk-smoke.sh). It observes real PhotoKit state but of the PROBE app, so it proves what a grant sequence does per simulator/runtime/Xcode (independent probe, cold-restart persistence) and can anchor a support matrix, but cannot makesetAppPermissionsreportverified: truefor an app that didn’t ship it. The existingios/Playground/Sources/PhotosAuthorizationProbe.swiftwritesDocuments/photos-authorization.jsonat launch (readWriteandaddOnly, each{ rawValue, name }, plusobservedAt); the Playground Info.plist gains both usage-description keys. It never callsrequestAuthorization. - C. In-app SDK status for integrated apps (follow-up, not implemented). A
PhotosAuthorizationInspectorinios/auto-mobile-sdkcould publish the same JSON from the real target, lettingverifyPhotosGrantreturnverified: truewhen the snapshot is fresh (only as new as the last launch/foreground) andreadWrite.name == "authorized". Costs SDK API surface; helps only SDK apps. A realPhotosAuthorizationVerifierneeds this; the production default stays unobservable. - D. Different TCC writes (all UNVERIFIED, not attempted). Direct
TCC.dbinserts (kTCCServicePhotosauth_value=3,kTCCServicePhotosAddother values) are unlikely to differ sincephotolibrarydseems to keep state the TCC row doesn’t set, andTCC.dbwrites are unsupported and may break between runtimes; granting after first launch or after aphotolibrarydrestart (the issue only tried before first launch); additional service names on iOS 17+ (the repo maps only the two). Run these with the option B probe on a dedicated simulator before changing host code.
Recommended path¶
- Use the option B probe as the independent reader for D and the cold-restart check.
- Default to non-fatal
PHOTOS_FULL_ACCESS_UNVERIFIEDafter dispatch, with a warning to launch the app and checkPHPhotoLibrary authorizationStatusForAccessLevel:ReadWrite. Add-only is insufficient;photos-addremains a separate dispatch success. - Verify a sequence with the Playground probe before claiming support for any simulator/runtime range.
- Option C for SDK-integrated apps to allow a real
verified: true.
Device checks owed¶
- Build/launch the Playground on a dedicated simulator; confirm
photos-authorization.jsonshowsnotDeterminedfor both levels on a fresh install. - Repeat the issue’s grant matrix against it, recording
readWrite/addOnlyper sequence plus a cold restart after the best one. - Try grant-after-launch and a raw TCC write (D); record Xcode and runtime versions.
Implied add-only readback (#12462)¶
Inferring photos-add from a full kTCCServicePhotos grant (auth_value 2) applies only on simulator
runtime major >= 27 (threshold ORCHESTRATOR-CHOSEN, #12486). The 27.0 capture in
test/fixtures/ios-tcc-photos/ shows add-only authorized even with a stale PhotosAdd row; the iOS 26.5
set shows PhotoKit addOnly notDetermined after reset+grant photos and denied after revoke+grant
photos. Runtime version comes from device.iosVersion or device.osVersion; with an unknown runtime,
readback reports the stored PhotosAdd row. Gating on the Photos row’s auth_version (27.0 wrote 2, 26.5
wrote 1) was rejected as too indirect and undocumented.