lib/stabilizer/src/heap.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 
  3 const config = @import("config.zig");
  4 const Marsaglia = @import("rng.zig").Marsaglia;
  5 
  6 const Allocator = std.mem.Allocator;
  7 const Alignment = std.mem.Alignment;
  8 const HeapConfig = config.HeapConfig;
  9 
 10 const allocation_magic: usize = 0x51ab_11e5_7ab1_1e5;
 11 
 12 const AllocationState = enum {
 13     inactive,
 14     active,
 15 };
 16 
 17 const Header = extern struct {
 18     magic: usize,
 19     base_addr: usize,
 20     base_len: usize,
 21     class_size: usize,
 22     requested_len: usize,
 23     alignment: usize,
 24     allocated: bool,
 25     tracked: bool,
 26 };
 27 
 28 const Slot = struct {
 29     memory: []u8,
 30 };
 31 
 32 const Pool = struct {
 33     class_size: usize,
 34     alignment: Alignment,
 35     pointer_validation: bool,
 36     slots: []Slot,
 37 
 38     fn init(
 39         heap: *ShuffleAllocator,
 40         class_size: usize,
 41         alignment: Alignment,
 42         pointer_validation: bool,
 43     ) !Pool {
 44         const count = heap.normalizedSlots();
 45         const slots = try heap.backing.alloc(Slot, count);
 46         var filled: usize = 0;
 47         errdefer {
 48             for (slots[0..filled]) |slot| heap.destroySlot(slot);
 49             heap.backing.free(slots);
 50         }
 51 
 52         while (filled < count) : (filled += 1) {
 53             slots[filled] = .{ .memory = try heap.createUserMemory(
 54                 class_size,
 55                 alignment,
 56                 class_size,
 57                 pointer_validation,
 58                 .inactive,
 59             ) };
 60         }
 61         fisherYatesSlots(slots, &heap.rng);
 62         return .{
 63             .class_size = class_size,
 64             .alignment = alignment,
 65             .pointer_validation = pointer_validation,
 66             .slots = slots,
 67         };
 68     }
 69 
 70     fn deinit(self: *Pool, heap: *ShuffleAllocator) void {
 71         for (self.slots) |slot| heap.destroySlot(slot);
 72         heap.backing.free(self.slots);
 73         self.* = undefined;
 74     }
 75 };
 76 
 77 pub const ShuffleAllocator = struct {
 78     backing: Allocator,
 79     config: HeapConfig,
 80     rng: Marsaglia,
 81     pools: std.ArrayListUnmanaged(Pool) = .empty,
 82     known: std.AutoHashMapUnmanaged(usize, AllocationState) = .empty,
 83 
 84     pub fn init(backing: Allocator, heap_config: HeapConfig, seed: u64) ShuffleAllocator {
 85         return .{
 86             .backing = backing,
 87             .config = normalizeHeapConfig(heap_config),
 88             .rng = Marsaglia.init(seed),
 89         };
 90     }
 91 
 92     pub fn deinit(self: *ShuffleAllocator) void {
 93         for (self.pools.items) |*pool| pool.deinit(self);
 94         self.pools.deinit(self.backing);
 95         self.known.deinit(self.backing);
 96         self.* = undefined;
 97     }
 98 
 99     pub fn allocator(self: *ShuffleAllocator) Allocator {
100         return .{ .ptr = self, .vtable = &vtable };
101     }
102 
103     pub fn malloc(self: *ShuffleAllocator, len: usize) ![*]u8 {
104         return try self.alloc(len, Alignment.fromByteUnits(1), true, @returnAddress());
105     }
106 
107     pub fn calloc(self: *ShuffleAllocator, count: usize, len: usize) ![*]u8 {
108         const total = std.math.mul(usize, count, len) catch return error.OutOfMemory;
109         const memory = try self.malloc(total);
110         @memset(memory[0..total], 0);
111         return memory;
112     }
113 
114     pub fn realloc(self: *ShuffleAllocator, ptr: ?[*]u8, new_len: usize) !?[*]u8 {
115         const memory = ptr orelse return try self.malloc(new_len);
116         const state = self.known.get(@intFromPtr(memory)) orelse return error.InvalidAllocation;
117         if (state == .inactive) return error.InvalidAllocation;
118         const header = checkedHeader(memory) orelse return error.InvalidAllocation;
119         if (!header.allocated) return error.InvalidAllocation;
120         const alignment = Alignment.fromByteUnits(header.alignment);
121         const old_len = header.requested_len;
122         const old_memory = memory[0..old_len];
123 
124         if (new_len == 0) {
125             self.free(old_memory, alignment, @returnAddress());
126             return null;
127         }
128 
129         if (self.resize(old_memory, alignment, new_len, @returnAddress())) return memory;
130 
131         const new_memory = try self.alloc(new_len, alignment, true, @returnAddress());
132         @memcpy(new_memory[0..@min(old_len, new_len)], old_memory[0..@min(old_len, new_len)]);
133         self.free(old_memory, alignment, @returnAddress());
134         return new_memory;
135     }
136 
137     pub fn freePointer(self: *ShuffleAllocator, ptr: ?[*]u8) void {
138         _ = self.freePointerIfOwned(ptr);
139     }
140 
141     pub fn freePointerIfOwned(self: *ShuffleAllocator, ptr: ?[*]u8) bool {
142         const memory = ptr orelse return true;
143         const state = self.known.get(@intFromPtr(memory)) orelse return false;
144         if (state == .inactive) reportDoubleFreeError();
145         const header = checkedHeader(memory) orelse return false;
146         self.free(memory[0..header.requested_len], Alignment.fromByteUnits(header.alignment), @returnAddress());
147         return true;
148     }
149 
150     pub fn ownsPointer(self: *const ShuffleAllocator, ptr: ?[*]u8) bool {
151         const memory = ptr orelse return false;
152         return self.known.get(@intFromPtr(memory)) != null;
153     }
154 
155     pub fn requestedLength(ptr: [*]u8) ?usize {
156         const header = checkedHeader(ptr) orelse return null;
157         if (!header.allocated) return null;
158         return header.requested_len;
159     }
160 
161     fn normalizedSlots(self: *const ShuffleAllocator) usize {
162         return if (self.config.shuffle_slots == 0) 1 else self.config.shuffle_slots;
163     }
164 
165     fn alloc(
166         self: *ShuffleAllocator,
167         len: usize,
168         alignment: Alignment,
169         pointer_validation: bool,
170         ret_addr: usize,
171     ) ![*]u8 {
172         if (!self.config.enabled or len > self.config.max_shuffled_size) {
173             const memory = try self.createUserMemory(
174                 len,
175                 alignment,
176                 0,
177                 pointer_validation,
178                 .active,
179             );
180             _ = ret_addr;
181             return memory.ptr;
182         }
183 
184         const class_size = try sizeClass(len, self.config.min_class_size);
185         const pool = try self.getPool(class_size, alignment, pointer_validation);
186         const index = self.rng.bounded(pool.slots.len);
187         const replacement = Slot{ .memory = try self.createUserMemory(
188             class_size,
189             alignment,
190             class_size,
191             pointer_validation,
192             .inactive,
193         ) };
194         const selected = pool.slots[index];
195         const header = headerFromUser(selected.memory.ptr);
196         if (header.allocated) reportDoubleFreeError();
197         if (pointer_validation) self.activatePointer(selected.memory.ptr);
198         pool.slots[index] = replacement;
199 
200         header.requested_len = len;
201         header.allocated = true;
202         return selected.memory.ptr;
203     }
204 
205     fn resize(self: *ShuffleAllocator, memory: []u8, alignment: Alignment, new_len: usize, ret_addr: usize) bool {
206         _ = self;
207         _ = ret_addr;
208         const header = checkedHeader(memory.ptr) orelse return false;
209         if (!header.allocated) return false;
210         if (header.alignment != alignment.toByteUnits()) return false;
211         const current_class = header.class_size;
212         if (current_class == 0) {
213             if (new_len <= header.base_len - headerOffset(memory.ptr, header)) {
214                 header.requested_len = new_len;
215                 return true;
216             }
217             return false;
218         }
219         if (new_len <= current_class) {
220             header.requested_len = new_len;
221             return true;
222         }
223         return false;
224     }
225 
226     fn remap(self: *ShuffleAllocator, memory: []u8, alignment: Alignment, new_len: usize, ret_addr: usize) ?[*]u8 {
227         _ = self;
228         _ = memory;
229         _ = alignment;
230         _ = new_len;
231         _ = ret_addr;
232         return null;
233     }
234 
235     fn free(self: *ShuffleAllocator, memory: []u8, alignment: Alignment, ret_addr: usize) void {
236         const header = checkedHeader(memory.ptr) orelse return;
237         if (header.alignment != alignment.toByteUnits()) return;
238         if (!header.allocated) reportDoubleFreeError();
239         header.allocated = false;
240         if (header.tracked) {
241             const state = self.known.getPtr(@intFromPtr(memory.ptr)) orelse unreachable;
242             std.debug.assert(state.* == .active);
243             state.* = .inactive;
244         }
245         if (header.class_size == 0 or !self.config.enabled) {
246             self.destroyUserMemory(memory.ptr, ret_addr);
247             return;
248         }
249 
250         const pool = self.findPool(header.class_size, alignment, header.tracked) orelse {
251             self.destroyUserMemory(memory.ptr, ret_addr);
252             return;
253         };
254         const index = self.rng.bounded(pool.slots.len);
255         const evicted = pool.slots[index];
256         pool.slots[index] = .{ .memory = memory.ptr[0..header.class_size] };
257         self.destroySlot(evicted);
258     }
259 
260     fn getPool(
261         self: *ShuffleAllocator,
262         class_size: usize,
263         alignment: Alignment,
264         pointer_validation: bool,
265     ) !*Pool {
266         if (self.findPool(class_size, alignment, pointer_validation)) |pool| return pool;
267         var pool = try Pool.init(self, class_size, alignment, pointer_validation);
268         errdefer pool.deinit(self);
269         try self.pools.append(self.backing, pool);
270         return &self.pools.items[self.pools.items.len - 1];
271     }
272 
273     fn findPool(
274         self: *ShuffleAllocator,
275         class_size: usize,
276         alignment: Alignment,
277         pointer_validation: bool,
278     ) ?*Pool {
279         for (self.pools.items) |*pool| {
280             if (pool.class_size == class_size and
281                 pool.alignment == alignment and
282                 pool.pointer_validation == pointer_validation)
283             {
284                 return pool;
285             }
286         }
287         return null;
288     }
289 
290     fn createUserMemory(
291         self: *ShuffleAllocator,
292         len: usize,
293         alignment: Alignment,
294         class_size: usize,
295         pointer_validation: bool,
296         state: AllocationState,
297     ) ![]u8 {
298         const user_alignment = Alignment.max(alignment, .of(Header));
299         const alignment_bytes = user_alignment.toByteUnits();
300         const overhead = std.math.add(usize, @sizeOf(Header), alignment_bytes - 1) catch
301             return error.OutOfMemory;
302         const base_len = std.math.add(usize, len, overhead) catch return error.OutOfMemory;
303         const base = try self.backing.alloc(u8, base_len);
304         const user_addr = user_alignment.forward(@intFromPtr(base.ptr) + @sizeOf(Header));
305         const header: *Header = @ptrFromInt(user_addr - @sizeOf(Header));
306         header.* = .{
307             .magic = allocation_magic,
308             .base_addr = @intFromPtr(base.ptr),
309             .base_len = base_len,
310             .class_size = class_size,
311             .requested_len = len,
312             .alignment = alignment.toByteUnits(),
313             .allocated = class_size == 0,
314             .tracked = false,
315         };
316         errdefer self.backing.free(base);
317         if (pointer_validation) {
318             try self.known.put(self.backing, user_addr, state);
319             header.tracked = true;
320         }
321         return @as([*]u8, @ptrFromInt(user_addr))[0..len];
322     }
323 
324     fn activatePointer(self: *ShuffleAllocator, ptr: [*]u8) void {
325         const header = headerFromUser(ptr);
326         std.debug.assert(header.tracked);
327         const state = self.known.getPtr(@intFromPtr(ptr)) orelse unreachable;
328         std.debug.assert(state.* == .inactive);
329         state.* = .active;
330     }
331 
332     fn destroySlot(self: *ShuffleAllocator, slot: Slot) void {
333         self.destroyUserMemory(slot.memory.ptr, @returnAddress());
334     }
335 
336     fn destroyUserMemory(self: *ShuffleAllocator, ptr: [*]u8, ret_addr: usize) void {
337         _ = ret_addr;
338         const header = checkedHeader(ptr) orelse return;
339         const base: [*]u8 = @ptrFromInt(header.base_addr);
340         if (header.tracked) std.debug.assert(self.known.remove(@intFromPtr(ptr)));
341         self.backing.free(base[0..header.base_len]);
342     }
343 
344     const vtable: Allocator.VTable = .{
345         .alloc = rawAlloc,
346         .resize = rawResize,
347         .remap = rawRemap,
348         .free = rawFree,
349     };
350 
351     fn rawAlloc(ctx: *anyopaque, len: usize, alignment: Alignment, ret_addr: usize) ?[*]u8 {
352         const self: *ShuffleAllocator = @ptrCast(@alignCast(ctx));
353         return self.alloc(len, alignment, self.config.pointer_validation, ret_addr) catch null;
354     }
355 
356     fn rawResize(ctx: *anyopaque, memory: []u8, alignment: Alignment, new_len: usize, ret_addr: usize) bool {
357         const self: *ShuffleAllocator = @ptrCast(@alignCast(ctx));
358         return self.resize(memory, alignment, new_len, ret_addr);
359     }
360 
361     fn rawRemap(ctx: *anyopaque, memory: []u8, alignment: Alignment, new_len: usize, ret_addr: usize) ?[*]u8 {
362         const self: *ShuffleAllocator = @ptrCast(@alignCast(ctx));
363         return self.remap(memory, alignment, new_len, ret_addr);
364     }
365 
366     fn rawFree(ctx: *anyopaque, memory: []u8, alignment: Alignment, ret_addr: usize) void {
367         const self: *ShuffleAllocator = @ptrCast(@alignCast(ctx));
368         self.free(memory, alignment, ret_addr);
369     }
370 };
371 
372 fn normalizeHeapConfig(heap_config: HeapConfig) HeapConfig {
373     var out = heap_config;
374     if (out.shuffle_slots == 0) out.shuffle_slots = 1;
375     if (out.min_class_size == 0) out.min_class_size = 1;
376     out.min_class_size = std.math.ceilPowerOfTwoAssert(usize, out.min_class_size);
377     return out;
378 }
379 
380 fn headerFromUser(ptr: [*]u8) *Header {
381     return @ptrFromInt(@intFromPtr(ptr) - @sizeOf(Header));
382 }
383 
384 fn checkedHeader(ptr: [*]u8) ?*Header {
385     const header = headerFromUser(ptr);
386     if (header.magic != allocation_magic) return null;
387     return header;
388 }
389 
390 fn headerOffset(ptr: [*]u8, header: *const Header) usize {
391     return @intFromPtr(ptr) - header.base_addr;
392 }
393 
394 pub fn sizeClass(len: usize, min_class_size: usize) !usize {
395     const minimum = if (min_class_size == 0) 1 else std.math.ceilPowerOfTwoAssert(usize, min_class_size);
396     return try std.math.ceilPowerOfTwo(usize, @max(len, minimum));
397 }
398 
399 fn fisherYatesSlots(slots: []Slot, rng: *Marsaglia) void {
400     if (slots.len <= 1) return;
401     var index = slots.len - 1;
402     while (index > 0) : (index -= 1) {
403         const swap_index = rng.bounded(index + 1);
404         std.mem.swap(Slot, &slots[index], &slots[swap_index]);
405     }
406 }
407 
408 fn reportDoubleFreeError() noreturn {
409     @panic("Double free error");
410 }
411 
412 fn exerciseTrackedPoolAllocationFailures(allocator: Allocator) !void {
413     var heap = ShuffleAllocator.init(allocator, .{
414         .shuffle_slots = 4,
415         .max_shuffled_size = 1024,
416     }, 1234);
417     defer heap.deinit();
418 
419     const shuffled = heap.allocator();
420     const memory = try shuffled.alloc(u8, 24);
421     defer shuffled.free(memory);
422     @memset(memory, 0xa5);
423 }
424 
425 test "shuffle allocator exposes upstream c heap operations" {
426     var heap = ShuffleAllocator.init(std.testing.allocator, .{
427         .shuffle_slots = 4,
428         .max_shuffled_size = 1024,
429     }, 1234);
430     defer heap.deinit();
431 
432     var ptr: ?[*]u8 = try heap.calloc(4, 8);
433     defer heap.freePointer(ptr);
434 
435     try std.testing.expectEqual(@as(usize, 32), ShuffleAllocator.requestedLength(ptr.?).?);
436     for (ptr.?[0..32]) |byte| try std.testing.expectEqual(@as(u8, 0), byte);
437 
438     ptr.?[0] = 0xaa;
439     ptr = (try heap.realloc(ptr, 128)).?;
440     try std.testing.expectEqual(@as(usize, 128), ShuffleAllocator.requestedLength(ptr.?).?);
441     try std.testing.expectEqual(@as(u8, 0xaa), ptr.?[0]);
442 
443     @memset(ptr.?[0..128], 0xbb);
444     ptr = (try heap.realloc(ptr, 16)).?;
445     try std.testing.expectEqual(@as(usize, 16), ShuffleAllocator.requestedLength(ptr.?).?);
446     for (ptr.?[0..16]) |byte| try std.testing.expectEqual(@as(u8, 0xbb), byte);
447 
448     const freed = ptr.?;
449     try std.testing.expect((try heap.realloc(ptr, 0)) == null);
450     try std.testing.expect(ShuffleAllocator.requestedLength(freed) == null);
451     ptr = null;
452     heap.freePointer(null);
453 }
454 
455 test "shuffle allocator marks freed shuffled pointers inactive" {
456     var heap = ShuffleAllocator.init(std.testing.allocator, .{
457         .shuffle_slots = 4,
458         .max_shuffled_size = 1024,
459     }, 1234);
460     defer heap.deinit();
461 
462     const ptr = try heap.malloc(24);
463     try std.testing.expect(heap.ownsPointer(ptr));
464     try std.testing.expectEqual(@as(usize, 24), ShuffleAllocator.requestedLength(ptr).?);
465     heap.freePointer(ptr);
466     try std.testing.expect(heap.ownsPointer(ptr));
467     try std.testing.expect(ShuffleAllocator.requestedLength(ptr) == null);
468     try std.testing.expectError(error.InvalidAllocation, heap.realloc(ptr, 24));
469 
470     var foreign = [_]u8{0};
471     try std.testing.expect(!heap.ownsPointer(foreign[0..].ptr));
472     try std.testing.expect(!heap.freePointerIfOwned(foreign[0..].ptr));
473 }
474 
475 test "allocator pointer validation is explicit and C pointers remain tracked" {
476     var heap = ShuffleAllocator.init(std.testing.allocator, .{
477         .pointer_validation = false,
478         .shuffle_slots = 4,
479         .max_shuffled_size = 1024,
480     }, 1234);
481     defer heap.deinit();
482 
483     const allocator = heap.allocator();
484     const raw = try allocator.alloc(u8, 24);
485     try std.testing.expectEqual(@as(usize, 0), heap.known.count());
486     try std.testing.expect(!heap.ownsPointer(raw.ptr));
487     allocator.free(raw);
488     try std.testing.expectEqual(@as(usize, 0), heap.known.count());
489 
490     const pointer = try heap.malloc(24);
491     try std.testing.expect(heap.ownsPointer(pointer));
492     try std.testing.expectEqual(AllocationState.active, heap.known.get(@intFromPtr(pointer)).?);
493     heap.freePointer(pointer);
494     try std.testing.expectEqual(AllocationState.inactive, heap.known.get(@intFromPtr(pointer)).?);
495 
496     const next_raw = try allocator.alloc(u8, 24);
497     try std.testing.expect(next_raw.ptr != pointer);
498     try std.testing.expect(!heap.ownsPointer(next_raw.ptr));
499     try std.testing.expectEqual(@as(usize, 4), heap.known.count());
500     allocator.free(next_raw);
501     try std.testing.expectEqual(@as(usize, 2), heap.pools.items.len);
502 }
503 
504 test "allocator pointer validation defaults to tracked pointers" {
505     var heap = ShuffleAllocator.init(std.testing.allocator, .{
506         .shuffle_slots = 1,
507         .max_shuffled_size = 1024,
508     }, 1234);
509     defer heap.deinit();
510 
511     const allocator = heap.allocator();
512     const memory = try allocator.alloc(u8, 24);
513     try std.testing.expect(heap.ownsPointer(memory.ptr));
514     allocator.free(memory);
515     try std.testing.expect(heap.ownsPointer(memory.ptr));
516 }
517 
518 test "pointer tracking allocation failure releases direct memory" {
519     var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 1 });
520     var heap = ShuffleAllocator.init(failing.allocator(), .{
521         .enabled = false,
522         .pointer_validation = false,
523     }, 1234);
524     defer heap.deinit();
525 
526     try std.testing.expectError(error.OutOfMemory, heap.malloc(24));
527     try std.testing.expectEqual(failing.allocated_bytes, failing.freed_bytes);
528 }
529 
530 test "tracked pool releases every failed initialization allocation" {
531     try std.testing.checkAllAllocationFailures(
532         std.testing.allocator,
533         exerciseTrackedPoolAllocationFailures,
534         .{},
535     );
536 }
537 
538 test "pooled pointer tracking failure preserves the selected slot" {
539     var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
540     var heap = ShuffleAllocator.init(failing.allocator(), .{
541         .pointer_validation = false,
542         .shuffle_slots = 1,
543         .max_shuffled_size = 1024,
544     }, 1234);
545 
546     const alignment = Alignment.fromByteUnits(1);
547     const pool = try heap.getPool(32, alignment, true);
548     const selected = pool.slots[0].memory.ptr;
549     var dummy_key: usize = 1;
550     while (heap.known.available > 0) : (dummy_key += 1) {
551         heap.known.putAssumeCapacity(dummy_key, .inactive);
552     }
553     const known_count = heap.known.count();
554     failing.fail_index = failing.alloc_index + 1;
555     try std.testing.expectError(error.OutOfMemory, heap.malloc(24));
556     try std.testing.expectEqual(selected, pool.slots[0].memory.ptr);
557     try std.testing.expect(!headerFromUser(selected).allocated);
558     try std.testing.expectEqual(known_count, heap.known.count());
559     try std.testing.expectEqual(AllocationState.inactive, heap.known.get(@intFromPtr(selected)).?);
560 
561     failing.fail_index = std.math.maxInt(usize);
562     const pointer = try heap.malloc(24);
563     heap.freePointer(pointer);
564     heap.deinit();
565     try std.testing.expectEqual(failing.allocated_bytes, failing.freed_bytes);
566 }
567 
568 test "realloc tracking failure preserves the old allocation" {
569     var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
570     var heap = ShuffleAllocator.init(failing.allocator(), .{
571         .pointer_validation = false,
572         .shuffle_slots = 1,
573         .max_shuffled_size = 1024,
574     }, 1234);
575 
576     const pointer = try heap.malloc(24);
577     pointer[0] = 0xa5;
578     var dummy_key: usize = 1;
579     while (heap.known.available > 0) : (dummy_key += 1) {
580         heap.known.putAssumeCapacity(dummy_key, .inactive);
581     }
582     failing.fail_index = failing.alloc_index + 1;
583     try std.testing.expectError(error.OutOfMemory, heap.realloc(pointer, 2048));
584     try std.testing.expectEqual(@as(u8, 0xa5), pointer[0]);
585     try std.testing.expectEqual(@as(usize, 24), ShuffleAllocator.requestedLength(pointer).?);
586     try std.testing.expect(heap.ownsPointer(pointer));
587 
588     failing.fail_index = std.math.maxInt(usize);
589     heap.freePointer(pointer);
590     heap.deinit();
591     try std.testing.expectEqual(failing.allocated_bytes, failing.freed_bytes);
592 }
593 
594 test "untracked direct allocations resize remap and free without the address map" {
595     var heap = ShuffleAllocator.init(std.testing.allocator, .{
596         .pointer_validation = false,
597         .max_shuffled_size = 8,
598     }, 1234);
599     defer heap.deinit();
600 
601     const allocator = heap.allocator();
602     var memory = try allocator.alloc(u8, 64);
603     @memset(memory, 0x5a);
604     try std.testing.expect(allocator.resize(memory, 32));
605     memory = memory.ptr[0..32];
606     try std.testing.expectEqual(@as(usize, 0), heap.known.count());
607 
608     memory = try allocator.realloc(memory, 128);
609     for (memory[0..32]) |byte| try std.testing.expectEqual(@as(u8, 0x5a), byte);
610     try std.testing.expectEqual(@as(usize, 0), heap.known.count());
611     allocator.free(memory);
612     try std.testing.expectEqual(@as(usize, 0), heap.known.count());
613 }
614 
615 test "allocation size overflow is reported as out of memory" {
616     var heap = ShuffleAllocator.init(std.testing.allocator, .{
617         .enabled = false,
618         .pointer_validation = false,
619     }, 1234);
620     defer heap.deinit();
621 
622     try std.testing.expectError(error.OutOfMemory, heap.malloc(std.math.maxInt(usize)));
623 }