Skip to content

[memory-leak] Fix CoTaskMem pixel-buffer leak in SKImage.Create raster failure path - #4455

Merged
mattleibow merged 6 commits into
mainfrom
dev/memory-leak-image-create-cotaskmem-7cb41f47978d8b90
Jul 22, 2026
Merged

[memory-leak] Fix CoTaskMem pixel-buffer leak in SKImage.Create raster failure path#4455
mattleibow merged 6 commits into
mainfrom
dev/memory-leak-image-create-cotaskmem-7cb41f47978d8b90

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Produced automatically by the Fixer - Memory Leak agentic workflow using the memory-leak-fixer skill (focus area 10 — allocation-failure path).

The fix

SKImage.Create(SKImageInfo) allocates an unmanaged pixel buffer with Marshal.AllocCoTaskMem and relies on the native raster image's release proc (SKImageRasterReleaseProxyForCoTaskMem, which calls Marshal.FreeCoTaskMem) to free it. That proc only runs when the native image is destroyed — so when sk_image_new_raster returns null (a valid-BytesSize info that Skia rejects, e.g. Rgba8888 + AlphaType.Unknown), no image is created, the proc never fires, and the buffer leaks.

The fix frees the buffer explicitly on the null-handle path — the idiomatic focus-area-10 pattern (return null on failure, but release any allocation made along the way):

var image = GetObject(SkiaApi.sk_image_new_raster(pixmap.Handle, DelegateProxies.SKImageRasterReleaseProxyForCoTaskMem, null));
if (image == null)
    Marshal.FreeCoTaskMem(pixels);
return image;

Managed-C# only (binding/SkiaSharp/SKImage.cs), no public signature change — ABI stable. The factory still returns null on failure (no exception-surface change).

Proof (red → green)

New regression test tests/Tests/SkiaSharp/SKImageTest.cs::FailedRasterCreateDoesNotLeakPixelBuffer drives 200 failing 4 MB Create calls and asserts private memory does not grow by a large fraction of what was allocated.

  • Without fix: FAIL — private memory grew 805 MB over 200 failed calls (~800 MB allocated → every buffer leaked).
  • With fix: PASS — growth ≈ 0. Full SKImageTest class green (45 passed / 7 hardware-skipped).

Commands (single-TFM, prebuilt natives via externals-download):

dotnet build tests/SkiaSharp.Tests.Console/SkiaSharp.Tests.Console.csproj \
  -f net10.0 -p:AllTargetFrameworks=net10.0 -p:BasicTargetFrameworks=net10.0 -p:PlatformTargetFrameworks=
dotnet SkiaSharp.Tests.dll --filter-method "*FailedRasterCreateDoesNotLeakPixelBuffer*"

Scope note

Framework bug, empirically proven. Managed-C# only, ABI-safe, internal-only change. Distinct from open PR #4453 (GCHandle leaks in FromPixels/FromTexture/SKSurface.Create/SKData.Create) — that PR does not touch SKImage.Create(SKImageInfo) or the CoTaskMem pixel buffer.

Labels: tenet/performance + perf/memory-leak.

Fixes #4454

Generated by Fixer - Memory Leak · ● 16.7M ·

SKImage.Create(SKImageInfo) allocates an unmanaged pixel buffer with
Marshal.AllocCoTaskMem and relies on the native raster image's release
proc to free it. When sk_image_new_raster fails (e.g. a valid-BytesSize
info Skia rejects), no image is created, the release proc never runs, and
the buffer leaks. Free it explicitly on the failure path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added partner/agentic-workflows Issues and PRs created by SkiaSharp agentic workflows. perf/memory-leak Unbounded memory growth: leaked native handles or undisposed objects. Implies tenet/performance. tenet/performance Performance related issues labels Jul 16, 2026
mattleibow added a commit that referenced this pull request Jul 17, 2026
…4476)

Add regression-tracking benchmarks for common colour/interop paths (#4476)

Derived from: #4370, #4385, #4442, #4453, #4455
CI: Track - Benchmarks run 29608575980 (green, all OS/roles)

The permanent regression-tracking suite (SkiaSharp.Benchmarks.Tracking) landed
recently alongside a wave of perf/agentic PRs, but it had no coverage for the
managed colour/interop paths those PRs actually touch. This adds a small,
curated set of trackers so a future regression (or the improvement itself) shows
up as a step in the persisted per-OS time/allocation history.

Following the suite's scaled-pyramid philosophy (a few small common-path trackers
plus one broad composite, not a micro-benchmark per PR), four benchmarks are
added under Tracking/Benchmarks/ (auto-linked into the source-mode project, so
they also drive the PR column):

  * ColorMathBenchmark - managed SKColor->SKColorF (#4370) and SKPMColor
    PreMultiply/UnPreMultiply (#4385). Zero-alloc; the ToColorF/ToColor ratio
    (the reverse operator stays native) is a managed-regression signal even on
    noisy shared runners. These two ports are the real, dashboard-visible speed
    wins - their time lines should drop when the ports merge.
  * RasterImageLifecycleBenchmark - create+dispose churn of SKImage.Create raster
    (#4455) and SKData.Create with a managed release proc (#4453). Allocation is
    the tracked signal for that object-lifecycle area.
  * RuntimeEffectShaderBenchmark - a per-frame animated SkSL shader: build
    uniforms (the SKColor uniform pays the #4370 convert), ToShader, draw. Covers
    the uniforms lifecycle whose SKData leak #4442 touched.
  * SceneRenderBenchmark - one broad frame (transforms, linear+radial gradients,
    filled/stroked primitives, a built path, a scaled image draw, a colour-filter
    layer, clipping, managed colour maths) so many unrelated future optimizations
    each nudge it - a merge indicator rather than a micro-benchmark.

Scope notes: the failure-path leak fixes (#4453, #4455), the native uniforms leak
(#4442), and the UAF/double-free fixes (#4372, #4468) are correctness/native-memory
changes that a throughput + managed-allocation dashboard largely will not register;
they are represented here by area, not as leak assertions (those belong in soak/
leak tests). #4372 and #4468 are intentionally not benchmarked (niche debug canvases;
HarfBuzz lives in a separate assembly the tracking project does not reference).

All benchmarks use public API only (the project references the nightly NuGet, not
internal SkiaApi), are deterministic and machine-independent (fixed seeds, no fonts
or external content), and compile against every benchmarked role - verified against
the 3.119.4 baseline (prev-major) and 4.151 nightly, with only SKPath/SKPathBuilder
behind the existing #if. A short local run and the full CI matrix (Linux/Windows/
macOS, all version roles plus the source-built PR column) emit time and allocation
data for every case.

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

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 -- 4455

PowerShell / Windows:

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

Step 2 — Add the local NuGet source

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

@github-actions

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.

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

This PR fixes an unmanaged memory leak in SKImage.Create(SKImageInfo) when raster image creation fails: the method allocates a CoTaskMem pixel buffer, but if the native factory returns null the release-proc never runs, so the buffer must be freed explicitly. It also adds a regression test to catch the leak.

Changes:

  • Free the CoTaskMem pixel buffer when sk_image_new_raster returns a null image.
  • Add a regression test that repeatedly hits the failure path and asserts process private memory doesn’t grow significantly.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
binding/SkiaSharp/SKImage.cs Frees the unmanaged pixel buffer on native raster-create failure to prevent leaks.
tests/Tests/SkiaSharp/SKImageTest.cs Adds a regression test that exercises the failure path repeatedly and checks for memory growth.

Comment thread binding/SkiaSharp/SKImage.cs
Comment thread tests/Tests/SkiaSharp/SKImageTest.cs Outdated
Comment thread tests/Tests/SkiaSharp/SKImageTest.cs Outdated
Comment thread tests/Tests/SkiaSharp/SKImageTest.cs Outdated
mattleibow and others added 3 commits July 20, 2026 17:19
The FailedRasterCreateDoesNotLeakPixelBuffer regression test measures the
leak via System.Diagnostics.Process.PrivateMemorySize64. That API is not
supported on WASM (no System.Diagnostics.Process at all), iOS, or Mac
Catalyst, so the test threw PlatformNotSupportedException and failed the
Tests WASM (Linux) and Tests iOS (macOS) legs (and Mac Catalyst).

Guard the test with SkipOnPlatform(IsBrowser || IsIOS || IsMacCatalyst),
matching the existing platform-skip pattern in this suite. The SKImage.Create
fix itself is platform-agnostic and remains exercised by the desktop and
Android test legs, where PrivateMemorySize64 is available.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4649389c-a7bd-443d-8c26-7c6b16cda819
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@mattleibow
mattleibow marked this pull request as ready for review July 22, 2026 21:53
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

📊 SkiaSharp benchmarks — PR #4455

this PR (full source build) vs 🌙 nightly · Linux · Windows · macOS

Informational only — this never blocks the PR. 🟢 faster / less allocation · 🔴 slower / more allocation; moves under 5% are hidden as noise.

⏱️ Times are raw BenchmarkDotNet means, and the ⭐ PR and baseline legs run on separate CI runners, so microbenchmarks can swing run-to-run — treat small time deltas as noise. Allocations are deterministic and the reliable signal. The interactive perf-dashboard (linked below) applies smoothing for the trend view.

Highlights

⏱️ Time — 🔴 28 slower · 🟢 35 faster

  • 🟢 RuntimeEffectShaderBenchmark.DrawFrame · Windows · 1.12 ms → 701.67 µs (-37%)
  • 🟢 ColorMathBenchmark.ToColor(Colors: 4096) · Windows · 15.52 µs → 10.66 µs (-31%)
  • 🟢 ColorMathBenchmark.UnPreMultiply(Colors: 4096) · Windows · 12.22 µs → 8.73 µs (-28%)
  • 🟢 ColorMathBenchmark.ToColor(Colors: 4096) · Linux · 15.39 µs → 11.76 µs (-24%)
  • 🟢 ColorMathBenchmark.PreMultiply(Colors: 4096) · Windows · 13.19 µs → 10.12 µs (-23%)
  • …and 58 more (see details below)
Full per-OS benchmark deltas

Linux

⏱️ Time (vs 🌙 nightly 4.151.0-nightly.83)

Benchmark baseline this PR Δ
ColorMathBenchmark.ToColor(Colors: 4096) 15.39 µs 11.76 µs 🟢 -24%
MatrixOpsBenchmark.Concat(Count: 4096) 217.38 µs 253.76 µs 🔴 +17%
MatrixOpsBenchmark.Invert(Count: 4096) 131.81 µs 150.32 µs 🔴 +14%
MatrixMapPointsBenchmark.MapPoint(Points: 4096) 71.94 µs 81.73 µs 🔴 +14%
MatrixMapPointsBenchmark.MapPoints(Points: 256) 204.4 ns 232.0 ns 🔴 +13%
ColorMathBenchmark.PreMultiply(Colors: 4096) 12.85 µs 14.49 µs 🔴 +13%
MatrixMapPointsBenchmark.MapPoint(Points: 256) 4.50 µs 5.08 µs 🔴 +13%
RasterImageLifecycleBenchmark.CreateDataWithReleaseProc(Count: 256) 98.98 µs 111.47 µs 🔴 +13%
MatrixMapPointsBenchmark.MapVector(Points: 4096) 171.72 µs 193.27 µs 🔴 +13%
ColorMathBenchmark.UnPreMultiply(Colors: 4096) 11.45 µs 12.65 µs 🔴 +10%
MatrixMapPointsBenchmark.MapRadius(Points: 256) 13.96 µs 15.33 µs 🔴 +10%
MatrixMapPointsBenchmark.MapVector(Points: 256) 10.79 µs 11.84 µs 🔴 +10%
MatrixMapPointsBenchmark.MapRect(Points: 256) 11.93 µs 12.81 µs 🔴 +7%
MatrixMapPointsBenchmark.MapRect(Points: 4096) 190.96 µs 204.88 µs 🔴 +7%
ColorMathBenchmark.ToColorF(Colors: 4096) 13.13 µs 14.07 µs 🔴 +7%
RuntimeEffectShaderBenchmark.DrawFrame 2.83 ms 3.03 ms 🔴 +7%
PathBoundsBenchmark.TightBounds(Points: 1024) 10.65 µs 11.39 µs 🔴 +7%
BitmapDrawBenchmark.DrawUnscaledTiles(Tiles: 64) 251.27 µs 267.95 µs 🔴 +7%
CanvasDrawBenchmark.Draw(Shapes: 64) 4.16 ms 4.41 ms 🔴 +6%
CanvasDrawBenchmark.Draw(Shapes: 512) 33.24 ms 35.17 ms 🔴 +6%
BitmapDrawBenchmark.DrawUnscaledTiles(Tiles: 256) 940.85 µs 994.12 µs 🔴 +6%
BitmapDrawBenchmark.DrawScaledTiles(Tiles: 64) 355.99 µs 376.05 µs 🔴 +6%
BitmapDrawBenchmark.DrawScaledTiles(Tiles: 256) 1.34 ms 1.40 ms 🔴 +5%

Windows

⏱️ Time (vs 🌙 nightly 4.151.0-nightly.83)

Benchmark baseline this PR Δ
RuntimeEffectShaderBenchmark.DrawFrame 1.12 ms 701.67 µs 🟢 -37%
ColorMathBenchmark.ToColor(Colors: 4096) 15.52 µs 10.66 µs 🟢 -31%
ColorMathBenchmark.UnPreMultiply(Colors: 4096) 12.22 µs 8.73 µs 🟢 -28%
ColorMathBenchmark.PreMultiply(Colors: 4096) 13.19 µs 10.12 µs 🟢 -23%
BitmapDrawBenchmark.DrawUnscaledTiles(Tiles: 256) 942.89 µs 734.99 µs 🟢 -22%
MatrixMapPointsBenchmark.MapPoints(Points: 256) 173.5 ns 208.5 ns 🔴 +20%
MatrixMapPointsBenchmark.MapPoint(Points: 4096) 79.32 µs 64.49 µs 🟢 -19%
RasterImageLifecycleBenchmark.CreateDataWithReleaseProc(Count: 256) 100.03 µs 118.46 µs 🔴 +18%
CanvasDrawBenchmark.Draw(Shapes: 64) 2.60 ms 2.14 ms 🟢 -18%
MatrixOpsBenchmark.Concat(Count: 4096) 230.64 µs 190.41 µs 🟢 -17%
CanvasDrawBenchmark.Draw(Shapes: 512) 20.95 ms 17.32 ms 🟢 -17%
MatrixMapPointsBenchmark.MapPoint(Points: 256) 4.85 µs 4.04 µs 🟢 -17%
LargeImageScaleBenchmark.UpscaleCrossfade(Size: 2048) 10.32 ms 8.61 ms 🟢 -17%
BitmapDrawBenchmark.DrawUnscaledTiles(Tiles: 64) 251.45 µs 210.19 µs 🟢 -16%
MatrixOpsBenchmark.Invert(Count: 4096) 132.16 µs 111.21 µs 🟢 -16%
BitmapDrawBenchmark.DrawScaledTiles(Tiles: 256) 1.26 ms 1.07 ms 🟢 -15%
LargeImageScaleBenchmark.UpscaleCrossfade(Size: 1024) 2.56 ms 2.21 ms 🟢 -14%
PathBoundsBenchmark.TightBounds(Points: 1024) 15.93 µs 13.81 µs 🟢 -13%
SceneRenderBenchmark.RenderFrame(Complexity: 4) 10.07 ms 8.73 ms 🟢 -13%
ColorParseBenchmark.Parse(Iterations: 1000) 77.86 µs 67.60 µs 🟢 -13%
SceneRenderBenchmark.RenderFrame(Complexity: 1) 2.11 ms 1.84 ms 🟢 -13%
PathBoundsBenchmark.TightBounds(Points: 64) 1.54 µs 1.35 µs 🟢 -12%
BitmapDrawBenchmark.DrawScaledTiles(Tiles: 64) 334.04 µs 304.84 µs 🟢 -9%
MatrixMapPointsBenchmark.MapRect(Points: 4096) 121.43 µs 111.22 µs 🟢 -8%
ColorMathBenchmark.ToColorF(Colors: 4096) 13.29 µs 14.05 µs 🔴 +6%

macOS

⏱️ Time (vs 🌙 nightly 4.151.0-nightly.83)

Benchmark baseline this PR Δ
PathBoundsBenchmark.TightBounds(Points: 64) 933.0 ns 838.2 ns 🟢 -10%
SceneRenderBenchmark.RenderFrame(Complexity: 1) 1.27 ms 1.18 ms 🟢 -7%
SceneRenderBenchmark.RenderFrame(Complexity: 4) 5.79 ms 5.39 ms 🟢 -7%
ColorMathBenchmark.ToColor(Colors: 4096) 20.01 µs 21.36 µs 🔴 +7%
MatrixMapPointsBenchmark.MapPoint(Points: 4096) 19.40 µs 18.12 µs 🟢 -7%
RasterImageLifecycleBenchmark.CreateRasterImage(Count: 256) 159.99 µs 149.52 µs 🟢 -7%
MatrixOpsBenchmark.Invert(Count: 4096) 69.83 µs 65.36 µs 🟢 -6%
PathBoundsBenchmark.TightBounds(Points: 1024) 8.71 µs 8.17 µs 🟢 -6%
MatrixOpsBenchmark.Concat(Count: 4096) 118.81 µs 111.64 µs 🟢 -6%
RuntimeEffectShaderBenchmark.DrawFrame 551.19 µs 517.92 µs 🟢 -6%
RasterImageLifecycleBenchmark.CreateDataWithReleaseProc(Count: 256) 68.74 µs 72.82 µs 🔴 +6%
MatrixMapPointsBenchmark.MapRadius(Points: 256) 6.51 µs 6.14 µs 🟢 -6%
MatrixMapPointsBenchmark.MapRadius(Points: 4096) 104.04 µs 98.53 µs 🟢 -5%
BitmapDrawBenchmark.DrawUnscaledTiles(Tiles: 64) 172.30 µs 181.15 µs 🔴 +5%
MatrixMapPointsBenchmark.MapPoint(Points: 256) 1.21 µs 1.15 µs 🟢 -5%

📈 Full interactive perf-dashboard & run details →

@mattleibow
mattleibow merged commit 2bd34ad into main Jul 22, 2026
27 of 87 checks passed
@mattleibow
mattleibow deleted the dev/memory-leak-image-create-cotaskmem-7cb41f47978d8b90 branch July 22, 2026 23:43
@github-actions

Copy link
Copy Markdown
Contributor Author

📦 Artifact size report

Packages from this PR (build 1522410) vs the latest nightly baseline 4.151.0-nightly.83 (observed 2026-07-22).

Total .nupkg size: 536.7 MB → 535.4 MB (−1.3 MB, -0.2%)

Packages

⚠️ marks growth over 500.0 KB or 2%. Changes under 50.0 KB are treated as noise.

Package baseline this PR Δ Δ%
SkiaSharp.NativeAssets.WinUI 109.0 MB 107.1 MB 🟢 −1.9 MB -1.7%
SkiaSharp.NativeAssets.NanoServer ⚠️ 21.7 MB 22.5 MB 🔴 +841.6 KB +3.8%
HarfBuzzSharp.NativeAssets.Win32 20.8 MB 20.5 MB 🟢 −258.2 KB -1.2%

+37 package(s) unchanged (< 50.0 KB).

Per-file changes

HarfBuzzSharp.NativeAssets.Win32

File Size
runtimes/win-x86/native/libHarfBuzzSharp.pdb 22.2 MB → 21.9 MB (🟢 −272.0 KB)
runtimes/win-x64/native/libHarfBuzzSharp.pdb 22.0 MB → 21.8 MB (🟢 −240.0 KB)
runtimes/win-arm64/native/libHarfBuzzSharp.pdb 22.3 MB → 22.3 MB (🟢 −8.0 KB)

SkiaSharp.NativeAssets.NanoServer

File Size
runtimes/win-x64/native/libSkiaSharp.pdb 68.8 MB → 70.2 MB (🔴 +1.5 MB)
🧬 win-x64 (runtimes/win-x64/native/libSkiaSharp.dll) 9.6 MB → 10.2 MB (🔴 +596.5 KB)

SkiaSharp.NativeAssets.WinUI

File Size
runtimes/win-x86/native/libGLESv2.pdb 67.3 MB → 67.0 MB (🟢 −240.0 KB)
runtimes/win-x64/native/libGLESv2.pdb 66.6 MB → 66.4 MB (🟢 −208.0 KB)
runtimes/win-arm64/native/libGLESv2.pdb 64.5 MB → 64.3 MB (🟢 −192.0 KB)
runtimes/win-x64/native/libEGL.pdb 6.4 MB → 6.3 MB (🟢 −32.0 KB)
runtimes/win-x86/native/libEGL.pdb 6.5 MB → 6.5 MB (🟢 −16.0 KB)
runtimes/win-arm64/native/SkiaSharp.Views.WinUI.Native.pdb 67.7 MB → 67.7 MB (🟢 −8.0 KB)
runtimes/win-x64/native/SkiaSharp.Views.WinUI.Native.pdb 68.0 MB → 68.0 MB (🟢 −8.0 KB)
🧬 win-x64 (runtimes/win-x64/native/libGLESv2.dll) 4.9 MB → 4.9 MB (🔴 +2.0 KB)
🧬 win-arm64 (runtimes/win-arm64/native/libGLESv2.dll) 4.4 MB → 4.4 MB (🔴 +1.0 KB)

Informational only — this never blocks the PR. Native binaries are labelled by os/arch.

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. perf/memory-leak Unbounded memory growth: leaked native handles or undisposed objects. Implies tenet/performance. tenet/performance Performance related issues

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[memory-leak] CoTaskMem pixel-buffer leak in SKImage.Create(SKImageInfo) on native failure path

2 participants