Skip to content

Surface: fix leaks on init error paths - #632

Draft
deblasis wants to merge 1 commit into
ghostty-main-basefrom
surface-init-leaks
Draft

Surface: fix leaks on init error paths#632
deblasis wants to merge 1 commit into
ghostty-main-basefrom
surface-init-leaks

Conversation

@deblasis

@deblasis deblasis commented Aug 14, 2026

Copy link
Copy Markdown
Owner

Three allocations in Surface.init are not released when a later step fails. Surface creation is recoverable (the apprt reports the error and the app keeps running), so each failure retains memory for the life of the process.

The font grid reference has no matching deref. app.font_grid_set.ref(...) increments a refcount, and nothing in init releases it on the error paths. Any failure after that call leaves the count above zero, so the SharedGrid and its atlases are never freed and the map entry is never removed, even after every real surface using that font config has closed. setFontSize already registers exactly this errdefer around the same call.

Two derived configs are built inline as call arguments. Renderer.init and Termio.init both receive .config = try .init(alloc, config). Each stores it only on success and neither unwinds it on its own failure paths, so a failure inside either leaks an arena. Renderer.init can fail in graphics API, swapchain or shaper setup; Termio.init can fail in Terminal.init.

Each is now a local guarded by an errdefer scoped to the window before ownership transfers. The scoping matters for the renderer one: at function scope that errdefer would double free the config alongside renderer_impl, so it is confined to a block that ends once Renderer.init returns. The termio one is already inside a block whose last statement is Termio.init.

Reachability is ordinary rather than exotic: a bad command in the config, a failing GPU init, or pty spawn failure all take these paths.

Testing

zig build test on macOS: 3349/3365 passed, 16 skipped, no failures. The one failing build step is xcodebuild test aborting on childPID > 0, which fails identically on the parent commit; that machine has no codesigning certificate.

Not included

Surface.init has a related defect I have deliberately left out. Its errdefers stay live past std.Thread.spawn for the renderer and IO threads, so a failure after that point deinits state the running threads are still reading, with no stop-notify or join. Thread.deinit documents that the caller must join first. On this branch the only reachable trigger is the IO thread spawn failing while the renderer thread is live, since set_title cannot fail on either apprt. The fix needs teardown-ordering judgment rather than an errdefer, so it seemed better raised separately.

Three allocations in Surface.init are not released when a later step
fails. Surface creation is recoverable, so each failure retains memory
for the life of the process.

The font grid reference taken from the shared set has no matching deref.
Any failure after it leaves the refcount above zero, so the SharedGrid
and its atlases are never freed and the map entry is never removed, even
once every real surface using that font config has closed. setFontSize
already registers this errdefer around the same call.

The derived configs passed to Renderer.init and Termio.init are built
inline as arguments. Both callees store them only on success and neither
unwinds them on its own failure paths, so a failure inside either leaks
an arena. Each is now a local guarded by an errdefer scoped to the window
before ownership transfers. Scoping matters for the renderer one: at
function scope that errdefer would double free the config alongside
renderer_impl.
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