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 }