diff --git a/CHANGELOG.md b/CHANGELOG.md index 58773ea..1310bc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/build.zig.zon b/build.zig.zon index 082f244..9e7ec81 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -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 diff --git a/nix/package.nix b/nix/package.nix index d3da5d5..b925e05 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -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 ]; diff --git a/scripts/verify-nix-deps.sh b/scripts/verify-nix-deps.sh index aaba014..a6fe9b3 100755 --- a/scripts/verify-nix-deps.sh +++ b/scripts/verify-nix-deps.sh @@ -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 diff --git a/src/main.zig b/src/main.zig index dc4e4da..ea38186 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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) }); diff --git a/src/nip11.zig b/src/nip11.zig index 4d1fe8f..a55a0e1 100644 --- a/src/nip11.zig +++ b/src/nip11.zig @@ -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});