Tk Source Code

View Ticket
Login
Ticket UUID: e8d4c8a5772de30d0694360d0751eaf580f6172d
Title: X11 (HarfBuzz/SheenBidi font backend): [font configure] of a font in use leaks the old font
Type: Bug Version: 9.1.0rc1
Submitter: anonymous Created on: 2026-09-29 09:07:20
Subsystem: (unused) Assigned To: nobody
Priority: 5 Medium Severity: Important
Status: Closed Last Modified: 2026-10-06 11:53:37
Resolution: Fixed Closed By: nobody
    Closed on:
Description:

Reconfiguring a named font that a widget is using leaks about 31 KB per change with the new X11 font backend (unix/tkUnixBidiFont.c). Tk 9.0 does not leak.

When a named font changes, UpdateDependentFonts() (generic/tkFont.c) passes each live TkFont built from it back to TkpGetFontFromAttributes() to be re-initialized in place. tkUnixRFont.c releases the old contents first:

if (fontPtr != NULL) {
    FinishedWithFont(fontPtr);
}
fontPtr = InitFont(tkwin, pattern, fontPtr);

tkUnixBidiFont.c calls InitFont() directly, so InitFont() overwrites the record's faces array, FcFontSet, FcPattern, Xft fonts, HarfBuzz font/face/blob objects, per-face charsets and the shaper's hb_buffer without freeing them.

Reproducer (wish, any X11 display, e.g. Xvfb):

proc rss {} {
    set f [open /proc/self/statm]; set s [read $f]; close $f
    expr {[lindex $s 1] * 4096 / 1048576.0}
}
font create F -family sans-serif -size 10
label .l -font F -text "The quick brown fox jumps over the lazy dog"
pack .l
update
for {set i 0} {$i < 1500} {incr i} {
    font configure F -size [expr {7 + ($i * 37) % 17}]
    update
    if {$i == 100} {set r0 [rss]}
}
puts [format "%.1f KB per font change" \
    [expr {([rss] - $r0) * 1024 / 1400}]]
exit

Results (Debian 13, amd64, Xvfb):

9.0.4       0.0 KB per font change
9.1.0rc1   31.7 KB per font change, linear, no plateau
patched     0.0 KB per font change

Without the label (font measure only) the leak does not show, because only fonts in use are re-initialized.

valgrind --leak-check=full on the reproducer with 300 changes reports 5.58 MB definitely lost on 9.1.0rc1 and 6.6 KB with the patch, and no invalid reads or writes. The loss records are allocated in InitFont() (the faces array), FcDefaultSubstitute() (the pattern) and FcFontSort() (the fontset), all under UpdateDependentFonts() -> TkpGetFontFromAttributes().

We found this in a long-running application that changes its UI scale at runtime by resizing the named fonts: it grew by about 1.2 MB/s under that load on 9.1.0rc1 and stays flat with the patch.

Proposed fix, the same as tkUnixRFont.c:

--- unix/tkUnixBidiFont.c
+++ unix/tkUnixBidiFont.c
@@ TkpGetFontFromAttributes
     UnixFtFont *fontPtr = (UnixFtFont *)tkFontPtr;
+    if (fontPtr != NULL) {
+     FinishedWithFont(fontPtr);
+    }
     fontPtr = InitFont(tkwin, pattern, fontPtr);

A related detail on the error path of the same function: when the first InitFont() fails, it has already destroyed the pattern (FcPatternDestroy() on the FcFontSort() failure path, or through FinishedWithFont() on the later ones), and TkpGetFontFromAttributes() then calls XftPatternDestroy(pattern) on it again before building the fallback pattern. That looks like a double free, but only on a font that fails to open; we have not hit it.

User Comments: serhiy.storchaka added on 2026-10-06 11:53:22:

Merged into main [13c5fdc004]. Tk 9.0 is not affected.


serhiy.storchaka added on 2026-10-04 10:40:40:

Proposed fix in branch x11-bidi-font-reconfigure-leak. It also fixes two related bugs: the fallback to "sans" destroyed the pattern a second time, and InitFont() used the font id to detect a re-used font record, so a failed re-initialization would have freed everything twice.