Ghostty segfaults during CoreText font discovery when any installed font has a NULL display name (CTFontCopyDisplayName → NULL)
Heads up on process: ghostty's
CONTRIBUTING.mdasks 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).
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.
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
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.
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.
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, healthyConfirming 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.
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").
- 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.