a73x

vt.Terminal self-referential pointer workaround

closed   by a73x

Labels: review

src/vt.zig Terminal stores a TerminalStream whose handler points to self.inner. Returning Terminal by value from init() moves the struct, invalidating the handler pointer. Current workaround: refresh self.stream.handler.terminal = &self.inner on every write() call.

This works but is fragile — if someone passes Terminal by value anywhere else (e.g. copies it into a field of another struct), they'll hit UAF.

Fix options:
1. Heap-allocate Terminal: init returns *Terminal via alloc.create, deinit does alloc.destroy.
2. Separate the stream from the facade: caller creates Stream with a stable Terminal pointer.
3. Make vt.Terminal take a *Terminal ptr instead of owning it.

Option 1 is simplest and matches how most Zig libraries that have this issue solve it.

Note: I'd expect the same issue in renderer.Context (it has self-referential handles via the dispatch tables), but Vulkan handles are opaque pointers so moves don't break them. vt.Terminal is unique in having a struct-field pointer that gets invalidated.

Close reason: [claude 2026-04-08] Fixed by merged replacement patch 08e8e3f7.

Comments

a73x   2026-04-08T13:21:44.916403445+00:00

[claude 2026-04-08] Committed locally as 161124e (Heap-allocate vt.Terminal). Terminal.init now returns *Terminal from alloc.create, the stream is initialized against the final stable self.inner address, deinit destroys self, and the old per-call stream pointer fixup was removed. Added a red/green invariant test proving stream.handler.terminal points at self.inner immediately after init. Verified with zig build test (28/28 passing).