Skip to content

fix: import device photos under iOS Lockdown Mode - #26006

Draft
jkmassel wants to merge 2 commits into
trunkfrom
jkmassel/lockdown-media-handoff
Draft

fix: import device photos under iOS Lockdown Mode#26006
jkmassel wants to merge 2 commits into
trunkfrom
jkmassel/lockdown-media-handoff

Conversation

@jkmassel

@jkmassel jkmassel commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Under iOS Lockdown Mode, importing a high-megapixel device photo fails outright. This makes it succeed.

Builds on #25997, which made the failure graceful and added the detection primitive this gates on.

Summary

  • The system's PhotosFileProvider extension performs a hardened full decode when it materializes a picked image under Lockdown Mode, and is killed at its 20 MB memory limit.
  • When Lockdown Mode is on, read the picked file with PHAssetResourceManager — serviced by photolibraryd, which has no such cap — instead of NSItemProvider.loadFileRepresentation.
  • Outside Lockdown Mode nothing changes: the same permission-free picker, the same item-provider path, no Photos prompt.
  • The two routes are exclusive. There is no per-asset fallback, so a failure is visible rather than silent.

Root Cause

Lockdown Mode forces a hardened full decode when the system materializes an image for loadFileRepresentation. A 36 MP photo needs roughly 36e6 x 4 bytes — about 144 MB — against the extension's 20 MB ActiveHard limit, so the extension is killed:

PhotosFileProvider [pid] exceeded mem limit: ActiveHard 20 MB (fatal)

which surfaces as NSItemProviderError -1000 wrapping NSCocoaErrorDomain 4099.

The cost tracks megapixels, not file size. Videos stream without a decode and are unaffected, which is why the bug looks intermittent: ordinary photos, screenshots and videos all import fine, and only large photos fail.

Fix

1. Read the file from the photo library under Lockdown Mode

WordPress/Classes/Utility/Media/PhotoLibraryFileLoader.swift (new): streams an asset's file with PHAssetResourceManager.writeData, and loads a bounded image with PHImageManager for the flows that crop rather than upload.

Resource preference is scoped by the asset's own media type — .image prefers [.fullSizePhoto, .photo, .alternatePhoto], .video prefers [.fullSizeVideo, .video]. The fullSize variants are the current rendition of an edited asset, so an edited photo uploads with its edits. Scoping by media type also keeps a Live Photo, which carries both a .photo and a .pairedVideo resource, from uploading the wrong half.

2. Reuse the existing export pipeline

WordPress/Classes/Utility/Media/ItemProviderMediaExporter.swift: takes an optional assetIdentifier and a prefersPhotoLibrarySource switch, defaulting to LockdownHelper.isDeviceLockdownModeEnabled.

Both routes share process(fileAt:resourceTypeIdentifier:), so type routing, the unsupported-format fallback to .jpeg, GPS stripping, Progress and #25997's error surfacing are identical either way. Reusing the exporter also gets deterministic cleanup for free — the streamed file lands in the staging directory the exporter already deletes.

We deliberately did not hand the streamed file to coordinator.addMedia(from: fileURL as NSURL). That path uses MediaURLExporter, which has neither the HEIC-to-JPEG fallback nor the same allowable-extension handling, so a HEIC — the iPhone default — would have uploaded as HEIC, which self-hosted sites don't accept.

3. Require Full Access under Lockdown Mode

WordPress/Classes/ViewRelated/Media/PhotosPickerPresenter.swift (new): presents a library-backed picker under Lockdown Mode, because only that supplies result.assetIdentifier.

PHPickerViewController displays the whole library regardless of what the app has been granted. Under limited authorization it would therefore offer items the app can't then read, so anything short of full access is refused up front with an alert offering Open Settings. The prompt only ever appears under Lockdown Mode.

Limited and denied get the same answer deliberately. Letting denied fall through to the permission-free picker — as #25997 does today — while blocking limited would invert user intent: granting some access would leave you worse off than granting none.

4. One currency for picked media

WordPress/Classes/ViewRelated/Media/PhotosPickerAsset.swift (new) pairs the item provider with the asset identifier, and DevicePhotosPickerDelegate replaces PHPickerViewControllerDelegate as the shared callback. That covers the Media Library +, the SwiftUI MediaPicker, Gutenberg, Aztec's full-screen action, the site icon and the avatar in one shape, and keeps the picker choice an implementation detail.

What We Explored

Legacy UIImagePickerController, so the picker only shows granted assets. This does not work. It runs out-of-process too — it presents _UIImagePickerPlaceholderViewController, a remote view controller host, which produces a long delay and the same full-library UI — and since limited authorization was introduced in iOS 14 it leaves info[.phAsset] nil whenever access is .limited, even for assets the app may read. So it supplies an asset only under full authorization, which is exactly where the library-backed picker already works. Documented in the developer forums rather than the API docs; the docs carry only the deprecation.

No system picker can be scoped to the app's granted assets. Doing that means building a grid on PHAsset.fetchAssets, which would also restore multiple selection under Lockdown Mode. Not attempted here.

Falling back to the item provider when an asset can't be resolved. Removed: in Lockdown Mode the item provider is precisely what can't serve the file, so retrying through it trades a clear failure for a silent one.

Test plan

Device testing on an iPhone 15 Pro (iOS 27.0) with Lockdown Mode on, against a self-hosted site. The same device reproduced the original failure before the change (media_import_item_unavailable, lockdown_mode: true).

  • 36 MP photo imports. Streaming picked asset 1 (.photo), no exceeded mem limit, ~102 ms to disk.
  • The export is byte-identical to a pre-change item-provider import of the same photo — bytes: 61651968 both times — so the two routes produce the same upload.
  • A second, differently sized photo imports, confirming it isn't specific to one asset.
  • A 42-second video imports. Streaming picked asset 2 (.video).
  • Camera capture (photo and video) is unaffected by the delegate change.
  • Limited authorization is refused with the Full Access alert.
  • Denied authorization (Photos set to None) is refused with the same alert.
  • Unit tests: 41 XCTest and 24 Swift Testing green, including the existing HEIC-to-JPEG, WebP, GPS-stripping and GIF cases that pin the non-Lockdown path.
  • Outside Lockdown Mode, confirm no Photos prompt appears and imports behave as before.

Notes

One gap is known and left in place:

Aztec's inline keyboard picker keeps plain PHPickerViewController. It is embedded as an input view with .continuousAndOrdered selection, which nothing else provides, so it retains the old behaviour under Lockdown Mode. Aztec's full-screen "Device Photos" action does get the new path.

Only .fullSizeVideo — an edited video — is covered by unit test rather than on device.

megapixels: 0 in the media_library_photo_added event is a pre-existing analytics bug, present on both builds. Not addressed here.

Under Lockdown Mode the system's PhotosFileProvider extension performs a
hardened full decode when it materializes a picked image, and is killed at
its 20 MB memory limit. A 36 MP photo needs ~144 MB, so importing one fails
outright: `loadFileRepresentation` returns `NSItemProviderError -1000` over
`NSCocoaErrorDomain 4099`.

When Lockdown Mode is on, present a library-backed `PHPickerViewController`
and read the picked file with `PHAssetResourceManager`, which is serviced by
`photolibraryd` and has no such cap. Outside Lockdown Mode nothing changes:
the same permission-free picker, the same item-provider path, no prompt.

The two routes are exclusive. A failure reading from the library is reported
rather than retried through the item provider, which in Lockdown Mode is the
thing that cannot serve the file. Limited Photos authorization is refused up
front, because the picker shows the whole library whatever the app has been
granted and would otherwise offer items it can't read.

Builds on #25997, which made the failure graceful and added the detection
primitive this gates on.
@jkmassel jkmassel self-assigned this Sep 8, 2026
@jkmassel jkmassel added this to the 27.3 milestone Sep 8, 2026
@dangermattic

Copy link
Copy Markdown
Collaborator
1 Warning
⚠️ This PR is larger than 500 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.
1 Message
📖 This PR is still a Draft: some checks will be skipped.

Generated by 🚫 Danger

@wpmobilebot

wpmobilebot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor
App Icon📲 You can test the changes from this Pull Request in WordPress by scanning the QR code below to install the corresponding build.
App NameWordPress
ConfigurationRelease-Alpha
Build Number34430
VersionPR #26006
Bundle IDorg.wordpress.alpha
Commit12ab678
Installation URL063im7442qd50
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot

wpmobilebot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor
App Icon📲 You can test the changes from this Pull Request in Jetpack by scanning the QR code below to install the corresponding build.
App NameJetpack
ConfigurationRelease-Alpha
Build Number34430
VersionPR #26006
Bundle IDcom.jetpack.alpha
Commit12ab678
Installation URL6o3jebh3h1l40
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

Denied access previously fell through to the permission-free picker while
limited access was refused outright, which inverted user intent: granting
*some* access left you worse off than granting none. Both now get the same
answer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants