Skip to content

Instantly share code, notes, and snippets.

@BasixKOR
Created July 9, 2026 17:41
Show Gist options
  • Select an option

  • Save BasixKOR/ed742c8c9d5c8f0ede401cd48758d8d3 to your computer and use it in GitHub Desktop.

Select an option

Save BasixKOR/ed742c8c9d5c8f0ede401cd48758d8d3 to your computer and use it in GitHub Desktop.
Ghostty CoreText NULL-display-name crash reproducer

Ghostty segfaults during CoreText font discovery when any installed font has a NULL display name (CTFontCopyDisplayName → NULL)

Heads up on process: ghostty's CONTRIBUTING.md asks bugs to start as an "Issue Triage" discussion, not a raw issue (new issues are disabled in the repo). This is written as an issue body but should be filed at https://github.com/ghostty-org/ghostty/discussions/new?category=issue-triage — the content maps 1:1. Please search open discussions/issues first and upvote instead of duplicating if a match exists (closest neighbors I found are listed at the bottom — none are this exact bug).

Summary

On macOS, DeferredFace.name() (CoreText backend) calls CTFontCopyDisplayName() and immediately dereferences the result with no NULL check. CoreText returns NULL for the display name of any font whose name table lacks a usable full-name / family-name record. When font discovery walks the installed fonts and hits such a font, Ghostty dereferences NULL and dies with EXC_BAD_ACCESS at address 0x0 inside CoreFoundation.

This takes down the whole terminal. In practice it surfaces as "Ghostty quits the moment I type" — typing (e.g. in a TUI that needs a glyph missing from the primary font) kicks off fallback discovery, which enumerates every installed font, one of which has the bad metadata.

The same crash is reproducible headlessly and deterministically with ghostty +list-fonts.

Version

Ghostty 1.3.2-main-+b14d92383 (channel: tip)
Zig 0.15.2, ReleaseFast, font engine: coretext, renderer: Metal
macOS 26.5.1 (25F80), Apple Silicon

Crash signature

From the breakpad minidump inside the .ghosttycrash Sentry envelope (~/.local/state/ghostty/crash/):

exception: EXC_BAD_ACCESS (code 0x1), fault address = 0x0
crashing thread pc: CoreFoundation + 0x15078c   (CFSt*/cstring path)
                lr: CoreFoundation + 0x7620
frame #2:          ghostty + 0x4f0ed4          == font.DeferredFace.name + 324

Frame #2 is DeferredFace.name. The NULL fault address matches a NULL CFStringRef being read as a C string.

Root cause

src/font/DeferredFace.zig, name() (CoreText branch), around line 187:

.coretext,
.coretext_freetype,
.coretext_harfbuzz,
.coretext_noshape,
=> if (self.ct) |ct| {
    const display_name = ct.font.copyDisplayName();          // <-- can be NULL
    return display_name.cstringPtr(.utf8) orelse unsupported: {
        break :unsupported display_name.cstring(buf, .utf8) orelse
            return error.OutOfMemory;
    };
},

copyDisplayName() in pkg/macos/text/font.zig wraps CTFontCopyDisplayName and declares a non-optional return, so a NULL from CoreText is silently turned into a *foundation.String pointing at 0x0:

pub fn copyDisplayName(self: *Font) *foundation.String {
    return @ptrFromInt(@intFromPtr(c.CTFontCopyDisplayName(@ptrCast(self))));
}

Apple documents CTFontCopyDisplayName as returning a non-null CFStringRef, but empirically it does return NULL for fonts whose name table has no full-name (nameID 4) / family (nameID 1) record. cstringPtr/cstring then dereferences 0x0 → segfault. There is no try/error path that can catch this; it's a hard crash in the CoreFoundation call, off the main thread (discovery runs on a worker), so it brings the process down.

Note familyName() a few lines up (line 157) is already defensive — it uses copyAttribute(.family_name) orelse return "unknown". name() is missing the equivalent guard.

Reproduction — no special/third-party app needed

