Skip to content

fix(ios): own the media upload delegate instead of holding it weakly - #625

Draft
jkmassel wants to merge 1 commit into
fix/media-server-site-root-guardfrom
fix/own-media-delegate-strongly
Draft

fix(ios): own the media upload delegate instead of holding it weakly#625
jkmassel wants to merge 1 commit into
fix/media-server-site-root-guardfrom
fix/own-media-delegate-strongly

Conversation

@jkmassel

@jkmassel jkmassel commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Stacked on #624. Second of ten PRs splitting #621.

What?

A host that releases its mediaUploadDelegate while 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 for processFile and uploadFile. Those reads are separated by a synchronous disk copy and an unbounded processFile. Held weakly, a host releasing its delegate in that window changes the answer between them: a file admitted for processing is delivered untouched.

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 the server's container holds it. What it did buy was the reference vanishing mid-request.

How?

  • MediaUploadServer.swift: UploadContext holds the delegate strongly and becomes a struct, dropping its @unchecked Sendable opt-out — MediaUploadDelegate is Sendable and DefaultMediaUploader is @unchecked Sendable, so it is implicitly Sendable. Immutable strong references make the three reads agree by construction.
  • EditorViewController.swift: mediaUploadDelegate becomes strong. 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. Hosts no longer need to retain the delegate themselves.

SwiftLint's weak_delegate is 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

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.

  • Both fail against a weak container, with the real symptom: passthroughUploadCalled → true
  • swift test — host suite green
  • iOS Simulator xcodebuild
  • SwiftLint clean

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.
@jkmassel jkmassel added [Type] Bug An existing feature does not function as intended iOS labels Sep 5, 2026
@jkmassel jkmassel self-assigned this Sep 5, 2026
@wpmobilebot

Copy link
Copy Markdown

XCFramework Build

This PR's XCFramework is available for testing. Add the following to your Package.swift:

.package(url: "https://github.com/wordpress-mobile/GutenbergKit", branch: "pr-build/625")

Built from 95ae0f0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

iOS [Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants