From 6d6c832cd37c1321401eccc7a4186803838724c2 Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Thu, 2 Apr 2026 21:22:27 -0500 Subject: [PATCH] fix pingLoop use-after-free: respect cancellation, check isClosed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pingLoop swallowed error.Canceled from io.sleep(), preventing ping_future.cancel() from stopping the task before client.deinit() freed the stream/TLS buffers. next writeFrame hit freed memory → GPF. two changes: - io.sleep() catch {} → catch return (cancellation-cooperative) - check client.isClosed() before writeFrame (defense-in-depth) also bumps zat to v0.3.0-alpha.11 and websocket.zig to 104608b. Co-Authored-By: Claude Opus 4.6 --- build.zig.zon | 8 ++++---- src/subscriber.zig | 9 ++++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index c257b5d..eee92fb 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,12 +5,12 @@ .minimum_zig_version = "0.16.0", .dependencies = .{ .zat = .{ - .url = "https://tangled.org/zat.dev/zat/archive/v0.3.0-alpha.10.tar.gz", - .hash = "zat-0.3.0-alpha.10-5PuC7nVhBQBinQH3IMZnhsHpL0OJcYhF4p3Wfo7OYAdX", + .url = "https://tangled.org/zat.dev/zat/archive/v0.3.0-alpha.11.tar.gz", + .hash = "zat-0.3.0-alpha.11-5PuC7nVhBQCNUnJEi_YUqQK6V8bbZacA-QN54nUunu4K", }, .websocket = .{ - .url = "https://github.com/zzstoatzz/websocket.zig/archive/4222f98.tar.gz", - .hash = "websocket-0.1.0-ZPISdQTUAwDJt7jFEbYJhdqRBztn8mHIFh-dNK5dOlxL", + .url = "https://github.com/zzstoatzz/websocket.zig/archive/104608b.tar.gz", + .hash = "websocket-0.1.0-ZPISdXjUAwC3rN7rT5NMG8HQJRug1NOboVWeX09SvSGv", }, .pg = .{ .url = "git+https://github.com/zzstoatzz/pg.zig?ref=dev#5ce2355b1d851075523709c7d3068dcdb0224322", diff --git a/src/subscriber.zig b/src/subscriber.zig index 1d753c3..a5d9549 100644 --- a/src/subscriber.zig +++ b/src/subscriber.zig @@ -367,13 +367,16 @@ pub const Subscriber = struct { fn pingLoop(client: *websocket.Client, self: *Subscriber) void { var fail_count: u32 = 0; while (!self.shouldStop()) { - // sleep in 1s increments so we can check shutdown + // sleep in 1s increments so we can check shutdown. + // return on any sleep error — critically, error.Canceled from + // ping_future.cancel() must not be swallowed, otherwise deinit + // frees the client while we're still running. var elapsed: u32 = 0; while (elapsed < ping_interval_sec and !self.shouldStop()) { - self.io.sleep(Io.Duration.fromSeconds(1), .awake) catch {}; + self.io.sleep(Io.Duration.fromSeconds(1), .awake) catch return; elapsed += 1; } - if (self.shouldStop()) return; + if (self.shouldStop() or client.isClosed()) return; client.writeFrame(.ping, &.{}) catch { fail_count += 1; -- 2.51.2