🤖 AI-generated finding. Produced by the memory-leak-fixer agentic workflow + skill. Focus area 0 — Undisposed native handle. Empirically proven with a red→green managed test (details below).
The leak
BlobExtensions.ToHarfBuzzBlob(this SKStreamAsset asset) has two paths:
- memory-base branch —
new Blob(memoryBase, size, MemoryMode.ReadOnly, () => asset.Dispose()) — correctly disposes the owned asset when the Blob is released.
- copy (else) branch —
new Blob(ptr, size, MemoryMode.ReadOnly, () => Marshal.FreeCoTaskMem(ptr)) — only frees the copied buffer and never disposes asset.
So for any stream that reports no native memory base (e.g. SKManagedStream), the owned native SKStreamAsset is leaked — its handle is only reclaimed by the finalizer, non-deterministically and late.
Retention/ownership path
source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/BlobExtensions.cs:21 — asset.GetMemoryBase() returns IntPtr.Zero for managed/non-memory streams → copy branch.
source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/BlobExtensions.cs:28-30 — copies into ptr, but the release delegate frees only ptr; asset (owned) is never disposed.
- Reached in the wild from
source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/SKShaper.cs:20 — Typeface.OpenStream(out index).ToHarfBuzzBlob(), where SKTypeface.OpenStream (binding/SkiaSharp/SKTypeface.cs:350) mints an owned SKStreamAsset.
Evidence (red→green)
Added tests/Tests/SkiaSharp/BlobExtensionsTest.cs: builds a SKManagedStream (no memory base → copy branch), converts it with ToHarfBuzzBlob(), then disposes the Blob and asserts the asset is disposed.
- Before fix:
Assert.True(asset.IsDisposed) FAILS (Actual: False) — asset leaked.
- After fix: PASSES. Neighbouring
*Shaper* (16) and HBBlobTest (4) tests remain green.
Scope note
- Framework bug (not a caller footgun): the two branches of the same helper have asymmetric ownership handling.
- Empirically proven via managed disposal test, not just static reasoning.
- Managed-C# fix, in
source/** only. No public signature changed — ABI stable.
Labels: tenet/performance, perf/memory-leak.
Generated by Fixer - Memory Leak · opus48 · 531.1 AIC · ⌖ 27.8 AIC · ⊞ 12K · ◷
The leak
BlobExtensions.ToHarfBuzzBlob(this SKStreamAsset asset)has two paths:new Blob(memoryBase, size, MemoryMode.ReadOnly, () => asset.Dispose())— correctly disposes the owned asset when theBlobis released.new Blob(ptr, size, MemoryMode.ReadOnly, () => Marshal.FreeCoTaskMem(ptr))— only frees the copied buffer and never disposesasset.So for any stream that reports no native memory base (e.g.
SKManagedStream), the owned nativeSKStreamAssetis leaked — its handle is only reclaimed by the finalizer, non-deterministically and late.Retention/ownership path
source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/BlobExtensions.cs:21—asset.GetMemoryBase()returnsIntPtr.Zerofor managed/non-memory streams → copy branch.source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/BlobExtensions.cs:28-30— copies intoptr, but the release delegate frees onlyptr;asset(owned) is never disposed.source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/SKShaper.cs:20—Typeface.OpenStream(out index).ToHarfBuzzBlob(), whereSKTypeface.OpenStream(binding/SkiaSharp/SKTypeface.cs:350) mints an ownedSKStreamAsset.Evidence (red→green)
Added
tests/Tests/SkiaSharp/BlobExtensionsTest.cs: builds aSKManagedStream(no memory base → copy branch), converts it withToHarfBuzzBlob(), then disposes theBloband asserts the asset is disposed.Assert.True(asset.IsDisposed)FAILS (Actual: False) — asset leaked.*Shaper*(16) andHBBlobTest(4) tests remain green.Scope note
source/**only. No public signature changed — ABI stable.Labels:
tenet/performance,perf/memory-leak.