From 1eec32413bab4cfa20c5136fcc681be4025bf499 Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Tue, 7 Apr 2026 09:31:10 -0500 Subject: [PATCH] fix UAF: dupe FrameWork.hostname per submit instead of borrowing FrameWork.hostname was a borrowed slice from sub.options.hostname, documented as "stable lifetime". it isn't: slurper.runWorker frees sub.options.hostname after sub.run() returns, but FrameWorks for that subscriber may still be queued in the frame pool. once the allocator reuses that memory, pool workers read garbage when logging chain breaks, host authority decisions, etc. repro: zlay-reconnect cronjob spawns ~1839 hosts in 134s. some subscribers churn within that window. corrupted hostnames appear in logs as DIDs (the freed slot got reused for a DID dup) or with stack-pointer-shaped bytes overlaying the suffix. fix: dupe hostname alongside data when submitting to the pool, free both in processFrame. one extra alloc/free per frame. --- src/frame_worker.zig | 3 ++- src/subscriber.zig | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/frame_worker.zig b/src/frame_worker.zig index ff41796..76f44a1 100644 --- a/src/frame_worker.zig +++ b/src/frame_worker.zig @@ -27,7 +27,7 @@ fn microTimestamp(io: Io) i64 { pub const FrameWork = struct { data: []u8, // raw frame bytes (heap-duped by reader, freed by worker) host_id: u64, - hostname: []const u8, // borrowed from subscriber (stable lifetime) + hostname: []const u8, // owned (heap-duped at submit, freed by worker) allocator: Allocator, io: Io, // shared references (all thread-safe, all outlive the work item) @@ -41,6 +41,7 @@ pub const FrameWork = struct { pub fn processFrame(work: *FrameWork) void { _ = work.bc.stats.pool_queued_bytes.fetchSub(work.data.len, .monotonic); defer work.allocator.free(work.data); + defer work.allocator.free(work.hostname); var arena = std.heap.ArenaAllocator.init(work.allocator); defer arena.deinit(); diff --git a/src/subscriber.zig b/src/subscriber.zig index 51ed362..bb4c1b8 100644 --- a/src/subscriber.zig +++ b/src/subscriber.zig @@ -478,11 +478,19 @@ const FrameHandler = struct { break :blk if (d) |s| std.hash.Wyhash.hash(0, s) else sub.options.host_id; }; const duped = sub.allocator.dupe(u8, data) catch return; + // dupe hostname per-frame: subscriber teardown (slurper.runWorker) + // frees sub.options.hostname after sub.run() returns, but FrameWorks + // can still be queued in the pool. borrowing the slice would be a + // use-after-free (corrupt hostnames in chain-break logs, etc.). + const hostname_dup = sub.allocator.dupe(u8, sub.options.hostname) catch { + sub.allocator.free(duped); + return; + }; const t0 = nanoTimestamp(io); if (pool.submit(did_key, .{ .data = duped, .host_id = sub.options.host_id, - .hostname = sub.options.hostname, + .hostname = hostname_dup, .allocator = sub.allocator, .io = sub.pool_io orelse sub.io, // pool_io (Threaded) for worker-safe ops .bc = sub.bc, @@ -500,6 +508,7 @@ const FrameHandler = struct { } else { // shutdown requested — don't advance cursor so reconnect replays this frame sub.allocator.free(duped); + sub.allocator.free(hostname_dup); } return; } -- 2.51.2