|
| 1 | +{ |
| 2 | + "meta": { |
| 3 | + "schemaVersion": "1.0", |
| 4 | + "number": 3394, |
| 5 | + "repo": "mono/SkiaSharp", |
| 6 | + "analyzedAt": "2026-02-13T00:24:40Z", |
| 7 | + "currentLabels": [] |
| 8 | + }, |
| 9 | + "summary": "Draft PR #3394 by Copilot adds GC.KeepAlive() calls to ~40 methods across 5 files (SKCanvas, SKPaint, SKImage, SKBitmap, SKPath) to prevent premature GC collection of managed objects during P/Invoke calls. This is a partial fix for #3393 (and duplicate #2821), covering ~20% of the ~910 affected call sites. The PR could not build due to CI firewall issues, and maintainer jonpryor's review raises a deeper concern: property setters and struct-to-native conversions (e.g. SKCanvasSaveLayerRec.ToNative()) also extract handles without KeepAlive protection. Types using GetObject() without HandleDictionary registration (SKPaint, SKPath, SKCodec, SKTextBlob, etc.) are additionally vulnerable.", |
| 10 | + "classification": { |
| 11 | + "type": { "value": "type/bug", "confidence": 0.97 }, |
| 12 | + "area": { "value": "area/SkiaSharp", "confidence": 0.98 }, |
| 13 | + "tenets": ["tenet/reliability"], |
| 14 | + "partner": "partner/unoplatform" |
| 15 | + }, |
| 16 | + "evidence": { |
| 17 | + "bugSignals": { |
| 18 | + "severity": "high", |
| 19 | + "errorType": "race condition", |
| 20 | + "errorMessage": "Premature GC collection of managed objects during P/Invoke calls causes crashes, AccessViolationException, or data corruption", |
| 21 | + "reproQuality": "partial", |
| 22 | + "targetFrameworks": ["net8.0-android"] |
| 23 | + }, |
| 24 | + "reproEvidence": { |
| 25 | + "codeSnippets": [ |
| 26 | + "var canvas = ...\nvar picture = ...\ncanvas.DrawPicture(picture);\n// `picture` is not referenced after this point, so GC can collect it\n// But native code may still be using picture.Handle!" |
| 27 | + ], |
| 28 | + "relatedIssues": [3393, 2821, 1258, 1244], |
| 29 | + "repoLinks": [ |
| 30 | + { "url": "https://github.com/unoplatform/uno/pull/21660", "description": "Uno Platform PR demonstrating real-world GC race condition with SkiaSharp" }, |
| 31 | + { "url": "https://github.com/dotnet/java-interop/issues/719", "description": "Same class of bug in dotnet/java-interop" } |
| 32 | + ] |
| 33 | + }, |
| 34 | + "versionAnalysis": { |
| 35 | + "mentionedVersions": ["3.116.0"], |
| 36 | + "currentRelevance": "likely", |
| 37 | + "relevanceReason": "Only 14 GC.KeepAlive calls exist in the current codebase across 6 files. The vast majority of ~910 P/Invoke call sites remain unprotected. This PR has not been merged." |
| 38 | + }, |
| 39 | + "fixStatus": { |
| 40 | + "likelyFixed": false, |
| 41 | + "confidence": 0.95, |
| 42 | + "reason": "PR #3394 is a draft, not merged, could not build in CI, and covers only ~20% of affected call sites. The review from jonpryor identifies additional unaddressed patterns (property setters, struct-to-native). Issue #1258 added a handful of GC.KeepAlive calls historically but the systemic problem remains.", |
| 43 | + "relatedPRs": [3394, 1258] |
| 44 | + } |
| 45 | + }, |
| 46 | + "analysis": { |
| 47 | + "summary": "This draft PR partially addresses a systemic GC safety issue in SkiaSharp's P/Invoke layer. When a managed wrapper object's handle is extracted and passed to native code, the GC can collect and finalize the wrapper before the native call completes, causing use-after-free. The PR adds GC.KeepAlive() after P/Invoke calls in ~40 methods, but the scope is incomplete: 67 P/Invoke calls in SKCanvas alone have zero GC.KeepAlive, and the broader codebase has ~910 unprotected call sites. Maintainer jonpryor's review raises a critical additional concern about types that use GetObject() without HandleDictionary registration (SKPaint, SKPath, SKCodec, SKFontStyle, SKTextBlob, SKVertices, SKDocument, etc.) — these have no static root keeping them alive, making them especially vulnerable in property-setter and struct-conversion scenarios.", |
| 48 | + "rationale": "Classified as type/bug because this PR fixes a genuine race condition that causes crashes and data corruption. Area is area/SkiaSharp because it affects the core binding layer's P/Invoke wrappers, not views or platform-specific code. Severity is high (not critical) because the race window is narrow and typically manifests under GC pressure on mobile devices. The tenet/reliability and partner/unoplatform labels match the parent issue #3393 since Uno Platform reported a real-world instance.", |
| 49 | + "keySignals": [ |
| 50 | + { "text": "SKCanvas.cs has 67 P/Invoke calls and 0 GC.KeepAlive calls", "source": "code search", "interpretation": "The most critical drawing class has zero protection against premature GC collection." }, |
| 51 | + { "text": "Only 14 GC.KeepAlive calls exist across 6 files (SKData, SKImage, SKFontStyleSet, SKTextBlob, SKSurface, SKBitmap)", "source": "code search", "interpretation": "Historical fixes (PR #1258) added some protections but the vast majority of call sites remain unprotected." }, |
| 52 | + { "text": "jonpryor review: '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...'", "source": "PR review comment", "interpretation": "The scope is broader than method parameters — property setters and struct-to-native conversions also extract handles. Types with GetObject() that bypass HandleDictionary are especially vulnerable." }, |
| 53 | + { "text": "SKPaint.GetObject just news up a value: 'handle == IntPtr.Zero ? null : new SKPaint(handle, true)'", "source": "binding/SkiaSharp/SKPaint.cs:831-832", "interpretation": "SKPaint instances have no static root in HandleDictionary, making them vulnerable to collection when their handle is extracted for P/Invoke." }, |
| 54 | + { "text": "Draft PR, not merged, CI firewall blocked build", "source": "PR metadata", "interpretation": "The PR has not been validated — it couldn't build in CI due to firewall rules blocking NuGet downloads." } |
| 55 | + ], |
| 56 | + "codeInvestigation": [ |
| 57 | + { "file": "binding/SkiaSharp/SKCanvas.cs", "lines": "533-538", "finding": "DrawPicture calls SkiaApi.sk_canvas_draw_picture with picture.Handle and paint.Handle but has no GC.KeepAlive — exactly the pattern described in the bug report and Chris Brumme's blog post", "relevance": "direct" }, |
| 58 | + { "file": "binding/SkiaSharp/SKCanvas.cs", "lines": "68-72", "finding": "SaveLayer(in SKCanvasSaveLayerRec rec) calls rec.ToNative() which extracts handles from Paint and Backdrop, then passes the native struct to sk_canvas_save_layer_rec — neither rec.Paint nor rec.Backdrop is kept alive", "relevance": "direct" }, |
| 59 | + { "file": "binding/SkiaSharp/SKCanvas.cs", "lines": "1100-1109", "finding": "SKCanvasSaveLayerRec.ToNative() extracts Paint?.Handle and Backdrop?.Handle into a native struct — after this point, Paint and Backdrop have no managed references and are eligible for GC", "relevance": "direct" }, |
| 60 | + { "file": "binding/SkiaSharp/SKPaint.cs", "lines": "831-832", "finding": "SKPaint.GetObject creates new instances without HandleDictionary registration — no static root keeps these alive, confirming jonpryor's concern about types that bypass GetOrAddObject", "relevance": "direct" }, |
| 61 | + { "file": "binding/SkiaSharp/SKShader.cs", "lines": "458-459", "finding": "SKShader.GetObject uses GetOrAddObject which registers in HandleDictionary — these instances ARE rooted and less vulnerable to premature collection", "relevance": "context" }, |
| 62 | + { "file": "binding/SkiaSharp/SKObject.cs", "lines": "14-32", "finding": "SKObject has ownedObjects and keepAliveObjects ConcurrentDictionaries, and RegisterHandle for HandleDictionary — this is the mechanism that protects some types but not others", "relevance": "context" }, |
| 63 | + { "file": "binding/SkiaSharp/SKData.cs", "lines": "167-191", "finding": "SKData has 5 existing GC.KeepAlive(stream) calls in stream-reading methods — this is the established pattern for the fix", "relevance": "context" } |
| 64 | + ], |
| 65 | + "workarounds": [ |
| 66 | + "Callers can add GC.KeepAlive() after their own SkiaSharp API calls to keep objects alive across native boundaries", |
| 67 | + "Hold explicit references to all SkiaSharp objects (e.g. assign to a field or local variable that outlives the API call scope)" |
| 68 | + ], |
| 69 | + "nextQuestions": [ |
| 70 | + "Should the fix be applied via the code generator (utils/generate.ps1) rather than manual edits to avoid maintenance burden?", |
| 71 | + "How should property setters be handled — should the setter keep a managed reference to the assigned value?", |
| 72 | + "Should types that bypass HandleDictionary (SKPaint, SKPath, etc.) be migrated to use GetOrAddObject?", |
| 73 | + "Is GC.KeepAlive(this) needed for instance methods, or is it sufficient to rely on callers to keep the receiver alive?" |
| 74 | + ], |
| 75 | + "resolution": { |
| 76 | + "hypothesis": "The .NET GC can collect managed wrapper objects after their IntPtr handles are extracted for P/Invoke calls, causing native code to operate on freed handles. The fix requires systematic GC.KeepAlive() calls after all P/Invoke invocations, plus addressing the deeper HandleDictionary registration gap for some types.", |
| 77 | + "proposals": [ |
| 78 | + { |
| 79 | + "title": "Complete the manual GC.KeepAlive audit", |
| 80 | + "description": "Expand PR #3394's approach to cover all ~910 P/Invoke call sites. Add GC.KeepAlive for every reference-type parameter (including 'this' via Handle) after each native call. This is the approach started in the PR but needs to cover the remaining ~80% of methods.", |
| 81 | + "confidence": 0.70, |
| 82 | + "effort": "large" |
| 83 | + }, |
| 84 | + { |
| 85 | + "title": "Generator-based GC.KeepAlive emission", |
| 86 | + "description": "Modify utils/generate.ps1 to automatically emit GC.KeepAlive calls in generated P/Invoke wrappers. This would handle generated methods automatically and reduce maintenance burden. Manual methods would still need hand-editing.", |
| 87 | + "confidence": 0.65, |
| 88 | + "effort": "medium" |
| 89 | + }, |
| 90 | + { |
| 91 | + "title": "Address HandleDictionary registration gap", |
| 92 | + "description": "Migrate types that use plain GetObject() (SKPaint, SKPath, SKCodec, SKDocument, SKFontStyle, SKTextBlob, SKVertices) to use GetOrAddObject with HandleDictionary registration. This provides a static root that prevents premature collection for property-assigned objects, as jonpryor's review identified.", |
| 93 | + "confidence": 0.60, |
| 94 | + "effort": "medium" |
| 95 | + } |
| 96 | + ], |
| 97 | + "recommendedProposal": "Complete the manual GC.KeepAlive audit", |
| 98 | + "recommendedReason": "This is the most direct and proven fix pattern (already used in 14 places). Generator-based emission is ideal long-term but the generator doesn't cover all methods. HandleDictionary migration is a separate concern that should be addressed alongside but not instead of GC.KeepAlive." |
| 99 | + } |
| 100 | + }, |
| 101 | + "output": { |
| 102 | + "actionability": { |
| 103 | + "suggestedAction": "needs-investigation", |
| 104 | + "confidence": 0.90, |
| 105 | + "reason": "The PR is a valid partial fix for a real systemic bug, but it's incomplete (~20% coverage), couldn't build in CI, and jonpryor's review identifies additional unaddressed patterns. The PR needs substantial rework to address review feedback and expand scope before it can be merged." |
| 106 | + }, |
| 107 | + "actions": [ |
| 108 | + { |
| 109 | + "type": "update-labels", |
| 110 | + "description": "Apply bug, SkiaSharp, reliability labels to match parent issue #3393", |
| 111 | + "risk": "low", |
| 112 | + "confidence": 0.95, |
| 113 | + "labels": ["type/bug", "area/SkiaSharp", "tenet/reliability"] |
| 114 | + }, |
| 115 | + { |
| 116 | + "type": "link-related", |
| 117 | + "description": "Cross-reference parent issue #3393", |
| 118 | + "risk": "low", |
| 119 | + "confidence": 0.98, |
| 120 | + "linkedIssue": 3393 |
| 121 | + }, |
| 122 | + { |
| 123 | + "type": "link-related", |
| 124 | + "description": "Cross-reference duplicate issue #2821 (same bug reported independently)", |
| 125 | + "risk": "low", |
| 126 | + "confidence": 0.90, |
| 127 | + "linkedIssue": 2821 |
| 128 | + }, |
| 129 | + { |
| 130 | + "type": "add-comment", |
| 131 | + "description": "Acknowledge PR, note incomplete scope, and flag jonpryor's review concerns", |
| 132 | + "risk": "high", |
| 133 | + "confidence": 0.80, |
| 134 | + "comment": "Thanks for the initial work on this. The GC.KeepAlive pattern is correct and matches the existing usage in SKData and SKImage.\n\nA few things to address before this can move forward:\n\n1. **Scope**: This covers ~40 methods but the issue affects ~910 call sites. We should decide whether to do this incrementally or find a generator-based approach.\n2. **Review feedback**: @jonpryor raises a valid concern about property setters and struct-to-native conversions (e.g. `SKCanvasSaveLayerRec.ToNative()`). Types that bypass `HandleDictionary` registration (`SKPaint`, `SKPath`, `SKCodec`, etc.) are especially vulnerable in these patterns.\n3. **Build**: The CI build failed due to firewall issues — this needs a successful build and test run.\n\nRelated: #3393 (parent issue), #2821 (duplicate report), #1258 (historical partial fix)." |
| 135 | + } |
| 136 | + ] |
| 137 | + } |
| 138 | +} |
0 commit comments