Skip to content

font: guard null CTFontCopyDisplayName in DeferredFace.name - #94

Open
lawrencecchen wants to merge 1 commit into
mainfrom
issue-7312-deferredface-null-name
Open

font: guard null CTFontCopyDisplayName in DeferredFace.name#94
lawrencecchen wants to merge 1 commit into
mainfrom
issue-7312-deferredface-null-name

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 4, 2026

Copy link
Copy Markdown

CTFontCopyDisplayName can return NULL; the @ptrFromInt(@intFromPtr(...)) laundering turned that into a non-optional pointer at address 0, and the subsequent CFStringGetCStringPtr deref crashed with EXC_BAD_ACCESS (Sentry CMUXTERM-MACOS-1C0Z, manaflow-ai/cmux#7312).

copyDisplayName now returns an optional and DeferredFace.name falls back to an empty name with a warning log. The only non-test caller is the CoreText branch of DeferredFace.name.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Fixes a macOS crash in the CoreText path when CTFontCopyDisplayName returns NULL. copyDisplayName now returns an optional; DeferredFace.name logs a warning and falls back to an empty string to prevent EXC_BAD_ACCESS.

  • Bug Fixes
    • Make Font.copyDisplayName return an optional to reflect possible NULL from CoreText.
    • In DeferredFace.name, guard NULL, warn, and return an empty name instead of dereferencing.

Written for commit 64c9c97. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling when a font display name is unavailable, preventing unexpected failures.
    • Added a fallback to return an empty font name and log a warning when no display name can be retrieved.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Font.copyDisplayName's return type was changed from a non-null pointer to a nullable pointer (?*foundation.String). DeferredFace.name was updated to handle the null case by logging a warning with font pointer and variation count, then returning an empty string.

Changes

CoreText Display Name Nullability

Layer / File(s) Summary
Nullable return type and nil-handling guard
pkg/macos/text/font.zig, src/font/DeferredFace.zig
copyDisplayName now returns ?*foundation.String; DeferredFace.name() guards the call with orelse, logging a warning with the font pointer and variation count and returning an empty string when no display name is available.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main change: guarding a null CTFontCopyDisplayName result in DeferredFace.name.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-7312-deferredface-null-name

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 4, 2026

Copy link
Copy Markdown

Greptile Summary

Fixes a crash (EXC_BAD_ACCESS) caused by CTFontCopyDisplayName returning NULL, which was previously laundered into a non-optional Zig pointer at address 0 and then immediately dereferenced.

  • pkg/macos/text/font.zig: copyDisplayName return type changed to ?*foundation.String; the existing @ptrFromInt(@intFromPtr(...)) idiom already maps a NULL C pointer to Zig null for optional types, consistent with the copyAttribute pattern in the same file.
  • src/font/DeferredFace.zig: The CoreText branch of DeferredFace.name now handles the null case with an orelse block that emits a log.warn (including the font pointer and variations count for diagnostics) and returns an empty string, matching the fallback at the end of the function.

Confidence Score: 4/5

Safe to merge; the crash fix is correct and the change is narrowly scoped to the null case.

The null-guard and optional return type are correct and consistent with the established copyAttribute pattern in the same file. The only outstanding concern is a pre-existing CFString memory leak in DeferredFace.name — display_name is never released after CTFontCopyDisplayName, and the same is true of copyFamilyName in face/coretext.zig. This PR does not introduce the leak but touches the affected path without addressing it.

src/font/DeferredFace.zig — the CoreText branch of name() leaks the CFString returned by copyDisplayName on every call.

Important Files Changed

Filename Overview
pkg/macos/text/font.zig Changed copyDisplayName return type from *foundation.String to ?*foundation.String to correctly surface the nullable CTFontCopyDisplayName return via the existing @ptrFromInt(@intFromPtr(...)) idiom.
src/font/DeferredFace.zig Added null-guard (orelse) around copyDisplayName() in the CoreText branch of DeferredFace.name; falls back to an empty string with a warning log. The returned display_name CFString object is never released (pre-existing leak).

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Caller
    participant DeferredFace
    participant Font as pkg/macos/text/Font
    participant CT as CoreText C API

    Caller->>DeferredFace: name(buf)
    DeferredFace->>Font: copyDisplayName()
    Font->>CT: CTFontCopyDisplayName(font_ref)
    CT-->>Font: CFStringRef (possibly NULL)
    Note over Font: @ptrFromInt(@intFromPtr(...))<br/>maps NULL to Zig null
    Font-->>DeferredFace: "?*foundation.String"

    alt display_name is null (new guard)
        DeferredFace->>DeferredFace: log.warn(...)
        DeferredFace-->>Caller: return empty string
    else display_name is non-null
        DeferredFace->>Font: cstringPtr(.utf8)
        alt fast path (inline storage)
            Font-->>DeferredFace: "*const u8 pointer"
            DeferredFace-->>Caller: []const u8 slice
        else slow path (allocation needed)
            DeferredFace->>Font: cstring(buf, .utf8)
            Font-->>DeferredFace: copied into buf
            DeferredFace-->>Caller: buf slice
        end
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Caller
    participant DeferredFace
    participant Font as pkg/macos/text/Font
    participant CT as CoreText C API

    Caller->>DeferredFace: name(buf)
    DeferredFace->>Font: copyDisplayName()
    Font->>CT: CTFontCopyDisplayName(font_ref)
    CT-->>Font: CFStringRef (possibly NULL)
    Note over Font: @ptrFromInt(@intFromPtr(...))<br/>maps NULL to Zig null
    Font-->>DeferredFace: "?*foundation.String"

    alt display_name is null (new guard)
        DeferredFace->>DeferredFace: log.warn(...)
        DeferredFace-->>Caller: return empty string
    else display_name is non-null
        DeferredFace->>Font: cstringPtr(.utf8)
        alt fast path (inline storage)
            Font-->>DeferredFace: "*const u8 pointer"
            DeferredFace-->>Caller: []const u8 slice
        else slow path (allocation needed)
            DeferredFace->>Font: cstring(buf, .utf8)
            Font-->>DeferredFace: copied into buf
            DeferredFace-->>Caller: buf slice
        end
    end
Loading

Comments Outside Diff (1)

  1. src/font/DeferredFace.zig, line 187-201 (link)

    P2 display_name leaks a CFString on every call — CTFontCopyDisplayName follows the CoreFoundation Copy rule (caller owns, must release). There is no defer display_name.release() here, so each call to DeferredFace.name() on a CoreText backend leaks one CFStringRef. The same pattern exists in face/coretext.zig:205 for copyFamilyName. Note that adding a defer release would also make the cstringPtr-derived slice dangling for the non-allocation fast path, so the correct fix likely involves copying into buf before releasing and restructuring the return accordingly.

Reviews (1): Last reviewed commit: "font: guard null CTFontCopyDisplayName i..." | Re-trigger Greptile

CTFontCopyDisplayName can return NULL; the @ptrFromInt(@intFromPtr(...))
laundering turned that into a non-optional pointer at address 0, and the
subsequent CFStringGetCStringPtr deref crashed (EXC_BAD_ACCESS, Sentry
CMUXTERM-MACOS-1C0Z, manaflow-ai/cmux#7312).

Make copyDisplayName return an optional and fall back to an empty name
with a warning log in DeferredFace.name.
@lawrencecchen
lawrencecchen force-pushed the issue-7312-deferredface-null-name branch from 411b866 to 64c9c97 Compare July 4, 2026 11:06

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
pkg/macos/text/font.zig (1)

155-157: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Sibling functions have the same latent null-crash pattern fixed here for copyDisplayName.

copyFamilyName and copyPostScriptName use the identical @ptrFromInt(@intFromPtr(...)) unpacking but declare non-optional return types. CTFontCopyFamilyName/CTFontCopyPostScriptName can also return NULL for malformed/corrupt fonts, per real-world reports (e.g. WebKit bug 237373: "crashes when given a font with a nil postscript name, font family, or display name"). These would suffer the exact same EXC_BAD_ACCESS this PR fixes for display name.

Consider making these two nullable as well, for consistency and to close the same class of bug preemptively — though this is out of the stated scope of this PR (display name only).

♻️ Suggested follow-up (separate from this PR's scope)
-    pub fn copyFamilyName(self: *Font) *foundation.String {
+    pub fn copyFamilyName(self: *Font) ?*foundation.String {
         return `@ptrFromInt`(`@intFromPtr`(c.CTFontCopyFamilyName(`@ptrCast`(self))));
     }
...
-    pub fn copyPostScriptName(self: *Font) *foundation.String {
+    pub fn copyPostScriptName(self: *Font) ?*foundation.String {
         return `@ptrFromInt`(`@intFromPtr`(c.CTFontCopyPostScriptName(`@ptrCast`(self))));
     }

Also applies to: 163-165

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/macos/text/font.zig` around lines 155 - 157, `copyFamilyName` and
`copyPostScriptName` use the same unsafe pointer-unwrapping pattern as
`copyDisplayName` and can also crash if CoreText returns NULL for malformed
fonts. Update these sibling methods in `Font` to use nullable return types and
handle a null result safely, matching the fix applied to `copyDisplayName` so
the same EXC_BAD_ACCESS path is closed consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pkg/macos/text/font.zig`:
- Around line 155-157: `copyFamilyName` and `copyPostScriptName` use the same
unsafe pointer-unwrapping pattern as `copyDisplayName` and can also crash if
CoreText returns NULL for malformed fonts. Update these sibling methods in
`Font` to use nullable return types and handle a null result safely, matching
the fix applied to `copyDisplayName` so the same EXC_BAD_ACCESS path is closed
consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6600aa66-be7d-43f8-8637-2ec635653f84

📥 Commits

Reviewing files that changed from the base of the PR and between 8495e58 and 64c9c97.

📒 Files selected for processing (2)
  • pkg/macos/text/font.zig
  • src/font/DeferredFace.zig

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant