Skip to content

Bug: [Memory leak] retain cycle in GutenbergCoverUploadProcessor #25441

Description

@amanjeetsingh150

Summary

GutenbergCoverUploadProcessor has a retain cycle that causes it to never be deallocated after a cover block upload.

Root Cause

The lazy var coverBlockProcessor creates a GutenbergBlockProcessor with a replacer closure that strongly captures self:

// GutenbergCoverUploadProcessor.swift
lazy var coverBlockProcessor = GutenbergBlockProcessor(for: CoverBlockKeys.name, replacer: { coverBlock in
    guard let mediaID = coverBlock.attributes[CoverBlockKeys.id] as? Int,
        mediaID == self.mediaUploadID else {  // strong capture of self
            return nil
    }
    ...
    let innerProcessor = self.isVideo(attributes) ? self.videoUploadProcessor() : self.imgUploadProcessor()
    ...
})

This creates a cycle:

GutenbergCoverUploadProcessor
  → lazy var coverBlockProcessor (strong)
    → GutenbergBlockProcessor.replacer closure (strong)
      → self (GutenbergCoverUploadProcessor) ← cycle

Detection

Detected using XCTestLeaks — a tool that runs leaks(1) after each XCTest case via a dylib shim.

Results from GutenbergCoverUploadProcessorTests before fix:

leaks=1  testCoverBlockProcessor
leaks=2  testCoverBlockProcessorWithOtherAttributes
leaks=3  testDeepNestedCoverBlockProcessor
leaks=4  testImageCoverInVideoCoverBlockProcessor
leaks=5  testMultipleCoverBlocksProcessor
leaks=5  testNestedCoverBlockProcessor
leaks=7  testUpdateOuterCoverBlockProcessor
leaks=7  testVideoCoverBlockProcessor
leaks=7  testVideoCoverInImageCoverBlockProcessor

After fix: all tests show leaks=0.

Fix

Use [weak self] in the replacer closure:

lazy var coverBlockProcessor = GutenbergBlockProcessor(for: CoverBlockKeys.name, replacer: { [weak self] coverBlock in
    guard let self,
          let mediaID = coverBlock.attributes[CoverBlockKeys.id] as? Int,
          mediaID == self.mediaUploadID else {
            return nil
    }
    ...
})

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions