[memory-leak] Fix native use-after-free in SKRegion.SpanIterator (root the SKRegion)#4355
Conversation
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>
There was a problem hiding this comment.
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
SKRegioninsideSKRegion.SpanIteratorvia 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. |
|
📖 Documentation Preview The documentation for this PR has been deployed and is available at: 🔗 View Staging Site This preview will be updated automatically when you push new commits to this PR. This comment is automatically updated by the documentation staging workflow. |
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>
📦 Try the packages from this PRWarning 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 -- 4355PowerShell / 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-4355More options
Or download manually from Azure Pipelines — look for the Remove the source when you're done: dotnet nuget remove source skiasharp-pr-4355 |
|
Updated to root the Behaviour is unchanged and |
… 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>
The leak
SKRegion.SpanIterator(binding/SkiaSharp/SKRegion.cs) handed the region's handle to nativesk_region_spanerator_new(...)but — unlike its sibling iteratorsRectIteratorandClipIterator— kept no managed reference to theSKRegion. NativeSkRegion::Spaneratorholds a raw pointer into the region's run data, andSKRegionisISKSkipObjectRegistration(so it is not rooted by theHandleDictionary). If theSKRegionwrapper was finalized while aSpanIteratorwas still alive, the native runs were freed underneath the iterator → use-after-free.The fix
Mirror the two sibling iterators: store the
SKRegionin aprivate 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), matchesRectIterator(:315) andClipIterator(:347) exactly, and changes no public signature (ABI-safe).Proof (red → green)
Added regression test
SpanIteratorKeepsItsRegionAlive(tests/Tests/SkiaSharp/SKRegionTest.cs): a[MethodImpl(NoInlining)]helper returns a liveSpanIteratorplus aWeakReferenceto 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.failed: 1— "The SKRegion was collected while its SpanIterator was still alive."succeeded: 1; the wholeSKRegionTestclass is 22/22 green.Commands (headless linux-x64; TFM pinned to avoid absent mobile workloads):
Scope
binding/SkiaSharp/SKRegion.cs); no*.generated.cs, noexternals/skia/**.Fixes #4354