Skip to content

Commit 9a8e499

Browse files
authored
Merge pull request #2 from midasdf/fix/adversarial-review-server-safety
Fix close_tab corruption, active_panes routing, and blocking writes
2 parents 3196c79 + 4b03fdf commit 9a8e499

4 files changed

Lines changed: 91 additions & 10 deletions

File tree

build.zig.zon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
.{
22
.name = .zplit,
3-
.version = "0.1.0",
3+
.version = "0.1.1",
44
.fingerprint = 0xbce5f8607c44d1ad,
55
.minimum_zig_version = "0.15.0",
66
.paths = .{ "build.zig", "build.zig.zon", "src" },

src/main.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ pub fn main() !void {
477477
return;
478478
}
479479
if (std.mem.eql(u8, subcmd, "--version") or std.mem.eql(u8, subcmd, "-v")) {
480-
std.debug.print("zplit v0.1.0\n", .{});
480+
std.debug.print("zplit v0.1.1\n", .{});
481481
return;
482482
}
483483

src/pane.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ fn replaceNodeInParent(
405405
}
406406
}
407407

408-
fn firstLeafId(node: *const LayoutNode) PaneId {
408+
pub fn firstLeafId(node: *const LayoutNode) PaneId {
409409
return switch (node.*) {
410410
.leaf => |l| l.id,
411411
.split => |s| firstLeafId(s.first),

src/server.zig

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ pub const ClientState = struct {
134134
mode: mode_mod.Mode = .normal,
135135
recv_buf: [RECV_BUF_SIZE]u8 = undefined,
136136
recv_len: usize = 0,
137+
write_failed: bool = false,
137138
allocator: std.mem.Allocator,
138139

139140
pub fn init(allocator: std.mem.Allocator, id: u16, fd: posix.fd_t) !*ClientState {
@@ -347,7 +348,7 @@ pub const Server = struct {
347348

348349
if (tag == TAG_LISTEN) {
349350
// Accept a new client connection
350-
const new_fd = posix.accept(self.listen_fd, null, null, posix.SOCK.CLOEXEC) catch continue;
351+
const new_fd = posix.accept(self.listen_fd, null, null, posix.SOCK.CLOEXEC | posix.SOCK.NONBLOCK) catch continue;
351352
if (self.clients.count() >= MAX_CLIENTS) {
352353
posix.close(new_fd);
353354
continue;
@@ -448,6 +449,18 @@ pub const Server = struct {
448449
cs.rows = hp.rows;
449450
cs.screen.resize(hp.cols, hp.rows) catch {};
450451
cs.screen.invalidate();
452+
// Initialize active_panes for all existing tabs and pick a valid active_tab
453+
var first_open_tab: ?u8 = null;
454+
for (self.tab_manager.tabs, 0..) |maybe_tab, i| {
455+
if (maybe_tab) |t| {
456+
const tab_idx: u8 = @intCast(i);
457+
cs.active_panes[tab_idx] = pane_mod.firstLeafId(t.pane_tree.root);
458+
if (first_open_tab == null) first_open_tab = tab_idx;
459+
}
460+
}
461+
if (first_open_tab) |tab_idx| {
462+
cs.active_tab = tab_idx;
463+
}
451464
if (self.active_client == null) {
452465
self.active_client = client_id;
453466
}
@@ -557,15 +570,23 @@ pub const Server = struct {
557570
const new_tab_idx = self.tab_manager.createTab(new_pane_id) catch return;
558571
self.spawnPaneState(new_pane_id) catch return;
559572
cs.active_tab = new_tab_idx;
560-
cs.active_panes[new_tab_idx] = new_pane_id;
573+
// Propagate new tab's focused pane to ALL clients
574+
var it = self.clients.valueIterator();
575+
while (it.next()) |other_cs| {
576+
other_cs.*.active_panes[new_tab_idx] = new_pane_id;
577+
}
561578
self.invalidateAllClients();
562579
self.composeAll();
563580
},
564581
.close_tab => {
565-
self.destroyAllPanesInTab(cs.active_tab);
566-
const nearest = self.tab_manager.closeTab(cs.active_tab) orelse return;
567-
// Update ALL clients viewing the closed tab
582+
// Reject closing the last tab before destroying any state
583+
if (self.tab_manager.tabCount() <= 1) return;
568584
const closed_tab = cs.active_tab;
585+
// Clean up per-pane client state (scroll_offsets) before destroying
586+
self.cleanupClientStateForTab(closed_tab);
587+
self.destroyAllPanesInTab(closed_tab);
588+
const nearest = self.tab_manager.closeTab(closed_tab) orelse return;
589+
// Update ALL clients viewing the closed tab
569590
var it = self.clients.valueIterator();
570591
while (it.next()) |other_cs| {
571592
if (other_cs.*.active_tab == closed_tab) {
@@ -822,6 +843,8 @@ pub const Server = struct {
822843
while (it.next()) |cs| {
823844
self.composeForClient(cs.*);
824845
}
846+
// Disconnect clients that failed to write (corrupted protocol stream)
847+
self.sweepFailedClients();
825848
}
826849

827850
// ── resizePtysForClient ──────────────────────────────────────────────
@@ -989,6 +1012,11 @@ pub const Server = struct {
9891012

9901013
// Swap buffers
9911014
cs.screen.swapBuffers();
1015+
1016+
// Disconnect clients that failed to write during this render
1017+
if (cs.write_failed) {
1018+
self.sweepFailedClients();
1019+
}
9921020
}
9931021

9941022
// ── sendDirtyRegionsTo ───────────────────────────────────────────────
@@ -1185,8 +1213,17 @@ pub const Server = struct {
11851213
const n = try protocol.encodeFrame(&buf, msg_type, payload);
11861214
var sent: usize = 0;
11871215
while (sent < n) {
1188-
const w = try posix.write(cs.fd, buf[sent..n]);
1189-
if (w == 0) return error.BrokenPipe;
1216+
const w = posix.write(cs.fd, buf[sent..n]) catch |err| {
1217+
// WouldBlock before any data sent = transient backpressure, skip frame
1218+
// WouldBlock after partial send = protocol stream corrupted, must disconnect
1219+
if (err == error.WouldBlock and sent == 0) return err;
1220+
cs.write_failed = true;
1221+
return err;
1222+
};
1223+
if (w == 0) {
1224+
cs.write_failed = true;
1225+
return error.BrokenPipe;
1226+
}
11901227
sent += w;
11911228
}
11921229
}
@@ -1213,6 +1250,50 @@ pub const Server = struct {
12131250
}
12141251
}
12151252

1253+
// ── sweepFailedClients ─────────────────────────────────────────────
1254+
1255+
/// Disconnect clients whose write_failed flag is set (protocol stream corrupted).
1256+
/// Safe to call after iteration since it collects IDs first.
1257+
fn sweepFailedClients(self: *Server) void {
1258+
var to_remove: [MAX_CLIENTS]u16 = undefined;
1259+
var count: u8 = 0;
1260+
var it = self.clients.iterator();
1261+
while (it.next()) |entry| {
1262+
if (entry.value_ptr.*.write_failed) {
1263+
if (count < MAX_CLIENTS) {
1264+
to_remove[count] = entry.key_ptr.*;
1265+
count += 1;
1266+
}
1267+
}
1268+
}
1269+
for (to_remove[0..count]) |cid| {
1270+
self.disconnectClient(cid);
1271+
}
1272+
}
1273+
1274+
// ── cleanupClientStateForTab ────────────────────────────────────────
1275+
1276+
/// Remove scroll_offsets entries for all panes in the given tab from every client.
1277+
fn cleanupClientStateForTab(self: *Server, tab_idx: u8) void {
1278+
const tab = self.tab_manager.activeTab(tab_idx);
1279+
var cit = self.clients.valueIterator();
1280+
while (cit.next()) |client_cs| {
1281+
self.removeScrollOffsetsInNode(client_cs.*, tab.pane_tree.root);
1282+
}
1283+
}
1284+
1285+
fn removeScrollOffsetsInNode(self: *Server, cs_ptr: *ClientState, node: *pane_mod.LayoutNode) void {
1286+
switch (node.*) {
1287+
.leaf => |l| {
1288+
_ = cs_ptr.scroll_offsets.remove(l.id);
1289+
},
1290+
.split => |s| {
1291+
self.removeScrollOffsetsInNode(cs_ptr, s.first);
1292+
self.removeScrollOffsetsInNode(cs_ptr, s.second);
1293+
},
1294+
}
1295+
}
1296+
12161297
// ── destroyAllPanesInTab ─────────────────────────────────────────────
12171298

12181299
fn destroyAllPanesInTab(self: *Server, tab_idx: u8) void {

0 commit comments

Comments
 (0)