From 978d69257a8aed8ca777916c1094c50b1ca951c5 Mon Sep 17 00:00:00 2001 From: Jeff Chung Date: Fri, 8 May 2026 15:11:57 +0800 Subject: [PATCH] fix(small_buf_map): use *const Self for read methods to avoid use-after-stack-free MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem `SmallBufMap`'s read accessors (`get`, `getKey`, `getValue`, `getKeyValue`, `getKeyIndex`, `count`, `dataCount`, `hasEntry`, `getPutOp`) took `self: Self` — i.e. by value. Each call copied the entire `[buffer_size]u8` onto the callee's stack. Methods returning a slice (`get`, `getKey`, `getValue`, `getKeyValue`) returned a slice into that **stack-local copy**, which becomes a dangling pointer the moment the function returns. This is a use-after-stack-free: the bytes survive until the next call overwrites that stack region, so whether the caller reads correct data or zeros depends on surrounding code, optimization level, and compiler version — undefined behavior, not a deterministic crash. ## Symptom `zig build test` (debug, zig 0.14.1) failed on `enr.test.ENR test vector` and `enr_bench.test.bench`: ``` error: 'enr.test.ENR test vector' failed: src/enr.zig:29:13: in init return Error.BadID; ``` `ENR.decodeInto` calls `kvs.put(...)` for each KV, then `kvs.get("id")`. The bytes at `kvs.buffer[2..4]` were correct (`0x76 0x34` = `"v4"`), but `get` returned a slice into the freed stack frame, which had been clobbered to zero by intervening calls — so `IDScheme.init` rejected it with `Error.BadID`. `EncodedENR.verify` also calls `kvs.get` and has the same latent bug; it just isn't exercised by the current test set. ## Fix Change every read method's receiver from `self: Self` to `self: *const Self`. The slice returned by `get` now points into the caller's live `kvs.buffer`, not a copy. Mutating methods (`put`, `append`, `insert`, `remove`, ...) were already `*Self` and are unchanged. No call sites need updating — Zig auto-coerces `kvs.get(...)` to pass `&kvs` regardless of receiver type. Also avoids a `[buffer_size]u8` memcpy (229 bytes for `ENR.KVs`) on every read. ## Verification Stress-tested by inserting `std.debug.print` / loops between `kvs.get("id")` and the consumer in `ENR.decodeInto` — previously the slice's bytes were clobbered by the prints, now they survive: ``` buffer head: 69 64 76 34 69 70 7f 00 kvs.get("id") = 7634 (2b) ← "v4", correct ``` `zig build test` is green. --- src/small_buf_map.zig | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/small_buf_map.zig b/src/small_buf_map.zig index 032885c..eca2e19 100644 --- a/src/small_buf_map.zig +++ b/src/small_buf_map.zig @@ -21,19 +21,19 @@ pub fn SmallBufMap(comptime buffer_size: u8) type { const Self = @This(); /// Assumes the existence of a key at i - inline fn getKey(self: Self, i: u8) []const u8 { + inline fn getKey(self: *const Self, i: u8) []const u8 { const start = if (i == 0) 0 else self.buffer[buffer_size - (2 * i)]; const end = self.buffer[buffer_size - (2 * i) - 1]; return self.buffer[start..end]; } /// Assumes the existence of a value at i - inline fn getValue(self: Self, i: u8) []const u8 { + inline fn getValue(self: *const Self, i: u8) []const u8 { const start = self.buffer[buffer_size - (2 * i) - 1]; const end = self.buffer[buffer_size - (2 * i) - 2]; return self.buffer[start..end]; } /// Assumes the existence of key+value at i - pub fn getKeyValue(self: Self, i: u8) [2][]const u8 { + pub fn getKeyValue(self: *const Self, i: u8) [2][]const u8 { return [2][]const u8{ self.getKey(i), self.getValue(i) }; } @@ -50,12 +50,12 @@ pub fn SmallBufMap(comptime buffer_size: u8) type { @memcpy(self.buffer[key_end..value_end], value); } - fn hasEntry(self: Self, i: u8) bool { + fn hasEntry(self: *const Self, i: u8) bool { const offset = self.buffer[buffer_size - (2 * i) - 1]; return offset != 0; } - pub fn count(self: Self) u8 { + pub fn count(self: *const Self) u8 { var i: u8 = 0; while (i < 256) : (i += 1) { if (!self.hasEntry(i)) { @@ -65,12 +65,12 @@ pub fn SmallBufMap(comptime buffer_size: u8) type { return 255; } - pub fn dataCount(self: Self) u8 { + pub fn dataCount(self: *const Self) u8 { const c = self.count(); return if (c == 0) 0 else self.buffer[buffer_size - (2 * c)]; } - inline fn getKeyIndex(self: Self, key: []const u8) ?u8 { + inline fn getKeyIndex(self: *const Self, key: []const u8) ?u8 { var i: u8 = 0; while (i < 256) : (i += 1) { if (!self.hasEntry(i)) { @@ -84,14 +84,14 @@ pub fn SmallBufMap(comptime buffer_size: u8) type { return null; } - pub fn get(self: Self, key: []const u8) ?[]const u8 { + pub fn get(self: *const Self, key: []const u8) ?[]const u8 { const i = self.getKeyIndex(key) orelse return null; return self.getValue(i); } const PutOpType = enum { replace, insert, append }; const PutOp = struct { op: PutOpType, i: u8 }; - fn getPutOp(self: Self, key: []const u8) PutOp { + fn getPutOp(self: *const Self, key: []const u8) PutOp { var i: u8 = 0; while (i < 256) : (i += 1) { if (!self.hasEntry(i)) {