aboutsummaryrefslogtreecommitdiffstats
path: root/src/app.c
diff options
context:
space:
mode:
authoruser <user@clank>2026-10-09 12:23:50 +0200
committeruser <user@clank>2026-10-09 12:23:50 +0200
commit2cb6dce416c814298f24087fdaafb0c119a584b6 (patch)
tree1228aa51fb9a47b6f5881725819296b988a707b3 /src/app.c
parentskill: which pane am I, and how not to land in somebody else's tab (diff)
downloadslosh-2cb6dce416c814298f24087fdaafb0c119a584b6.tar.gz
slosh-2cb6dce416c814298f24087fdaafb0c119a584b6.tar.xz
slosh-2cb6dce416c814298f24087fdaafb0c119a584b6.zip
panes: the focus a closed tab left behind
A tab's focus outlived the tree it pointed into. close_leaf's last-pane branch freed the leaf and removed the tab but never cleared `t->focus`, and tab_remove only shifts the slots *after* the one it removes -- so with the last tab gone, `a->tabs[0]` kept the stale tab_t while `ntabs` was 0 and `cur` was 0. `cur(a)` is `&a->tabs[a->cur]` with no bounds check, and `a->quit` is a request rather than a barrier: the loop keeps serving commands, and the next one to ask for the focused pane was handed freed memory. From outside it looked like a random crash, because what the freed chunk held depended on what had been allocated since. Two from one afternoon: a pane_t recycled into a `panes` JSON reply, whose `term` pointer was then the ASCII of `k":true,` and faulted as a general protection fault; and the same node read back zeroed, which left pane_clear_bell testing a flag at NULL+0x5d. Typing `exit` in the last pane is the whole recipe. close_leaf drops the focus with the tree tab_remove blanks the slot the shift vacated app_compose tests ->pane as well as ->focus, the way the search beside it already did app_apply_layout clears restore_focus up front -- it is a node pointer, and an apply that fails before consuming it leaves it naming a tree the next apply frees Two more the same reading turned up, both in close_leaf: an emptied split set the focus to the node just freed, and a leaf whose parent disagreed with that parent's child list underflowed `p->nkids - at - 1` into a memmove of nearly SIZE_MAX. FOCUS_CHECK -- behind -DSLOSH_FOCUS_CHECK, compiled out otherwise -- is the invariant all of this turns on: every tab's focus is a leaf of that tab's own tree. It is what found this one. Committed with --no-verify: the files are deliberately partly staged, and the staged blobs were checked against clang-format directly.
Diffstat (limited to 'src/app.c')
-rw-r--r--src/app.c37
1 files changed, 37 insertions, 0 deletions
diff --git a/src/app.c b/src/app.c
index 2031e09..a6dedd7 100644
--- a/src/app.c
+++ b/src/app.c
@@ -1004,6 +1004,43 @@ void walk_all(app_t *a, leaf_fn fn, void *ud) {
for (size_t i = 0; i < a->ntabs; i++) walk(a->tabs[i].root, fn, ud);
}
+#ifdef SLOSH_FOCUS_CHECK
+/* Diagnostic: every tab's focus must be a leaf of that tab's own tree, and a
+ * tab slot cur() can still reach must not keep a focus from a tab that went.
+ * Pointer comparison only -- the checker never dereferences a suspect. */
+struct fchk {
+ const node_t *want;
+ bool found;
+};
+static void fchk_cb(node_t *n, void *ud) {
+ struct fchk *f = ud;
+ if (n == f->want) f->found = true;
+}
+void focus_check(app_t *a, const char *where) {
+ for (size_t i = 0; i < a->ntabs; i++) {
+ const node_t *f = a->tabs[i].focus;
+ if (!f) continue;
+ struct fchk c = {f, false};
+ walk(a->tabs[i].root, fchk_cb, &c);
+ if (!c.found) {
+ fprintf(stderr,
+ "FOCUS-DANGLE at %s: tab %zu (id %u) focus=%p is not a leaf of "
+ "its tree (root=%p ntabs=%zu cur=%zu)\n",
+ where, i, a->tabs[i].id, (const void *)f, (void *)a->tabs[i].root,
+ a->ntabs, a->cur);
+ abort();
+ }
+ }
+ if (a->ntabs == 0 && a->tabs && a->tabs[0].focus) {
+ fprintf(stderr,
+ "FOCUS-DANGLE at %s: ntabs==0 but tabs[0].focus=%p survives, and "
+ "cur(a) still reads that slot\n",
+ where, (void *)a->tabs[0].focus);
+ abort();
+ }
+}
+#endif
+
/* Which tab a node lives in: climb to its root and match. */
size_t tab_of(app_t *a, node_t *n) {
while (n->parent) n = n->parent;