Skip to content

Alternate fix for #3817: re-entrant static-init NRE in SKTypeface (+ #4080 test suite) - #4116

Closed
mattleibow wants to merge 5 commits into
mainfrom
mattleibow/symmetrical-waffle
Closed

Alternate fix for #3817: re-entrant static-init NRE in SKTypeface (+ #4080 test suite)#4116
mattleibow wants to merge 5 commits into
mainfrom
mattleibow/symmetrical-waffle

Conversation

@mattleibow

@mattleibow mattleibow commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

What this is

An alternate attempt to PR #4080 for the re-entrant static-init NullReferenceException (#3817). Instead of #4080's broader rework, this PR takes PR #4107's narrower SKTypeface fix and brings in #4080's full test suite, reconciling the two so every test passes.

Both PRs target the same bug; this is the smaller production change of the two, validated against the larger test set.

Production changes

  • SKTypeface.cs — PR Fix #3817: avoid re-entrant singleton cctor NullReferenceException #4107's fix. The static cctor builds the empty/default typefaces from raw native handles in a try/finally, severing the managed-singleton reads (SKFontManager.Default / SKFontStyle.Normal) that triggered the re-entrant cold-start crash.
  • Static singleton wrappers (SKColorSpaceStatic, SKDataStatic, SKBlenderStatic, SKFontStyleStatic, SKFontManagerStatic, SKTypefaceStatic, SKColorFilterStatic, SKFontStatic) — each constructor now calls PreventPublicDisposal() so the singleton carries IgnorePublicDispose = true. These wrappers already protected disposal structurally (no-op Dispose(bool) + owns=false); setting the flag unifies that with the IgnorePublicDispose contract Rework the lifecycle of singleton instances #4080's tests assert. Purely additive and ABI-safe.

