Skip to content

Fix finalizer crash in SKPath when SKPathBuilder is collected first#3796

Merged
mattleibow merged 1 commit into
mono:mainfrom
ramezgerges:dev/fix-skpath-finalizer-crash
May 5, 2026
Merged

Fix finalizer crash in SKPath when SKPathBuilder is collected first#3796
mattleibow merged 1 commit into
mono:mainfrom
ramezgerges:dev/fix-skpath-finalizer-crash

Conversation

@ramezgerges

@ramezgerges ramezgerges commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Description of Change

Repro from a downstream app on 3.119: the finalizer thread crashed in sk_pathbuilder_detach_path with this stack:

SkiaApi.sk_pathbuilder_detach_path(IntPtr)
SKPath.FlushBuilder()
SKPath.get_Handle()
SKNativeObject.Dispose(Boolean)
SKObject.Dispose(Boolean)
SKPath.Dispose(Boolean)
SKNativeObject.Finalize()

SKPath holds a private SKPathBuilder for batched mutations and overrides Handle to flush pending mutations on every read. SKPathBuilder is itself an SKObject with its own finalizer and the CLR makes no guarantee about finalization order — when the builder finalizes first, its native pathbuilder is freed and its Handle is set to IntPtr.Zero. The path's finalizer then runs SKNativeObject.Dispose(false), which reads Handle on line 269 to check != IntPtr.Zero. That dispatches into our overridden getter, FlushBuilder calls
sk_pathbuilder_detach_path(_builder.Handle), and the C side dereferences nullptr (or a dangling pointer if the same address was reallocated).

Skip FlushBuilder once disposal has begun. The pending builder mutations are about to be discarded along with the path either way, and DisposeNative still cleans up _builder defensively (the CAS in SKNativeObject.Dispose makes a second Dispose a no-op if the builder already finalized).

Added a regression test that deterministically reproduces the crash without the fix (Test host process crashed) and passes with it.

Bugs Fixed

  • Fixes #

API Changes

None.

Behavioral Changes

None.

Required skia PR

None.

PR Checklist

  • Has tests (if omitted, state reason in description)
  • Rebased on top of main at time of PR
  • Merged related skia PRs
  • Changes adhere to coding standard
  • Updated documentation

@github-actions

github-actions Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

📦 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 -- 3796

PowerShell / Windows:

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

Step 2 — Add the local NuGet source

dotnet nuget add source ~/.skiasharp/hives/pr-3796/packages --name skiasharp-pr-3796
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-3796

@mattleibow

mattleibow commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

@mtmattei

Copy link
Copy Markdown

here is verbatim crash:

Environment:

  • Windows 11, .NET SDK 10.0.103 (band 100, latestFeature roll-forward)
  • Uno.Sdk 6.6.0-dev.208
  • SkiaSharp 4.147.0-preview.1.1 (managed + Views.Uno.WinUI + HarfBuzz)
  • TFM: net10.0-desktop
  • Uno SkiaRenderer feature enabled, UnoDisableLottieSkiaVersionCheck=true
  • App: minimal Uno Single Project shell with NavigationView + 3 pages,
    each rendering via Uno.WinUI.Graphics2DSK.SKCanvasElement subclasses

Crash (identical in Debug and Release, ~20-30s after window opens, no
user interaction required):

Fatal error.
0xC0000005
at SkiaSharp.SkiaApi.sk_pathbuilder_detach_path(IntPtr)
at SkiaSharp.SKPath.get_Handle()
at SkiaSharp.SKObject.Dispose(Boolean)
at SkiaSharp.SKPath.Dispose(Boolean)
at SkiaSharp.SKNativeObject.Finalize()
at System.GC.RunFinalizers()

The app code never directly constructs SKPath — these are paths created
inside Uno's own rendering pipeline. Suggests an SKPath/SKPathBuilder
lifecycle bug at the Uno-preview/SkiaSharp-4.147-preview boundary
(double-free, or native handle freed before managed finalizer runs).

@ramezgerges
ramezgerges force-pushed the dev/fix-skpath-finalizer-crash branch from 0118d89 to 9db03cf Compare April 30, 2026 14:46
@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

Repro from a downstream app on 3.119: the finalizer thread crashed in
sk_pathbuilder_detach_path with this stack:

    SkiaApi.sk_pathbuilder_detach_path(IntPtr)
    SKPath.FlushBuilder()
    SKPath.get_Handle()
    SKNativeObject.Dispose(Boolean)
    SKObject.Dispose(Boolean)
    SKPath.Dispose(Boolean)
    SKNativeObject.Finalize()

SKPath holds a private SKPathBuilder for batched mutations and overrides
Handle to flush pending mutations on every read. SKPathBuilder is itself
an SKObject with its own finalizer and the CLR makes no guarantee about
finalization order — when the builder finalizes first, its native
pathbuilder is freed and its Handle is set to IntPtr.Zero. The path's
finalizer then runs SKNativeObject.Dispose(false), which reads Handle
on line 269 to check `!= IntPtr.Zero`. That dispatches into our
overridden getter, FlushBuilder calls
sk_pathbuilder_detach_path(_builder.Handle), and the C side dereferences
nullptr (or a dangling pointer if the same address was reallocated).

Skip FlushBuilder once disposal has begun. The pending builder mutations
are about to be discarded along with the path either way, and
DisposeNative still cleans up _builder defensively (the CAS in
SKNativeObject.Dispose makes a second Dispose a no-op if the builder
already finalized).

Added a regression test that deterministically reproduces the crash
without the fix (Test host process crashed) and passes with it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ramezgerges
ramezgerges force-pushed the dev/fix-skpath-finalizer-crash branch from 9db03cf to cf50c6f Compare May 1, 2026 17:34
@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

@mattleibow
mattleibow enabled auto-merge (squash) May 5, 2026 15:10
@mattleibow
mattleibow disabled auto-merge May 5, 2026 15:10
@mattleibow
mattleibow merged commit 87b59aa into mono:main May 5, 2026
5 of 6 checks passed
@mattleibow mattleibow added this to the 4.x Preview 2 milestone May 19, 2026
mattleibow added a commit that referenced this pull request Jul 3, 2026
Expand the skill's scan taxonomy from 6 to 12 families, each backed by a
real historical SkiaSharp leak fix (finalizer/collection ordering #3796/#3291,
Clone double-free #2904, disposing native statics #1863/#4080/#1224,
field-not-nulled #1256/#1344, stream/callback/delegate-proxy lifetime
#3589/#2916/#996, allocation-failure #1784/#1642). Broaden Phase 1.3 de-dup to
search by api/type name (real leaks are filed as [BUG], not [memory-leak]) and
add the Blob.FromStream / PR #3473 worked example. Extend the Phase 3.2 fix
table to cover all 12 families. Document two verified, un-filed family-6
candidates surfaced by a model-diverse scan (SKRegion.SpanIterator missing
parent ref; SKPixmap.ExtractSubset/With* not propagating pixelSource).

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

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants