#14245

libghostty: ghostty_surface_free deadlocks the app thread when the surface's reader is blocked pushing into the app mailbox

Openjcollieopened 1mo ago

Discussed in https://github.com/ghostty-org/ghostty/discussions/14234

Originally posted by umputun September 14, 2026

Issue Description

Freeing a surface can deadlock the app thread forever. Surface.deinit joins the io thread on the calling (app) thread, the io thread joins its reader thread, and the reader can be blocked in a .forever push into the app mailbox, a queue only the app thread drains. Once the app thread is inside the join nothing drains the queue, so the reader never returns, the io thread never returns, and ghostty_surface_free never returns. Every other surface whose reader then posts a surface message parks in the same push.

The wait chain at 683d8db (unchanged on d5eba8d16):

In the original user's sample the blocked reader came through a CSI action (Stream.csiDispatchFinal -> StreamHandler.vt -> Mailbox.push) and four other surfaces' readers were parked in the same push from StreamHandler.windowTitle. In my reproductions the closing surface's reader is in windowTitle -> Mailbox.push.

A second shape of the same hang, seen in every single-surface run: the reader stall fills the 4-slot gather ring, the gather stage stops reading the pty, and Exec.threadExit never reaches the quit-pipe write because Subprocess.stop -> killPid (https://github.com/ghostty-org/ghostty/blob/683d8db643b95cf229bfb5fe9fab9ae677920343/src/termio/Exec.zig#L1176-L1214) loops on killpg / waitpid(WNOHANG) / 10 ms sleep with no bound. ps shows the child's login wrapper in exit state with its own child a zombie or also exiting, and waitpid never returns it. I read that as the wrapper's exit blocking on tty output drain; the sample proves the loop and the full ring, not the kernel wait.

This is an embedding report: libghostty built from 683d8db, embedded in agterm (macOS, SwiftUI; https://github.com/umputun/agterm). A user hit it with 10 surfaces open (https://github.com/umputun/agterm/issues/606), and it reproduces on demand with the recipe below, in the shipped agterm 0.28.0 and in a Debug build using the same pinned libghostty. Ghostty.app 1.3.1 and the tip build (d5eba8d16) did not hang when the spamming command exited on its own and the surface closed on child exit; closing a still-producing Ghostty.app surface was not tested.

Expected Behavior

ghostty_surface_free returns. A surface whose program is producing output at the moment of the close is torn down like any other; at worst its pending output is dropped.

Actual Behavior

ghostty_surface_free blocks forever in pthread_join. The app thread is gone, so every surface in the process stops updating and the process has to be killed with SIGKILL. In the io-join shape the closed surface's child was already reaped; in the kill-loop shape its login wrapper is stuck in exit with a zombie or exiting child.

Reproduction Steps

Tested host: agterm 0.28.0 (and a Debug build using the same pinned libghostty) on macOS 26.6.2, libghostty 683d8db. The close is agterm's agtermctl session close, which calls ghostty_surface_free on the app thread.

  1. Create a surface and run in it (zsh): while :; do printf '\e]0;t%s\a' $RANDOM; done
  2. Close that surface from the app thread.
  3. The close never returns. sample <pid> shows the app thread in Surface.deinit -> pthread_join, the surface's io thread in the killPid loop (single surface) or in pthread_join on its reader (eight spamming surfaces, one closed), and its io-reader in apprt.surface.Mailbox.push -> __ulock_wait2.

Control runs: on the 0.28.0 instance, with the spamming surface open and no close, the app thread answered 10 control requests over 10 seconds; on the Debug instance, closing one idle surface with eight idle surfaces open took 67 ms.

Raw sample outputs (Zig frames stripped) and the symbolication table resolving them against libghostty-internal.a: https://gist.github.com/umputun/9ffd6da3e3b93d649ae9c109c59823d4

Ghostty Logs

2026-09-14 13:21:17.565 E  agterm[69525:1d9e6a0] [com.mitchellh.ghostty:default] io_exec: shell could not be detected, no automatic shell integration will be injected

That is the only com.mitchellh.ghostty line during the 0.28.0 run; nothing is logged at the hang.

Ghostty Version

libghostty from ghostty-org/ghostty 683d8db643b95cf229bfb5fe9fab9ae677920343 (2026-08-25)
zig build -Doptimize=ReleaseFast -Demit-xcframework=true -Dxcframework-target=native, zig 0.16.0
embedded in agterm 0.28.0 (https://github.com/umputun/agterm)
the cited code is unchanged on main at d5eba8d16 (Ghostty 1.3.2-main-+d5eba8d16)

OS Version Information

macOS 26.6.2 (25G83), Apple Silicon

Minimal Ghostty Configuration

The config agterm passes to libghostty in the reproduction (theme = agterm is a theme file agterm bundles; the other two are unrelated to the hang):

theme = agterm
mouse-scroll-multiplier = 3
right-click-action = paste

Additional Relevant Configuration

Downstream report with the user's samples: https://github.com/umputun/agterm/issues/606

AI usage disclosure: Claude Code and Codex, driven by me, did the offset symbolication (instruction-sequence matching against libghostty-internal.a), ran the reproductions and wrote the first draft of this text. I reviewed the cited code paths and edited the text.

I acknowledge that:

  • I have reviewed the FAQ and confirm that my issue is NOT among them.
  • I have searched the Ghostty documentation and confirm there doesn't exist a configuration option that solves my issue.
  • I have searched the Ghostty repository (both open and closed Discussions and Issues) and confirm this is not a duplicate of an existing issue or discussion.
  • I have checked the "Preview" tab on all text fields to ensure that everything looks right, and have wrapped all configuration and code in code blocks with a group of three backticks (```) on separate lines.

Activity

5
jcollie1mo ago

I can reproduce on Linux/GTK with this script:

#!/bin/sh
for i in 1 2 3 4; do
  setsid timeout 60 bash -c 'while :; do printf "\033]0;t$RANDOM\007"; done' &
done
sleep 3

run as ghostty -e ./spam.sh and Ghostty will hang here:

debug(app): mailbox message=surface_message
debug(surface): changing title "t22020"
debug(gtk_ghostty_application): must_quit due to no app windows
debug(gtk_ghostty_application): exiting runloop
debug(glib): DEBUG: dconf: sync
debug(renderer_thread): mailbox message=.{ .focus = false }
debug(io_thread): mailbox message=focused
debug(renderer_thread): starting renderer thread shutdown
debug(renderer_thread): renderer thread exited
debug(io_thread): starting IO thread shutdown
AKhozya1mo ago

Happens quite often for me during switch to ghostty from somewhere or on attaching external monitor to mac. Symptoms - 1st active tab hangs, on switch to other tab it hangs and then whole ghostty hangs. MacOS 27.0 latest build from tip (28 Sep one)

jakob13791mo ago

Another reproduction, different environment: Linux/Wayland on niri, NixOS, kernel 7.2.6, ghostty 1.3.1 (nixpkgs), GTK single-instance (--gtk-single-instance=true --initial-window=false), default io_uring async backend.

Whole app (all windows) froze. All threads at 0% CPU. Same join chain as the original report:

  • main thread: pthread_join → waiting on a surface's io thread
  • that io thread: pthread_join → waiting on its io-reader
  • io-reader: futex wait inside ghostty code (binary is stripped, so the frame is unsymbolized, offset 0x1539c3a)

The stuck surface's renderer thread and child process were already gone, so it was mid-teardown. Every other surface's io/renderer threads were idle normally in io_cqring_wait/poll.

gdb (relevant threads)
Thread 3 (LWP 1125098) "io":
#3  __pthread_clockjoin_ex () from libc.so.6
#4  0x0000569f892ba46d in ?? ()   // ghostty+0x144c46d

Thread 2 (LWP 1125100) "io-reader":
#0  0x0000569f893a7c3a in ?? ()   // ghostty+0x1539c3a (futex)

Thread 1 (LWP 1093803) main:
#3  __pthread_clockjoin_ex () from libc.so.6
#4  0x0000569f891d64a8 in ?? ()   // ghostty+0x13684a8

Kernel stacks for all three: futex_do_wait ← __futex_wait ← futex_wait ← do_futex.

lovehumans8d ago

Please prioritize this, this is happening multiple times a day, causing me to look for a different TTY now.

trag1c8d ago

Please prioritize this, this is happening multiple times a day, causing me to look for a different TTY now.

womp womp