fix(ios): own the media upload delegate instead of holding it weakly - #625
Draft
jkmassel wants to merge 1 commit into
Draft
fix(ios): own the media upload delegate instead of holding it weakly#625jkmassel wants to merge 1 commit into
jkmassel wants to merge 1 commit into
Conversation
The server reads the delegate three times per request — once at the admission gate (`handlesFile`), then again for `processFile` and `uploadFile` — and those reads are separated by a synchronous disk copy and an unbounded `processFile`. Held weakly, a host that released its delegate in that window changed the answer between reads: a file admitted for processing was forwarded to WordPress unprocessed. Hold it strongly, as Android already does with a plain `val`. Immutable strong references make the three reads agree by construction, and an in-flight upload keeps the delegate alive until it unwinds. The `weak` bought no leak protection to trade away. The cycle it named runs through `EditorViewController.mediaUploadDelegate` — a host object retaining the view controller forms `EditorViewController -> delegate -> EditorViewController` regardless of how this container holds it. What it did buy was the reference vanishing mid-request. So `mediaUploadDelegate` becomes strong too, and the machinery that existed only to police the old contract goes with it: `mediaUploadDelegateWasAssigned` and the released-before-load trap have nothing left to catch, because the editor now owns the delegate for its lifetime. Hosts no longer need to retain it themselves. `UploadContext` becomes a struct and drops its `@unchecked Sendable` opt-out: `MediaUploadDelegate` is `Sendable` and `DefaultMediaUploader` is `@unchecked Sendable`, so it is implicitly Sendable. `doesNotStronglyRetainDelegate` pinned the invariant being removed, so it is replaced by `retainsDelegateForServerLifetime`, asserting both halves — the server owns the delegate while it runs, and releases it afterward. `processesForHostReleasedDelegate` covers the bug directly; against a weak container it fails with the real symptom, `passthroughUploadCalled`. SwiftLint's `weak_delegate` is suppressed with the reasoning inline. The rule is arguably right that the name no longer fits — a later commit renames the property, and the suppression goes away with it.
This was referenced Sep 5, 2026
XCFramework BuildThis PR's XCFramework is available for testing. Add the following to your .package(url: "https://github.com/wordpress-mobile/GutenbergKit", branch: "pr-build/625")Built from 95ae0f0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #624. Second of ten PRs splitting #621.
What?
A host that releases its
mediaUploadDelegatewhile an upload is in flight gets the file forwarded to WordPress unprocessed. Hold the delegate strongly, as Android already does.Why?
The server reads the delegate three times per request — once at the admission gate (
handlesFile), then again forprocessFileanduploadFile. Those reads are separated by a synchronous disk copy and an unboundedprocessFile. Held weakly, a host releasing its delegate in that window changes the answer between them: a file admitted for processing is delivered untouched.The
weakbought no leak protection to trade away. The cycle it named runs throughEditorViewController.mediaUploadDelegate— a host object retaining the view controller formsEditorViewController → delegate → EditorViewControllerregardless of how the server's container holds it. What it did buy was the reference vanishing mid-request.How?
UploadContextholds the delegate strongly and becomes astruct, dropping its@unchecked Sendableopt-out —MediaUploadDelegateisSendableandDefaultMediaUploaderis@unchecked Sendable, so it is implicitly Sendable. Immutable strong references make the three reads agree by construction.mediaUploadDelegatebecomes strong. The machinery that existed only to police the old contract goes with it —mediaUploadDelegateWasAssignedand the released-before-load trap have nothing left to catch. Hosts no longer need to retain the delegate themselves.SwiftLint's
weak_delegateis suppressed with the reasoning inline. The rule is arguably right that the name no longer fits; #630 renames the property and the suppression goes away with it.Testing Instructions
doesNotStronglyRetainDelegatepinned the invariant being removed, so it is replaced byretainsDelegateForServerLifetime, asserting both halves — the server owns the delegate while it runs, and releases it afterward.processesForHostReleasedDelegatecovers the bug directly.passthroughUploadCalled → trueswift test— host suite greenxcodebuild