Skip to content

fix(tmux): use-after-free in receivedListWindows action slice#11213

Open
daiimus wants to merge 1 commit intoghostty-org:mainfrom
daiimus:fix/tmux-viewer-uaf-list-windows
Open

fix(tmux): use-after-free in receivedListWindows action slice#11213
daiimus wants to merge 1 commit intoghostty-org:mainfrom
daiimus:fix/tmux-viewer-uaf-list-windows

Conversation

@daiimus
Copy link
Copy Markdown

@daiimus daiimus commented Mar 7, 2026

Summary

receivedListWindows stores windows.items — a slice into a local std.ArrayList(Window) — directly into the action list. The ArrayList is freed by defer windows.deinit(self.alloc) on function return, leaving the action holding a dangling pointer.

Currently latent: consumers only read .len (inline in the fat pointer), not the pointed-to data. Becomes a live crash when anything iterates action.windows[i].

Fix

Dupe the slice into the arena allocator so it shares the action list's lifetime:

const arena_windows = try arena_alloc.dupe(Window, windows.items); try actions.append(arena_alloc, .{ .windows = arena_windows });

Test

Added "list-windows action data survives function return" — runs through the startup sequence and dereferences action.windows[0].id, .width, .height in the check callback. Reads freed memory without the fix.

AI Disclosure

Bug was identified and fix was authored with Claude Code assistance. I understand the root cause: the local ArrayList's .items slice becomes invalid after deinit, but was stored into the action struct that outlives the function scope. The fix copies into the arena that backs all actions.

receivedListWindows stores a slice of Window structs into the action
list so callers can process GUI window changes. Previously, this slice
pointed directly into a local ArrayList's backing memory, which was
freed by defer on function return. The action then held a dangling
pointer.

This is currently latent because consumers only read the slice length
(inline in the fat pointer), not the pointed-to data. It becomes a
live crash the moment anyone iterates the window contents.

Fix: dupe the slice into the arena allocator so it shares the same
lifetime as the action list.

Add a regression test that dereferences action.windows[0].id/width/
height after the function returns, which would read freed memory
without the fix.
Copilot AI review requested due to automatic review settings March 7, 2026 19:57
@daiimus daiimus requested a review from a team as a code owner March 7, 2026 19:57
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a use-after-free bug in the tmux viewer's receivedListWindows function. Previously, windows.items — a slice backed by a local std.ArrayList — was stored directly into the action list. Since the ArrayList is freed by a defer when the function returns, this left the action holding a dangling pointer. The fix duplicates the slice into the arena allocator that backs the action list, ensuring the data survives the function return.

Changes:

  • Duplicate windows.items into arena_alloc before appending to the action list, preventing a use-after-free
  • Added a regression test that exercises the full startup-to-list-windows flow and validates that window data in the action is accessible after the function returns

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// is a local ArrayList whose backing memory is freed by the
// defer above when this function returns.
const arena_windows = try arena_alloc.dupe(Window, windows.items);
try actions.append(arena_alloc, .{ .windows = arena_windows });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to check in more detail, but on first pass this doesn't seem right. Why not change windows to be on the arena, possibly layout too so it's not in its own arena but using the shared arena?

Please explain your thinking, not the agents.

daiimus pushed a commit to daiimus/ghostty that referenced this pull request Mar 11, 2026
…ship

Replace per-window layout_arena with a shared windows_arena on Viewer.
All window and layout allocations now happen on a single arena that is
reset as a unit during receivedListWindows (free_all) and layoutChanged
(retain_capacity + selective rebuild via Layout.clone).

This addresses Mitchell's review on PR ghostty-org#11213: 'Why not change windows
to be on the arena, possibly layout too so it's not in its own arena
but using the shared arena?'

Changes:
- Add windows_arena field to Viewer, initialized from action_arena's
  child allocator
- Remove layout_arena from Window and Window.deinit()
- receivedListWindows: reset windows_arena, rebuild all windows+layouts
- layoutChanged: reset windows_arena with retain_capacity, clone
  unchanged windows' layouts via new Layout.clone() method
- syncLayouts: remove window.deinit() calls (arena owns memory)
- Add Layout.clone() for deep-copying layout trees across allocators
- Add 3 new tests: clone leaf, clone split tree, shared arena
  preservation across layout changes
daiimus pushed a commit to daiimus/ghostty that referenced this pull request Mar 12, 2026
…ship

Replace per-window layout_arena with a shared windows_arena on Viewer.
All window and layout allocations now happen on a single arena that is
reset as a unit during receivedListWindows (free_all) and layoutChanged
(retain_capacity + selective rebuild via Layout.clone).

This addresses Mitchell's review on PR ghostty-org#11213: 'Why not change windows
to be on the arena, possibly layout too so it's not in its own arena
but using the shared arena?'

Changes:
- Add windows_arena field to Viewer, initialized from action_arena's
  child allocator
- Remove layout_arena from Window and Window.deinit()
- receivedListWindows: reset windows_arena, rebuild all windows+layouts
- layoutChanged: reset windows_arena with retain_capacity, clone
  unchanged windows' layouts via new Layout.clone() method
- syncLayouts: remove window.deinit() calls (arena owns memory)
- Add Layout.clone() for deep-copying layout trees across allocators
- Add 3 new tests: clone leaf, clone split tree, shared arena
  preservation across layout changes
daiimus pushed a commit to daiimus/ghostty that referenced this pull request Mar 15, 2026
…ship

Replace per-window layout_arena with a shared windows_arena on Viewer.
All window and layout allocations now happen on a single arena that is
reset as a unit during receivedListWindows (free_all) and layoutChanged
(retain_capacity + selective rebuild via Layout.clone).

This addresses Mitchell's review on PR ghostty-org#11213: 'Why not change windows
to be on the arena, possibly layout too so it's not in its own arena
but using the shared arena?'

Changes:
- Add windows_arena field to Viewer, initialized from action_arena's
  child allocator
- Remove layout_arena from Window and Window.deinit()
- receivedListWindows: reset windows_arena, rebuild all windows+layouts
- layoutChanged: reset windows_arena with retain_capacity, clone
  unchanged windows' layouts via new Layout.clone() method
- syncLayouts: remove window.deinit() calls (arena owns memory)
- Add Layout.clone() for deep-copying layout trees across allocators
- Add 3 new tests: clone leaf, clone split tree, shared arena
  preservation across layout changes
daiimus pushed a commit to daiimus/ghostty that referenced this pull request Mar 19, 2026
…ship

Replace per-window layout_arena with a shared windows_arena on Viewer.
All window and layout allocations now happen on a single arena that is
reset as a unit during receivedListWindows (free_all) and layoutChanged
(retain_capacity + selective rebuild via Layout.clone).

This addresses Mitchell's review on PR ghostty-org#11213: 'Why not change windows
to be on the arena, possibly layout too so it's not in its own arena
but using the shared arena?'

Changes:
- Add windows_arena field to Viewer, initialized from action_arena's
  child allocator
- Remove layout_arena from Window and Window.deinit()
- receivedListWindows: reset windows_arena, rebuild all windows+layouts
- layoutChanged: reset windows_arena with retain_capacity, clone
  unchanged windows' layouts via new Layout.clone() method
- syncLayouts: remove window.deinit() calls (arena owns memory)
- Add Layout.clone() for deep-copying layout trees across allocators
- Add 3 new tests: clone leaf, clone split tree, shared arena
  preservation across layout changes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants