Skip to content

[memory-leak] Fix native use-after-free in SKRegion.SpanIterator (root the SKRegion)#4355

Open
github-actions[bot] wants to merge 2 commits into
mainfrom
dev/memory-leak-skregion-spaniterator-65fa9da9fc1e4543
Open

[memory-leak] Fix native use-after-free in SKRegion.SpanIterator (root the SKRegion)#4355
github-actions[bot] wants to merge 2 commits into
mainfrom
dev/memory-leak-skregion-spaniterator-65fa9da9fc1e4543

Conversation

@github-actions

@github-actions github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🤖 AI-generated fix. Produced automatically by the SkiaSharp Memory Leak Fixer agentic workflow using the memory-leak-fixer skill, and empirically validated (red→green) on linux-x64 against the milestone's pre-built natives.

The leak

SKRegion.SpanIterator (binding/SkiaSharp/SKRegion.cs) handed the region's handle to native sk_region_spanerator_new(...) but — unlike its sibling iterators RectIterator and ClipIterator — kept no managed reference to the SKRegion. Native SkRegion::Spanerator holds a raw pointer into the region's run data, and SKRegion is ISKSkipObjectRegistration (so it is not rooted by the HandleDictionary). If the SKRegion wrapper was finalized while a SpanIterator was still alive, the native runs were freed underneath the iterator → use-after-free.

The fix

Mirror the two sibling iterators: store the SKRegion in a private readonly SKRegion region; field and assign it in the constructor, so the region stays rooted for the iterator's whole lifetime. This is the idiomatic family-5 fix (add a managed root), matches RectIterator (:315) and ClipIterator (:347) exactly, and changes no public signature (ABI-safe).

public class SpanIterator : SKObject, ISKSkipObjectRegistration
{
    private readonly SKRegion region;

    internal SpanIterator (SKRegion region, int y, int left, int right)
        : base (SkiaApi.sk_region_spanerator_new (region.Handle, y, left, right), true)
    {
        this.region = region;
    }
    // ...
}

Proof (red → green)

Added regression test SpanIteratorKeepsItsRegionAlive (tests/Tests/SkiaSharp/SKRegionTest.cs): a [MethodImpl(NoInlining)] helper returns a live SpanIterator plus a WeakReference to its region (the iterator being the only strong root); the test forces GC and asserts the region is still alive and the iterator still yields the correct intercepts.

  • Without the fix: failed: 1"The SKRegion was collected while its SpanIterator was still alive."
  • With the fix: succeeded: 1; the whole SKRegionTest class is 22/22 green.

Commands (headless linux-x64; TFM pinned to avoid absent mobile workloads):

dotnet cake --target=externals-download
dotnet build tests/SkiaSharp.Tests.Console/SkiaSharp.Tests.Console.csproj \
  -p:AllTargetFrameworks=net10.0 -p:BasicTargetFrameworks=net10.0 -f net10.0
dotnet exec tests/SkiaSharp.Tests.Console/bin/Debug/net10.0/SkiaSharp.Tests.dll \
  --filter-method "*SpanIteratorKeepsItsRegionAlive*"

Scope

  • Managed-C# only (binding/SkiaSharp/SKRegion.cs); no *.generated.cs, no externals/skia/**.
  • No public API/ABI change (adds a private field + assignment in an internal ctor).
  • Empirically proven, not just statically reasoned.

Fixes #4354

Generated by Fixer - Memory Leak · ● 64.8M ·

SKRegion.SpanIterator passed the region handle to the native
sk_region_spanerator_new but, unlike its sibling iterators RectIterator
and ClipIterator, kept no managed reference to the SKRegion. The native
SkRegion::Spanerator holds a raw pointer into the region's run data, so
once the SKRegion wrapper (which is ISKSkipObjectRegistration and thus
not rooted by the HandleDictionary) was finalized while a SpanIterator
was still alive, the native runs were freed underneath the iterator,
producing a use-after-free.

Mirror the two sibling iterators by storing the SKRegion in a private
readonly field so it stays rooted for the iterator's whole lifetime.

