-
Notifications
You must be signed in to change notification settings - Fork 595
[BUG] Add GC.KeepAlive calls to protect P/Invoke parameters from premature collection #3394
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
…mplementation Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
| } | ||
| ``` | ||
|
|
||
| ### Property Setters |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't fully understand SkiaSharp, but the existence of properties deeply concerns me. Spitballing:
var paint = new SKPaint();
var shader = SKShader.CreateColor(new SKColor(0));
paint.Shader = shader;What keeps shader alive?
For this specific example, the answer appears to be the SKShader.CreateColor() hits SKShader.GetObject() which hits SKObject.GetOrAddObject() which "registers" instances within HandleDictionary, and as HandleDictionary holds static members, those are all roots which keep things alive.
However, not all T.GetObject() methods use GetOrAddObject(). Consider SKPaint.GetObject(), which just news up a value:
SkiaSharp/binding/SkiaSharp/SKPaint.cs
Lines 831 to 832 in 34c5ed8
| internal static SKPaint GetObject (IntPtr handle) => | |
| handle == IntPtr.Zero ? null : new SKPaint (handle, true); |
Thus:
var bitmap = new SKBitmap();
var canvas = new SKCanvas(bitmap);
var save = new SKCanvasSaveLayerRec();
var paint = new SKPaint();
save.Paint = paint;
canvas.SaveLayer(save);What keeps the GC from collecting paint "too soon"?
Is there a "too soon"?
SkiaSharp/binding/SkiaSharp/SKCanvas.cs
Lines 68 to 72 in 34c5ed8
| public int SaveLayer (in SKCanvasSaveLayerRec rec) | |
| { | |
| var native = rec.ToNative (); | |
| return SkiaApi.sk_canvas_save_layer_rec (Handle, &native); | |
| } |
native is from:
SkiaSharp/binding/SkiaSharp/SKCanvas.cs
Lines 1100 to 1106 in 34c5ed8
| internal readonly SKCanvasSaveLayerRecNative ToNative () => | |
| new SKCanvasSaveLayerRecNative { | |
| fBounds = Bounds is { } bounds ? &bounds : (SKRect*)null, | |
| fPaint = Paint?.Handle ?? IntPtr.Zero, | |
| fBackdrop = Backdrop?.Handle ?? IntPtr.Zero, | |
| fFlags = Flags | |
| }; |
Thus, my conjecture: nothing references SKCanvasSaveLayerRec.Paint after the .ToNative() invocation, so if we expand and annotate SaveLayer(), we get:
partial class SKCanvas {
public int SaveLayer (in SKCanvasSaveLayerRec rec)
{
var native = rec.ToNative ();
// Nothing references `rec.Paint` after this point.
// `rec.Paint` is collected by the GC, *invalidating* SKCanvasSaveLayerRecNative.fPaint
return SkiaApi.sk_canvas_save_layer_rec (Handle, &native);
// *boom*, as native code accesses an invalid pointer.
}
}Is this likely? No. That's a very narrow window for things to go wrong. (Though adding a Thread.Sleep(1000) between rec.ToNative() and the sk_canvas_save_layer_rec() could make things more likely… 😉)
I am thus rather concerned about the behavior of properties in general. If the property type uses HandleDictionary, things should be fine. If they don't…
One git grep -3 'static.*GetObject' later, and the following files are concerning:
binding/SkiaSharp.Skottie/Animation.csbinding/SkiaSharp/GRGlInterface.csbinding/SkiaSharp/GRVkExtensions.csbinding/SkiaSharp/SKCodec.csbinding/SkiaSharp/SKDocument.csbinding/SkiaSharp/SKFontStyle.csbinding/SkiaSharp/SKPaint.csbinding/SkiaSharp/SKPath.csbinding/SkiaSharp/SKString.csbinding/SkiaSharp/SKTextBlob.csbinding/SkiaSharp/SKVertices.cs
Overview
This PR addresses a critical GC safety issue where managed objects can be prematurely collected while native code is still operating on their handles, potentially causing crashes or data corruption.
Problem
As described in Chris Brumme's blog post on Lifetime, GC.KeepAlive, handle recycling:
The .NET runtime cannot see into native code. Once a handle is extracted from a managed object and passed to a P/Invoke method, the GC may:
This race condition is particularly problematic under GC pressure (mobile devices, memory-constrained environments) and can manifest as intermittent crashes that are difficult to diagnose.
Real-World Example
From the issue description and unoplatform/uno#21660:
Solution
Add
GC.KeepAlive()calls after P/Invoke methods to ensure reference type parameters remain alive until the native call completes:Changes in This PR
Fixed Classes (40+ methods)
DrawPicture,DrawImage,DrawPath,DrawPaint,DrawRegion,DrawRect,DrawRoundRect,DrawOval,DrawCircle,DrawPoint,DrawPoints,DrawText,DrawDrawable,DrawVertices,DrawArc,DrawRoundRectDifference,DrawAtlas,DrawPatch,DrawImageNinePatch,DrawImageLattice, and allClip*and annotation methodsSKFont, property setters (Shader,MaskFilter,ColorFilter,ImageFilter,Blender,PathEffect),SetColor,GetFillPathFromPixelCopy,FromPixels,FromEncodedData,PeekPixelsExtractSubsetAddRoundRect,AddPath(all overloads),AddPathReverseDocumentation
Scope
This issue affects approximately 200+ methods across 56 files in
binding/SkiaSharp. This PR implements the fix for the most critical and commonly-used APIs (~20% of total methods), providing:Testing
Changes follow the established pattern and are minimally invasive. Each fix:
GC.KeepAlive()calls after existing P/Invoke callsTesting should include:
Related Issues
Remaining Work
The implementation guide (
GC_KEEPALIVE_IMPLEMENTATION_GUIDE.md) documents all remaining files and provides a checklist for completing this work. High-priority files still needing fixes include:The pattern is consistent and well-documented, making it straightforward to complete the remaining implementations.
Warning
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
0t3vsblobprodcus362.vsblob.vsassets.iodotnet build -c Release(dns block)2kmvsblobprodcus39.vsblob.vsassets.iodotnet build -c Release(dns block)2zrvsblobprodcus388.vsblob.vsassets.iodotnet build -c Release(dns block)7devsblobprodcus323.vsblob.vsassets.iodotnet build -c Release(dns block)7tjvsblobprodcus341.vsblob.vsassets.iodotnet build -c Release(dns block)bcnvsblobprodcus378.vsblob.vsassets.iodotnet build -c Release(dns block)c78vsblobprodcus322.vsblob.vsassets.iodotnet build -c Release(dns block)frdvsblobprodcus327.vsblob.vsassets.iodotnet build -c Release(dns block)ibzvsblobprodcus369.vsblob.vsassets.iodotnet build -c Release(dns block)imzvsblobprodcus368.vsblob.vsassets.iodotnet build -c Release(dns block)josvsblobprodcus372.vsblob.vsassets.iodotnet build -c Release(dns block)kgfvsblobprodcus314.vsblob.vsassets.iodotnet build -c Release(dns block)kh4vsblobprodcus325.vsblob.vsassets.iodotnet build -c Release(dns block)kijvsblobprodcus387.vsblob.vsassets.iodotnet build -c Release(dns block)l49vsblobprodcus358.vsblob.vsassets.iodotnet build -c Release(dns block)s4uvsblobprodcus326.vsblob.vsassets.iodotnet build -c Release(dns block)u3hvsblobprodcus371.vsblob.vsassets.iodotnet build -c Release(dns block)u6ovsblobprodcus377.vsblob.vsassets.iodotnet build -c Release(dns block)If you need me to access, download, or install something from one of these locations, you can either:
Original prompt
Fixes #3393
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.