Skip to content

[memory-leak] Fix double-free in HarfBuzzSharp.Buffer.UnicodeFunctions getter - #4468

Draft
github-actions[bot] wants to merge 4 commits into
mainfrom
dev/memory-leak-buffer-unicodefunctions-f44c958d98a7dd98
Draft

[memory-leak] Fix double-free in HarfBuzzSharp.Buffer.UnicodeFunctions getter#4468
github-actions[bot] wants to merge 4 commits into
mainfrom
dev/memory-leak-buffer-unicodefunctions-f44c958d98a7dd98

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI-generated fix. Produced by the memory-leak-fixer agentic workflow + memory-leak-fixer skill.

The bug

HarfBuzzSharp.Buffer.UnicodeFunctions (binding/HarfBuzzSharp/Buffer.cs) wrapped the result of hb_buffer_get_unicode_funcs — a borrowed (transfer-none) reference owned by the buffer — in an owning UnicodeFunctions. Disposing that wrapper called hb_unicode_funcs_destroy on a handle the still-alive buffer references, causing a double-free / premature destroy (Area 1: wrong owns: flag).

The fix

  • UnicodeFunctions now tracks ownership via a private owns flag; DisposeHandler only destroys the native handle when it owns it.
  • The borrowed getter result is wrapped owns: false, so its Dispose is a no-op — matching HarfBuzz's transfer-none contract.
  • Managed-only change in binding/HarfBuzzSharp/**; no public signature changed (ABI-safe).

This is the idiomatic borrowed → don't free fix from the leak catalogue's Area 1.

Proof (red→green)

A regression test (HBUnicodeFuncsTest.BufferUnicodeFunctionsGetterDoesNotOwnBorrowedHandle) sets a custom UnicodeFunctions on a buffer, reads it back via the getter, disposes the wrapper + original while the buffer is still alive, and asserts the shared handle was not destroyed.

Executed locally against the shipped HarfBuzzSharp.NativeAssets.Linux native library (MSBuild was unavailable in the runner, so sources were compiled directly with csc):

Sources Output
Current (HEAD) destroyed=TrueRED (over-freed; a using-buffer variant crashed with malloc(): unaligned tcache chunk detected)
With this fix destroyed=FalseGREEN

Both directions verified (revert ⇒ red, re-apply ⇒ green).

Scope note

  • Type: framework bug (double-free), not a mere usage footgun.
  • Proof: empirically proven red→green.
  • ABI: internal-only; no public API changed.

Fixes #4467

Generated by Fixer - Memory Leak · ● 37.7M ·

hb_buffer_get_unicode_funcs returns a borrowed (transfer-none) reference
owned by the buffer. The getter wrapped it in an owning UnicodeFunctions,
so disposing the returned wrapper called hb_unicode_funcs_destroy and
over-freed a handle still referenced by the live buffer (double-free /
premature destroy).

Make UnicodeFunctions track ownership and wrap the borrowed getter result
non-owning so Dispose is a no-op for it.

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 17, 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 -- 4468

PowerShell / Windows:

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

Step 2 — Add the local NuGet source

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

@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

Fixes a native lifetime/ownership bug in HarfBuzzSharp where Buffer.UnicodeFunctions wrapped a borrowed hb_unicode_funcs_t* as owned, leading to premature destroy / double-free when the wrapper was disposed.

Changes:

  • Track native-handle ownership inside UnicodeFunctions and only call hb_unicode_funcs_destroy when the wrapper truly owns the handle.
  • Update Buffer.UnicodeFunctions getter to wrap the HarfBuzz “transfer-none” result as owns: false.
  • Add a regression test to ensure disposing the getter result does not destroy the underlying shared handle while the buffer is still alive.

Reviewed changes

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

File Description
binding/HarfBuzzSharp/UnicodeFunctions.cs Adds an internal ownership flag and gates hb_unicode_funcs_destroy behind it.
binding/HarfBuzzSharp/Buffer.cs Wraps hb_buffer_get_unicode_funcs result as non-owning (owns: false) per HarfBuzz contract.
tests/Tests/HarfBuzzSharp/HBUnicodeFuncsTest.cs Adds regression test covering the borrowed-handle disposal scenario.

Comment thread tests/Tests/HarfBuzzSharp/HBUnicodeFuncsTest.cs
Comment on lines +159 to +163
// The buffer is intentionally left alive (and its funcs referenced) for the
// whole test, so the funcs must remain valid.
var buffer = new Buffer();
buffer.UnicodeFunctions = unicodeFunctions;

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.

Addressed in efba6ff — switched to using var buffer so it stays alive through the assertion and is disposed cleanly at scope exit, matching the other HarfBuzzSharp tests.

Copilot AI added 2 commits July 20, 2026 17:21
The regression test BufferUnicodeFunctionsGetterDoesNotOwnBorrowedHandle
was inserted inside ShouldSetDecomposeDelegate's body: the method's
closing brace was missing and the new [Fact] method was nested as an
invalid public local function. This produced two compile errors in
SkiaSharp.Tests.csproj:

  HBUnicodeFuncsTest.cs(149,3): error CS0106: The modifier 'public' is not valid for this item
  HBUnicodeFuncsTest.cs(173,2): error CS1513: } expected

A non-compiling test assembly caused every Tests leg to fail on every
platform (WASM/Windows/Linux/macOS/Catalyst/Android/iOS) while the
Build Managed jobs passed. Close ShouldSetDecomposeDelegate properly and
de-nest the new test as a class member.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 278bdb81-233e-4f16-bbf3-ec801600bd12
Addresses Copilot review feedback: use 'using var buffer' so the buffer
stays alive through the assertion and is disposed cleanly at scope exit,
matching the disposal convention of the other HarfBuzzSharp tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 278bdb81-233e-4f16-bbf3-ec801600bd12
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: No status

Development

Successfully merging this pull request may close these issues.

[memory-leak] Double-free in HarfBuzzSharp.Buffer.UnicodeFunctions getter (borrowed handle wrapped as owned)

3 participants