Adds a red->green regression test (SpanIteratorKeepsItsRegionAlive) that
fails without this fix (the region is collected while the iterator is
alive) and passes with it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added the partner/agentic-workflows Issues and PRs created by SkiaSharp agentic workflows. label Jul 7, 2026
@mattleibow
mattleibow marked this pull request as ready for review July 7, 2026 15:02
@mattleibow
mattleibow requested a review from Copilot July 7, 2026 15:02
mattleibow
mattleibow previously approved these changes Jul 7, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a native use-after-free hazard in SKRegion.SpanIterator by ensuring the iterator keeps its source SKRegion rooted for the iterator’s lifetime, aligning its retention behavior with RectIterator and ClipIterator.

Changes:

  • Root SKRegion inside SKRegion.SpanIterator via a private readonly field assigned in the iterator constructor.
  • Add a regression test that forces GC/finalization and validates the region remains alive while the iterator is alive and usable.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
binding/SkiaSharp/SKRegion.cs Adds a private SKRegion field to SpanIterator to keep the source region alive and prevent native use-after-free.
tests/Tests/SkiaSharp/SKRegionTest.cs Adds a regression test ensuring SpanIterator keeps its region alive across forced GC and remains functional.

Comment thread tests/Tests/SkiaSharp/SKRegionTest.cs
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

📖 Documentation Preview

The documentation for this PR has been deployed and is available at:

🔗 View Staging Site
🔗 View Staging Docs
🔗 View Staging Gallery (Blazor)
🔗 View Staging Gallery (Uno Platform)
🔗 View Staging SkiaFiddle

This preview will be updated automatically when you push new commits to this PR.


This comment is automatically updated by the documentation staging workflow.

mattleibow added a commit that referenced this pull request Jul 7, 2026
Add focus_family override to memory-leak-fixer workflow (#4356)

Changes: main...dev/memory-leak-fixer-focus-override

The "Fixer - Memory Leak" agentic workflow picks which leak family to deep-scan
via a time-based round-robin (day-of-year + hour, mod 11). That keeps unattended
runs varied, but it also means any *one* family only gets a deep scan when the
clock happens to rotate onto it — roughly once every 11 hours. There was no way
to point the scanner at a specific family on demand, which made it hard to
exercise the finding-issue -> fix-PR pairing end to end while iterating.

The first real run (28829268499) made this concrete: its focus landed on
family 1 (Wrong `owns:`), a hardened area, so it correctly emitted a `noop`.
Correct behavior, but not testable — we couldn't force it onto, say, the
finalizer/collection-ordering family that covers the SKRegion iterators.

Add an optional `focus_family` workflow_dispatch input (0-10, blank = rotate):

  * When set to a bare number, Step 1 passes it to the skill's Phase 1.1, which
    now honors an explicit override and skips the round-robin.
  * When blank (schedule / pull_request / dispatch without it), the rotation is
    unchanged.
  * Independent of `dry_run`, so `focus_family=5 dry_run=false` can force a real
    run on a chosen family.

Rendered through the same gh-aw path as the existing `dry_run`/`event_name`
expressions: gh-aw auto-wires `GH_AW_GITHUB_EVENT_INPUTS_FOCUS_FAMILY` onto the
prompt-building step and substitutes it into the runtime-imported prompt. Lock
recompiled clean (0 errors, 0 warnings).

Validated live: the PR self-test dry-run (28844087566, blank input -> rotation)
passed; a dispatched `focus_family=5` run (28844126750) opened its report with
"Focus family: 5", ran a full red->green probe, and de-duped to a noop. A
second `focus_family=5` run (28882528714) found the same SKRegion.SpanIterator
candidate but stood down against the already-open issue #4354 + fix PR #4355,
proving the de-dup guardrail on demand.

Workflow + skill only; no product code changes (ABI-neutral).

Co-authored-by: Matthew Leibowitz <mattleibow@live.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Root the SKRegion in all three iterators (Rect/Clip/Span) via the framework's
built-in Referenced(this, region) helper rather than a private readonly field,
so the whole iterator family uses the canonical keep-alive mechanism
consistently (no mixed styles). Behaviour is unchanged - the region stays
rooted for the iterator's lifetime and is released on dispose - and the
SpanIteratorKeepsItsRegionAlive regression test still passes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

📦 Try the packages from this PR

Warning

Do not run these scripts without first reviewing the code in this PR.

Step 1 — Download the packages

bash / macOS / Linux:

curl -fsSL https://raw.githubusercontent.com/mono/SkiaSharp/main/scripts/get-skiasharp-pr.sh | bash -s -- 4355

PowerShell / Windows:

iex "& { $(irm https://raw.githubusercontent.com/mono/SkiaSharp/main/scripts/get-skiasharp-pr.ps1) } 4355"

Step 2 — Add the local NuGet source

dotnet nuget add source ~/.skiasharp/hives/pr-4355/packages --name skiasharp-pr-4355
More options
Option Description
--successful-only / -SuccessfulOnly Only use successful builds
--force / -Force Overwrite previously downloaded packages
--list / -List List available artifacts without downloading
--build-id ID / -BuildId ID Download from a specific build

Or download manually from Azure Pipelines — look for the nuget artifact on the build for this PR.

Remove the source when you're done:

dotnet nuget remove source skiasharp-pr-4355

@mattleibow

Copy link
Copy Markdown
Contributor

Updated to root the SKRegion via SkiaSharp's built-in Referenced(this, region) helper instead of a hand-rolled private readonly SKRegion field. Applied to all three iterators (RectIterator/ClipIterator/SpanIterator) so the whole family uses the canonical keep-alive mechanism consistently rather than mixing styles.

Behaviour is unchanged and SpanIteratorKeepsItsRegionAlive still passes (full suite: same results as base — the only failure is the pre-existing ganesh-gl/DiagonalLines GPU golden flake, unrelated to this change). The keep-alive helper is documented in #4377.

mattleibow added a commit that referenced this pull request Jul 8, 2026
… hand-rolled fields (#4377)

Teach memory-leak-fixer to use SKObject keep-alive helpers instead of hand-rolled fields (#4377)

Context: documentation/dev/memory-management.md
Related: #4372, #4355

SkiaSharp already has a built-in mechanism for the "root a related object so
the GC can't collect it while its owner lives" problem: SKObject's static
`Referenced(owner, child)` (backed by the per-object `KeepAliveObjects`
dictionary), plus `Owned`/`OwnedBy` (backed by `OwnedObjects`) for the
root-and-dispose case. `Referenced` is used idiomatically by SKDocument,
SKColorSpace, and SKSVG — but it was never documented in the authoritative
memory-management model, and the memory-leak-fixer skill's area 5 taught a
hand-rolled `private readonly` field (or a `List<T>`) instead.

Because the guidance never pointed at the real feature, recent generated leak
fixes reinvented it: #4372 hand-rolled a `List<SKCanvas>` plus a field, and
#4355 added a `private readonly SKRegion` field. This PR closes that gap so
future runs reach for the framework mechanism instead.

Changes:

  * documentation/dev/memory-management.md — new "Keeping Related Objects
    Alive" section documenting Referenced / Unreferenced / UnreferencedAll and
    Owned / OwnedBy, and the KeepAliveObjects vs OwnedObjects dictionaries
    (roots-only vs roots-and-disposes).
  * memory-leak-fixer/references/types-of-leaks.md — area 5 now teaches
    `Referenced(this, parent)` as the idiomatic fix, with the mutable-set
    variant (`Unreferenced` / `UnreferencedAll`); keeps a plain field as an
    accepted local-convention fallback (e.g. the SKRegion iterators) to avoid
    mixed styles; updates the Where-to-look / How-to-find-it / Watch-out notes.
  * memory-leak-fixer/SKILL.md — self-review gate now flags a hand-rolled
    keep-alive field/List<T> where the helpers suffice.

Docs/skill only — no binding code changes here. The symmetric inverse helpers
(`Unreferenced`, `UnreferencedAll`) that the docs reference are introduced and
consumed in #4372; #4355 consumes the pre-existing `Referenced`.

Co-authored-by: Matthew Leibowitz <mattleibow@live.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

partner/agentic-workflows Issues and PRs created by SkiaSharp agentic workflows.

Projects

Status: Approved

Development

Successfully merging this pull request may close these issues.

[memory-leak] Native use-after-free: SKRegion.SpanIterator does not root its SKRegion

2 participants