The bad metadata in my case came from a Korean font-subscription app (Sandoll Cloud) that registers OTFs with nil-name descriptors, but the trigger is generic: any installed font whose CoreText display name resolves to NULL. Here's a self-contained repro that builds such a font from scratch and registers it at session scope (auto-reverts on logout, and the script unregisters it).

1. Build a minimal, valid, fixed-pitch font with no full-name record (make_nilname_font.py):

#!/usr/bin/env python3
# pip install fonttools && python3 make_nilname_font.py
from fontTools.fontBuilder import FontBuilder
from fontTools.pens.ttGlyphPen import TTGlyphPen
from fontTools.ttLib.tables.O_S_2f_2 import Panose

fb = FontBuilder(1000, isTTF=True)
fb.setupGlyphOrder([".notdef", "A"])
fb.setupCharacterMap({0x41: "A"})
pen = TTGlyphPen(None)
pen.moveTo((100, 0)); pen.lineTo((100, 700)); pen.lineTo((500, 700)); pen.lineTo((500, 0)); pen.closePath()
fb.setupGlyf({".notdef": TTGlyphPen(None).glyph(), "A": pen.glyph()})
fb.setupHorizontalMetrics({".notdef": (600, 0), "A": (600, 100)})
fb.setupHorizontalHeader(ascent=800, descent=-200)

# NOTE: no "fullName" (nameID 4) — this is the trigger.
fb.setupNameTable({"familyName": "NilNameRepro", "styleName": "Regular",
                   "psName": "NilNameRepro-Regular"})

# Fixed-pitch so the font matches ghostty's monospace discovery query.
panose = Panose()
panose.bFamilyType = 2; panose.bWeight = 5; panose.bProportion = 9  # monospaced
for a in ("bSerifStyle","bContrast","bStrokeVariation","bArmStyle","bLetterform","bMidline","bXHeight"):
    setattr(panose, a, 0)
fb.setupOS2(panose=panose)
fb.setupPost(isFixedPitch=1)
fb.save("NilNameRepro-NoFullName.ttf")
print("wrote NilNameRepro-NoFullName.ttf")

2. Register it (session scope) and run discovery (regctl.swift):

// swift regctl.swift register|unregister <font-path>
import CoreText, Foundation
let a = CommandLine.arguments
let url = URL(fileURLWithPath: a[2]) as CFURL
var err: Unmanaged<CFError>?
let ok = a[1] == "register"
    ? CTFontManagerRegisterFontsForURL(url, .session, &err)
    : CTFontManagerUnregisterFontsForURL(url, .session, &err)
print(ok ? "\(a[1]) OK" : "FAILED: \(err!.takeRetainedValue().localizedDescription)")
$ python3 make_nilname_font.py
$ swift regctl.swift register NilNameRepro-NoFullName.ttf
$ /Applications/Ghostty.app/Contents/MacOS/ghostty +list-fonts ; echo "exit=$?"
# -> exit=1, prints nothing, a new ~/.local/state/ghostty/crash/*.ghosttycrash appears
$ swift regctl.swift unregister NilNameRepro-NoFullName.ttf
$ /Applications/Ghostty.app/Contents/MacOS/ghostty +list-fonts >/dev/null ; echo "exit=$?"
# -> exit=0, healthy

Confirming the descriptor CoreText produces for this file:

descriptor: family=NilNameRepro name=NilNameRepro-Regular display=<nil>
CTFontCopyDisplayName -> <NULL>

+list-fonts reaches the buggy path because it enumerates with .monospace = true and calls face.name(&buf) on every match (src/cli/list_fonts.zig:118-131). The interactive crash is the same code reached via fallback discovery when rendering a missing glyph.

Suggested fix

Guard the NULL, mirroring what familyName() already does. Two options:

Minimal — in DeferredFace.name(): make copyDisplayName fallible/optional and fall back to family name or PostScript name, else a placeholder:

