gtk: fix stale pointers to property bindings

Previously, the property binding created in `Surface.bindIsSplit` would
get freed automatically when the source object (the SplitTree widget)
got finalized. A subsequent call to `bindIsSplit` could then cause a
crash by using the stale pointer to the binding. This bug could e.g. be
triggered by dragging the surface from a single-surface tab to another
tab.

We now create an extra reference to the binding object so that Surface
essentially owns the binding and is responsible for freeing it.

Updated the binding in `SurfaceScrolledWindow` to use the same pattern.
That one was probably fine, because the binding is only created once,
but let's be safe.
This commit is contained in:
Daniel Kinzler
2026-08-28 14:25:33 +02:00
parent 4540d499ae
commit 6cd684d5d3
2 changed files with 27 additions and 5 deletions

View File

@@ -853,14 +853,24 @@ pub const Surface = extern struct {
pub fn bindIsSplit(self: *Self, tree: *SplitTree) void {
const priv = self.private();
if (priv.is_split_binding) |bind| bind.unbind();
if (priv.is_split_binding) |binding| {
binding.unbind();
binding.unref();
priv.is_split_binding = null;
}
priv.is_split_binding = tree.as(gobject.Object).bindProperty(
const binding = tree.as(gobject.Object).bindProperty(
"is-split",
self.as(gobject.Object),
"is-split",
.{ .sync_create = true },
);
// The ref created by bindProperty is owned by the binding itself.
// We need another ref to prevent the binding object from being
// freed if the source object (SplitTree) is finalized. Otherwise
// our pointer to the binding could become stale.
binding.ref();
priv.is_split_binding = binding;
}
/// Callback used to determine whether unfocused-split-fill / unfocused-split-opacity
@@ -1881,6 +1891,12 @@ pub const Surface = extern struct {
priv.config = null;
}
if (priv.is_split_binding) |binding| {
binding.unbind();
binding.unref();
priv.is_split_binding = null;
}
if (priv.vadj_signal_group) |group| {
group.setTarget(null);
group.as(gobject.Object).unref();

View File

@@ -89,7 +89,8 @@ pub const SurfaceScrolledWindow = extern struct {
const priv = self.private();
if (priv.config_binding) |binding| {
binding.as(gobject.Object).unref();
binding.unbind();
binding.unref();
priv.config_binding = null;
}
@@ -168,18 +169,23 @@ pub const SurfaceScrolledWindow = extern struct {
// Unbind old config binding if it exists
if (priv.config_binding) |binding| {
binding.as(gobject.Object).unref();
binding.unbind();
binding.unref();
priv.config_binding = null;
}
// Bind config from surface to our config property
if (priv.surface) |surface| {
priv.config_binding = surface.as(gobject.Object).bindProperty(
const binding = surface.as(gobject.Object).bindProperty(
properties.config.name,
self.as(gobject.Object),
properties.config.name,
.{ .sync_create = true },
);
// Keep another ref, otherwise the binding would be freed and
// our pointer become stale if the surface gets finalized.
binding.ref();
priv.config_binding = binding;
}
}