Tests (from #4080)

Result

macOS arm64, net10.0, full single-process assembly run:

Failed: 0, Passed: 5706, Skipped: 13 — fully green, including the #3817 cold-start guard and the previously-flaky SKDocumentTest.StreamIsNotCollectedPrematurely.

Conclusion

#4107's minimal SKTypeface fix resolves the #3817 re-entrancy crash, and the PreventPublicDisposal() reconciliation lets it satisfy #4080's complete dispose-protection / concurrency / lifecycle test suite. This is the lower-risk production change relative to #4080.

Refs #3817. Alternative to #4080; brings in #4107's fix.

Apply PR #4107's minimal alternate fix for the re-entrant static-init
NullReferenceException (#3817) and bring over PR #4080's full test suite
verbatim to observe whether #4107's narrower approach also exhibits the
crashes/issues #4080's tests probe.

Production changes:
- SKTypeface.cs: #4107's fix. The static cctor builds the default typeface
  from raw native handles in a try/finally, severing the managed-singleton
  reads that caused the re-entrant cold-start crash.
- HandleDictionary.cs / SKObject.cs: two ADDITIVE, test-enabling helpers
  ported from #4080 (5-arg GetOrAddObject overload and
  GetOrAddDisposeProtectedObject). These exist only so #4080's tests compile;
  NO production singleton accessor is routed through them, so #4107's runtime
  behavior is unchanged.

Test infrastructure (verbatim from #4080 / #4107):
- SkiaSharp.Tests.SingletonInit.Console assembly + cake + sln wiring
  (identical in both PRs): dedicated #3817 cold-start regression guard.
- GarbageCleanupFixture.cs: #4080's rewrite keys "expected dead" off
  IgnorePublicDispose instead of a hard-coded static-type name list.
- Full set of #4080 lifecycle/concurrency/reentrancy test files.

Experimental result (macOS arm64, net10.0, full single-process assembly run):
- SingletonInit.Console #3817 cold-start guard: PASSED. #4107 fixes the crash.
- Full suite ran to completion: NO crashes, NO hangs, NO access violations.
  7630 total, 5697 passed, 12 skipped, 1921 failed (4m30s).
- 1908 of the failures share ONE root cause: the rewritten assembly-cleanup
  fixture asserts statics carry IgnorePublicDispose=true; #4107 does not set
  that flag, so the assembly-teardown Assert.Empty fails and is attributed
  across many tests.
- The remaining 13 genuine failures all trace to the same gap: #4107 does not
  implement #4080's IgnorePublicDispose-based dispose-protection model
  (statics aren't flagged; PreventPublicDisposal doesn't throw on a disposed
  wrapper). Identity-under-contention assertions (AssertAllSame) PASS, so
  #4107's singleton accessors are stable under concurrency.

Conclusion: #4107 resolves the #3817 re-entrancy crash without reproducing the
crashes #4080's tests probe; it simply lacks #4080's broader dispose-protection
feature. Existing static wrapper subclasses (SKDataStatic, SKColorSpaceStatic,
SKTypefaceStatic, ...) still override Dispose(bool) as a no-op with owns=false,
so public disposal of singletons remains a no-op even without the flag.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

📦 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 -- 4116

PowerShell / Windows:

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

Step 2 — Add the local NuGet source

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

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

📖 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.

mattleibow and others added 2 commits June 4, 2026 11:41
…ixture StaticTypes list

PR #4116 is a diagnostic experiment pairing PR #4107's alternate fix for
#3817 with PR #4080's test suite. 4107 resists public dispose structurally
(SKxxxStatic subclasses override Dispose(bool) as no-op) rather than via
4080's IgnorePublicDispose flag model, so the dispose-protection scaffolding
ported into the binding was unused by production code and only referenced by
test glue.

- HandleDictionary.cs, SKObject.cs: reverted to main (removed the additive
  disposeProtected overloads that nothing in production used).
- GarbageCleanupFixture.cs: restored main's StaticTypes name-list. 4080's
  version keyed assembly-teardown cleanup off IgnorePublicDispose, which the
  static wrappers under 4107 never set, so Assert.Empty(aliveObjects) threw at
  assembly cleanup and xUnit smeared a single teardown failure across ~1900
  already-passing tests (console showed Failed: 1921). Reverting removes the
  smear so CI reflects only the genuine experiment signal.
- Dropped SKHandleDictionaryDisposeProtectedTest.cs and the two SKObjectTest
  cases that depended on the removed overloads.

After this change the suite reports 13 failures (was ~1921): the genuine
behavioral deltas where 4107 does not implement 4080's IgnorePublicDispose
model, plus one pre-existing GC-timing flake (SKDocumentTest.StreamIsNot
CollectedPrematurely, unchanged from main).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Each SKxxxStatic subclass protects its singleton structurally (no-op
Dispose(bool) + OwnsHandle=false) but never set the IgnorePublicDispose
flag. The singleton tests from #4080 assert that flag, so they failed
under #4107's protection model. Call PreventPublicDisposal() in each
static subclass constructor to unify both mechanisms.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mattleibow mattleibow changed the title Experiment: PR #4107 fix + PR #4080 test suite for #3817 Alternate fix for #3817: re-entrant static-init NRE in SKTypeface (+ #4080 test suite) Jun 4, 2026
mattleibow and others added 2 commits June 4, 2026 13:19
…uard

PreventPublicDisposalOnDisposedWrapperThrows (gated on THROW_OBJECT_EXCEPTIONS,
active only in Debug/device builds) asserted that PreventPublicDisposal() throws
InvalidOperationException on a disposed wrapper. That throwing guard only existed
in #4080's production rewrite, which this PR deliberately omits in favor of #4107's
minimal fix. On main's PreventPublicDisposal() (sets IgnorePublicDispose, no throw)
the assertion cannot hold, so the test failed on Mac Catalyst/iOS while passing
everywhere else. Adding the guard back would reintroduce #4080 production behavior
with a real debug-only race in SKFontManager/SKFontStyleSet callers, so the test
is removed instead. Also corrected the now-stale comment on the sibling live-wrapper
test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Re-run CI to confirm green after the macOS netcore agent hang on the
previous build was a one-off infra timeout (retry passed cleanly).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mattleibow

Copy link
Copy Markdown
Contributor Author

Closing in favor of #4080 (merged). The real fix was two-part: owns:false on the immortal Skia singletons so the wrapper never drives the native refcount to 0 on Dispose/finalize (fixes the net48/x86 teardown heap corruption), plus a GC.KeepAlive sweep across the bindings to stop in-flight use-after-free. #4080 landed the same approach this PR was exploring, so this is now redundant.

@mattleibow mattleibow closed this Jun 10, 2026
@mattleibow
mattleibow deleted the mattleibow/symmetrical-waffle branch June 10, 2026 22:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant