From 4d8b5ba3fd122818b597a5a222ad6e9cbdf00e23 Mon Sep 17 00:00:00 2001 From: sepehr-safari Date: Wed, 23 Sep 2026 12:04:59 +0300 Subject: [PATCH] fix: free the TLS state when a wss dial fails after the handshake The TLS state and its two 64 KiB buffers were freed only by errdefers inside the block that allocated them. Once that block had exited, a failed websocket upgrade, or a dial cancelled while waiting for it, left all three behind. An errdefer at the level of the dial now frees them on every later failure. Closes #110. --- CHANGELOG.md | 4 ++++ src/relay.zig | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fabc1c..cfe9af9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- A `wss://` dial that got through the TLS handshake and then failed frees its TLS state. The state and its two 64 KiB buffers were freed only by errdefers inside the block that created them, which had already exited, so a relay that answered the websocket upgrade with anything but 101, or a dial cancelled while waiting for that answer, leaked all three. A client that retries such a relay leaked on every retry. + ## [0.14.3] - 2026-09-23 ### Added diff --git a/src/relay.zig b/src/relay.zig index 5264ab7..a9d541d 100644 --- a/src/relay.zig +++ b/src/relay.zig @@ -770,6 +770,14 @@ pub fn dial(gpa: std.mem.Allocator, io: std.Io, url: []const u8) !*Relay { // No TLS layer, so `reader` already IS the transport reader and the // loop's own `bufferedLen` check covers it. }; + // The TLS state outlives the block that made it, so its errdefers are gone + // by now. A wss dial that got through TLS and then failed the websocket + // upgrade, or was cancelled waiting for it, leaked all of it. + errdefer if (transport.tls_state) |ts| { + gpa.free(ts.read_buffer); + gpa.free(ts.write_buffer); + gpa.destroy(ts); + }; const relay = try gpa.create(Relay); errdefer gpa.destroy(relay);