gtk: fix crash caused by missing dbus connection (#13101)

Fixes #13075 where GTK app will crash if a D-Bus connection can't be
opened. If you have global keybinds set in your config, Ghostty will
crash immediately in both debug and release builds. With no global
keybinds it still crashes when you reload the config, but only in builds
with safety checks enabled, due to a failed assertion.

This problem is rooted in `GlobalShortcuts`, which implements the XDG
global shortcuts protocol. The `refresh` function is triggered every
time the config changes and once on startup. If there are global
keybinds in the config but no D-Bus connection, `refresh` will still try
to setup the global keybinds by calling the `request` method, which will
use `priv.dbus_connection.?` while the field is null. Depending on the
build mode this either fails the Zig runtime safety check immediately or
eventually causes a segmentation fault somewhere in `gio/glib` when the
null pointer is used.
Additionally, even if there are no global keybinds set, Ghostty will
still crash when the config is reloaded, because the `close` function
exits early if `dbus_connection` is null and doesn't clean up the arena
that was created in the first call to `refresh` on startup. The next
call to `refresh` will then fail the `priv.arena == null` assertion.
This only happens if built with safety checks enabled.

As a fix `close` will now always clean up the arena and `refresh` will
exit early if there is no D-Bus connection.

To easily reproduce the crash, change
`Application.startupGlobalShortcuts` (in
`src/apprt/gtk/class/application.zig`) to set the D-Bus connection to
null with `priv.global_shortcuts.setDbusConnection(null)`. Then run with
a global keybind e.g. `ghostty
--keybind="global:ctrl+o=toggle_quick_terminal"`.

#### AI Disclosure
No AI was used.
This commit is contained in:
Leah Amelia Chen
2026-06-26 21:07:12 +08:00
committed by GitHub

View File

@@ -108,34 +108,35 @@ pub const GlobalShortcuts = extern struct {
fn close(self: *Self) void {
const priv = self.private();
const dbus = priv.dbus_connection orelse return;
if (priv.response_subscription != 0) {
dbus.signalUnsubscribe(priv.response_subscription);
priv.response_subscription = 0;
}
if (priv.dbus_connection) |dbus| {
if (priv.response_subscription != 0) {
dbus.signalUnsubscribe(priv.response_subscription);
priv.response_subscription = 0;
}
if (priv.activate_subscription != 0) {
dbus.signalUnsubscribe(priv.activate_subscription);
priv.activate_subscription = 0;
}
if (priv.activate_subscription != 0) {
dbus.signalUnsubscribe(priv.activate_subscription);
priv.activate_subscription = 0;
}
if (priv.handle) |handle| {
// Close existing session
dbus.call(
"org.freedesktop.portal.Desktop",
handle,
"org.freedesktop.portal.Session",
"Close",
null,
null,
.{},
-1,
null,
null,
null,
);
priv.handle = null;
if (priv.handle) |handle| {
// Close existing session
dbus.call(
"org.freedesktop.portal.Desktop",
handle,
"org.freedesktop.portal.Session",
"Close",
null,
null,
.{},
-1,
null,
null,
null,
);
priv.handle = null;
}
}
if (priv.arena) |*arena| {
@@ -151,7 +152,8 @@ pub const GlobalShortcuts = extern struct {
const priv = self.private();
// We need configuration to proceed.
// We need a dbus connection and configuration to proceed.
if (priv.dbus_connection == null) return;
const config = if (priv.config) |v| v.get() else return;
// Setup our new arena that we'll use for memory allocations.