=> if (self.ct) |ct| {
    const display_name = ct.font.copyDisplayName() orelse
        return ct.font.copyFamilyNameOrUnknown(...); // or just return "unknown"
    ...
},

Cleaner — in pkg/macos/text/font.zig: change the wrapper to return an optional so callers must handle NULL (this also future-proofs other call sites):

pub fn copyDisplayName(self: *Font) ?*foundation.String {
    return @ptrFromInt(@intFromPtr(c.CTFontCopyDisplayName(@ptrCast(self))));
}

Either way the goal is: a single font with degenerate name metadata must not crash discovery — it should be skipped or given a placeholder name. This is the CoreText analogue of #2991 ("Graceful handling of font files that fail to load").

Possibly related (not duplicates)

  • Discussion #10420 — "Segfault in claude before its first answer in Fontconfig.discover" — same symptom (crash in discovery when a TUI renders), Linux/fontconfig path rather than CoreText.
  • Issue #2991 — "Graceful handling of font files that fail to load" — same theme (one bad font shouldn't crash), different failure mode.
  • Discussion #8626 / #10949 — font-replacement and broken-font-config crashes.
#!/usr/bin/env python3
# Builds NilNameRepro-NoFullName.ttf: a valid, minimal, fixed-pitch TTF whose
# `name` table has family/subfamily/PostScript names but NO full font name
# (nameID 4). CoreText resolves such a font with CTFontCopyDisplayName == NULL,
# which crashes ghostty's font discovery (DeferredFace.name()).
#
# pip install fonttools && python3 make_nilname_font.py
from fontTools.fontBuilder import FontBuilder
from fontTools.pens.ttGlyphPen import TTGlyphPen
from fontTools.ttLib.tables.O_S_2f_2 import Panose
fb = FontBuilder(1000, isTTF=True)
fb.setupGlyphOrder([".notdef", "A"])
fb.setupCharacterMap({0x41: "A"})
pen = TTGlyphPen(None)
pen.moveTo((100, 0))
pen.lineTo((100, 700))
pen.lineTo((500, 700))
pen.lineTo((500, 0))
pen.closePath()
fb.setupGlyf({".notdef": TTGlyphPen(None).glyph(), "A": pen.glyph()})
fb.setupHorizontalMetrics({".notdef": (600, 0), "A": (600, 100)})
fb.setupHorizontalHeader(ascent=800, descent=-200)
# NOTE: no "fullName" (nameID 4) — this is the trigger.
fb.setupNameTable({
"familyName": "NilNameRepro",
"styleName": "Regular",
"psName": "NilNameRepro-Regular",
})
# Fixed-pitch so the font matches ghostty's monospace discovery query
# (`+list-fonts` filters on the monospace symbolic trait).
panose = Panose()
panose.bFamilyType = 2
panose.bWeight = 5
panose.bProportion = 9 # monospaced
for attr in ("bSerifStyle", "bContrast", "bStrokeVariation", "bArmStyle",
"bLetterform", "bMidline", "bXHeight"):
setattr(panose, attr, 0)
fb.setupOS2(panose=panose)
fb.setupPost(isFixedPitch=1)
fb.save("NilNameRepro-NoFullName.ttf")
print("wrote NilNameRepro-NoFullName.ttf")
// Register or unregister a font URL at session scope.
// usage: swift regctl.swift register|unregister /path/to/font.ttf
import CoreText
import Foundation
let args = CommandLine.arguments
guard args.count == 3, ["register", "unregister"].contains(args[1]) else {
print("usage: regctl.swift register|unregister <font-path>"); exit(2)
}
let url = URL(fileURLWithPath: args[2]) as CFURL
var err: Unmanaged<CFError>?
let ok = args[1] == "register"
? CTFontManagerRegisterFontsForURL(url, .session, &err)
: CTFontManagerUnregisterFontsForURL(url, .session, &err)
if ok {
print("\(args[1]) OK: \(args[2])")
} else {
print("\(args[1]) FAILED: \(err?.takeRetainedValue().localizedDescription ?? "unknown error")")
exit(1)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment