Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.6.1] - 2026-08-12

Two ways a remote peer could crash or wedge the relay, a third that needs the kernel to refuse a socket registration, and a packaging build that had been failing since 0.5.12.

### Changed

- The vendored HTTP server is re-pinned to upstream, which has now adopted the connection timeout fix wisp has carried since 0.5.15. No behavior changes for anyone running 0.6.0: this retires a local patch and picks up an upstream atomics ordering fix that the WebSocket slot reclamation relies on (#180)

### Fixed

- A connection handed over in the same batch as another could be dropped from
every tracking list, leaking its slot and file descriptor and leaving the
worker spinning on it. The epoll worker snapshots its handover list by taking
the head and clearing the list, but the snapshot entries keep their links, so
releasing one through the general path rewrote the live list's head and tail
from stale pointers. Anything handed over before the next drain was then
unreachable by any timeout sweep, while its still-registered socket was
re-reported on every loop iteration. Reachable remotely without
authentication by interleaving WebSocket upgrades with requests that end in a
close. Measured on the unfixed build: 42 leaked descriptors and a core pinned
at 99.6% after the load stopped (#181)
- A connection handed over in the same batch as another could be dropped from every tracking list, leaking its slot and file descriptor and leaving the worker spinning on it. Anything handed over before the next drain became unreachable by any timeout sweep, while its still-registered socket was re-reported on every loop iteration. Reachable remotely without authentication by interleaving WebSocket upgrades with requests that end in a close. Measured on the unfixed build: 42 leaked descriptors and a core pinned at 99.6% after the load stopped. Covered by a new integration test that fails without the fix (#181)
- A connection could be recycled while another thread still held its lock, so that thread then wrote into a record it no longer owned. On a build with safety checks this aborts the process; on the release build there is no such check, and a later connection can inherit a lock that is already held, which leaves two threads inside the same connection's list bookkeeping at once. Reachable remotely without authentication by driving connection churn. Affected every path that publishes a connection to the event loop while still holding its lock (#183)
- A failed connection setup ran its whole cleanup twice, destroying the connection record and closing the socket a second time each. A double destroy links the record to itself on the free list, so later connections are handed a record that is still in use. Not directly reachable by a remote peer: it needs the kernel to refuse the socket registration, either out of memory or against the per-user watch limit (#184)
- The Nix package build had been failing since 0.5.12, when the WebSocket dependency pin moved and the offline dependency set was not updated with it, so `nix build` stopped with "package not found". The packaged version string had also been reporting 0.5.10 since 0.5.11. Both are fixed, and CI now builds the Nix package and starts the resulting binary, so neither can break unnoticed again (#182)

## [0.6.0] - 2026-08-11

Expand Down Expand Up @@ -250,7 +251,8 @@ rolling out.
- Import/export to JSONL format
- Configuration via TOML file or environment variables

[Unreleased]: https://github.com/privkeyio/wisp/compare/v0.6.0...HEAD
[Unreleased]: https://github.com/privkeyio/wisp/compare/v0.6.1...HEAD
[0.6.1]: https://github.com/privkeyio/wisp/compare/v0.6.0...v0.6.1
[0.6.0]: https://github.com/privkeyio/wisp/compare/v0.5.15...v0.6.0
[0.5.15]: https://github.com/privkeyio/wisp/compare/v0.5.14...v0.5.15
[0.5.14]: https://github.com/privkeyio/wisp/compare/v0.5.13...v0.5.14
Expand Down
2 changes: 1 addition & 1 deletion build.zig.zon
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.{
.name = .wisp,
.version = "0.6.0",
.version = "0.6.1",
.fingerprint = 0xc4bdec9fe6401a8d,
.dependencies = .{
// websocket.zig: client for spider outbound connections and the epoll
Expand Down
2 changes: 1 addition & 1 deletion nix/package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ let
in
stdenv.mkDerivation (finalAttrs: {
pname = "wisp";
version = "0.6.0"; # keep in sync with build.zig.zon .version
version = "0.6.1"; # keep in sync with build.zig.zon .version
inherit src;

nativeBuildInputs = [ zig_0_16 ];
Expand Down
30 changes: 23 additions & 7 deletions scripts/verify-nix-deps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,30 @@ while read -r hash want; do
fi
done <<< "$required_pairs"

zon_version="$(grep -oP '(?<=\.version = ")[^"]+' build.zig.zon | head -1)"
nix_version="$(grep -oP '(?<=^ version = ")[^"]+' nix/package.nix | head -1)"
if [ -z "$zon_version" ] || [ -z "$nix_version" ]; then
echo "FAIL - could not read a version from build.zig.zon ('$zon_version') or nix/package.nix ('$nix_version')"
fail=1
elif [ "$zon_version" != "$nix_version" ]; then
echo "FAIL - version mismatch: build.zig.zon says '$zon_version', nix/package.nix says '$nix_version'"
# build.zig.zon is the source of truth. Three other files carry the version by
# hand and none of them is generated, so each can drift silently: nix/package.nix
# did exactly that, sitting at 0.5.10 across six releases. src/nip11.zig is the
# one users see, since it is what the relay reports over the wire.
zon_version="$(grep -oP '(?<=\.version = ")[^"]+' build.zig.zon | head -1 || true)"
if [ -z "$zon_version" ]; then
echo "FAIL - could not read .version from build.zig.zon"
fail=1
else
check_version() { # file description extracted
if [ -z "$3" ]; then
echo "FAIL - could not read the version from $1 ($2)"
fail=1
elif [ "$3" != "$zon_version" ]; then
echo "FAIL - version mismatch: build.zig.zon says '$zon_version', $1 says '$3' ($2)"
fail=1
fi
}
check_version nix/package.nix "nix package version" \
"$(grep -oP '(?<=^ version = ")[^"]+' nix/package.nix | head -1)"
check_version src/main.zig "startup log line" \
"$(grep -oP '(?<=Wisp v)[0-9]+\.[0-9]+\.[0-9]+(?= starting)' src/main.zig | head -1)"
check_version src/nip11.zig "NIP-11 relay information document" \
"$(grep -oP '(?<=\\"version\\":\\")[^\\]+' src/nip11.zig | head -1)"
fi

[ "$fail" -eq 0 ] || exit 1
Expand Down
2 changes: 1 addition & 1 deletion src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ pub fn main(init: std.process.Init) !void {
return error.InvalidSyncMode;
};

std.log.info("Wisp v0.6.0 starting", .{});
std.log.info("Wisp v0.6.1 starting", .{});
std.log.info("Listening on {s}:{d}", .{ config.host, config.port });
std.log.info("Storage: {s} (sync={s})", .{ config.storage_path, @tagName(sync_mode) });

Expand Down
2 changes: 1 addition & 1 deletion src/nip11.zig
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ pub fn write(config: *const Config, nip86: *const Nip86Handler, w: anytype) !voi

try w.writeAll(",\"supported_nips\":[1,2,9,11,13,16,33,40,42,45,50,51,65,70,77,86]");
try w.writeAll(",\"software\":\"https://github.com/privkeyio/wisp\"");
try w.writeAll(",\"version\":\"0.6.0\"");
try w.writeAll(",\"version\":\"0.6.1\"");

try w.writeAll(",\"limitation\":{");
try w.print("\"max_message_length\":{d}", .{config.max_message_size});
Expand Down
Loading