From e2e4905c98ab701f3901252958964e81c2d16df7 Mon Sep 17 00:00:00 2001 From: Tom Burgin <0ftu56jh@duck.com> Date: Tue, 11 Aug 2026 05:04:42 +0100 Subject: [PATCH] Fix a data race in FontHandler's single-entry font cache FontToXFont memoises the last (Font, XFont) pair in two independent statics, _lastFont and _lastXFont, written without synchronisation. A reader can confirm _lastFont is its own Font and then read an _lastXFont that another thread has already replaced, so the method returns an XFont belonging to a different Font - typically a different size. Nothing throws; the document is silently typeset with the wrong font. Publish both references as one immutable entry assigned to a single volatile field instead, so a reader sees either the complete previous entry or the complete new one. Behaviour, allocation count and the cache-hit fast path are otherwise unchanged. --- .../Rendering/FontHandler.cs | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/FontHandler.cs b/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/FontHandler.cs index 075e4e82..90dc3959 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/FontHandler.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/FontHandler.cs @@ -23,10 +23,13 @@ static class FontHandler /// internal static XFont FontToXFont(Font font) { - // Check if both WeakReferences are still valid and point to the font we need. - if (_lastXFont != null && _lastFont != null && - _lastFont.TryGetTarget(out var lastFont) && font == lastFont && - _lastXFont.TryGetTarget(out var lastXFont)) + // Read the whole cache entry once. The Font and the XFont are published together by a single + // reference assignment, so a concurrent update can never be observed half-applied and this can + // never return an XFont belonging to a different Font. See FontCacheEntry. + var entry = _lastEntry; + if (entry != null && + entry.FontRef.TryGetTarget(out var lastFont) && font == lastFont && + entry.XFontRef.TryGetTarget(out var lastXFont)) return lastXFont; XFontStyleEx style = GetXStyle(font); @@ -39,16 +42,31 @@ internal static XFont FontToXFont(Font font) #if DEBUG_ CreateFontCounter++; #endif - _lastFont = new(font); - _lastXFont = new(xFont); + _lastEntry = new FontCacheEntry(font, xFont); #if FORCE_MEMORYLEAK _lastFont2 = font; #endif return xFont; } - static WeakReference? _lastXFont; - static WeakReference? _lastFont; + /// + /// The single-entry font cache. Both WeakReferences are assigned in the constructor and the instance + /// is published by one atomic reference assignment, so a reader sees either the complete previous + /// entry or the complete new one — never the Font from one and the XFont from another. + /// + sealed class FontCacheEntry + { + internal FontCacheEntry(Font font, XFont xFont) + { + FontRef = new WeakReference(font); + XFontRef = new WeakReference(xFont); + } + + internal readonly WeakReference FontRef; + internal readonly WeakReference XFontRef; + } + + static volatile FontCacheEntry? _lastEntry; #if FORCE_MEMORYLEAK static Font? _lastFont2; #endif