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):
- app thread:
ghostty_surface_free->Surface.deinit->self.io_thr.join()(https://github.com/ghostty-org/ghostty/blob/683d8db643b95cf229bfb5fe9fab9ae677920343/src/Surface.zig#L818) - io thread:
termio.Thread.threadMain->Exec.threadExit->exec.read_thread.join()(https://github.com/ghostty-org/ghostty/blob/683d8db643b95cf229bfb5fe9fab9ae677920343/src/termio/Exec.zig#L229) - io-reader (parse stage):
ReadThread.threadMainPosix->Termio.processOutput-> a stream handler action ->surfaceMessageWriter(https://github.com/ghostty-org/ghostty/blob/683d8db643b95cf229bfb5fe9fab9ae677920343/src/termio/stream_handler.zig#L130-L140) ->apprt.surface.Mailbox.push->App.Mailbox.push->BlockingQueue.pushwaiting oncond_not_full(https://github.com/ghostty-org/ghostty/blob/683d8db643b95cf229bfb5fe9fab9ae677920343/src/datastruct/blocking_queue.zig#L115) - the queue is
BlockingQueue(Message, 64)(https://github.com/ghostty-org/ghostty/blob/683d8db643b95cf229bfb5fe9fab9ae677920343/src/App.zig#L611) and its only consumer isApp.drainMailboxfromApp.tick(https://github.com/ghostty-org/ghostty/blob/683d8db643b95cf229bfb5fe9fab9ae677920343/src/App.zig#L265), on the app thread
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.
- Create a surface and run in it (zsh):
while :; do printf '\e]0;t%s\a' $RANDOM; done - Close that surface from the app thread.
- The close never returns.
sample <pid>shows the app thread inSurface.deinit->pthread_join, the surface'siothread in thekillPidloop (single surface) or inpthread_joinon its reader (eight spamming surfaces, one closed), and itsio-readerinapprt.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 injectedThat 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 = pasteAdditional 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
5Happens 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)
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'siothread - that
iothread:pthread_join→ waiting on itsio-reader io-reader: futex wait inside ghostty code (binary is stripped, so the frame is unsymbolized, offset0x1539c3a)
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+0x13684a8Kernel stacks for all three: futex_do_wait ← __futex_wait ← futex_wait ← do_futex.
Please prioritize this, this is happening multiple times a day, causing me to look for a different TTY now.
Please prioritize this, this is happening multiple times a day, causing me to look for a different TTY now.
womp womp
I can reproduce on Linux/GTK with this script:
run as
ghostty -e ./spam.shand Ghostty will hang here: