authorgravatar for justus@klausecker.deJustus Klausecker <justus@klausecker.de> 2026-03-25 11:20:21+01:00
committergravatar for justus@klausecker.deJustus Klausecker <justus@klausecker.de> 2026-03-25 11:20:21+01:00
log9bfe827adedeffea591232fb713bea7b145c0add
treeeb6c963e70e446ec23f2187ab3534ffbb7997f42
parent94355f1920d880837823812481140270d0dc631e

Revert "std.heap.ArenaAllocator: Make `resize` and `free` check whether allocation is within current node more rigorously"

This reverts commit 589bcb2544fed9a3454908adba4a2f97f1405e9c. The scenario presented in the reverted commit cannot actually happen. Even if there are two contiguous arena nodes N1 and N2 and the `end_index` of N1 points to somewhere in N2, a `resize` can never lead to an increase of the `end_index` of N1 since it checks whether it's `<= size` first. A `resize`/`free` *can* decrease `end_index`, but even if it is wrongly assumed that some allocation that belongs to N2 actually belongs to N1 based on the `end_index` of N1, it can only ever be decreased to the start of the buffer of N2. That's because a valid allocation of N2 logically cannot be at any lower address than N2 itself. And any point still in N2 can never also be in N1, so there's no danger of overwriting any other allocations of N1.

1 files changed, 12 insertions(+), 25 deletions(-)

lib/std/heap/ArenaAllocator.zig+12-25
...@@ -319,11 +319,6 @@ fn pushFreeList(arena: *ArenaAllocator, first: *Node, last: *Node) void {...@@ -319,11 +319,6 @@ fn pushFreeList(arena: *ArenaAllocator, first: *Node, last: *Node) void {
319 }319 }
320}320}
321321
322fn sliceContainsSlice(container: []u8, slice: []u8) bool {
323 return @intFromPtr(slice.ptr) >= @intFromPtr(container.ptr) and
324 @intFromPtr(slice.ptr + slice.len) <= @intFromPtr(container.ptr + container.len);
325}
326
327fn alignedIndex(buf_ptr: [*]u8, end_index: usize, alignment: Alignment) usize {322fn alignedIndex(buf_ptr: [*]u8, end_index: usize, alignment: Alignment) usize {
328 // Wrapping arithmetic to avoid overflows since `end_index` isn't bounded by323 // Wrapping arithmetic to avoid overflows since `end_index` isn't bounded by
329 // `size`. This is always ok since the max alignment in byte units is also324 // `size`. This is always ok since the max alignment in byte units is also
...@@ -548,17 +543,12 @@ fn resize(ctx: *anyopaque, memory: []u8, alignment: Alignment, new_len: usize, r...@@ -548,17 +543,12 @@ fn resize(ctx: *anyopaque, memory: []u8, alignment: Alignment, new_len: usize, r
548 assert(new_len > 0);543 assert(new_len > 0);
549544
550 const node = arena.loadFirstNode().?;545 const node = arena.loadFirstNode().?;
551 const buf = node.loadBuf();546 const buf_ptr = @as([*]u8, @ptrCast(node)) + @sizeOf(Node);
552
553 if (!sliceContainsSlice(buf, memory)) {
554 // Not within current node.
555 return new_len <= memory.len;
556 }
557547
558 const cur_end_index = @atomicLoad(usize, &node.end_index, .monotonic);548 const cur_end_index = @atomicLoad(usize, &node.end_index, .monotonic);
559549 if (buf_ptr + cur_end_index != memory.ptr + memory.len) {
560 if (buf.ptr + cur_end_index != memory.ptr + memory.len) {550 // It's not the most recent allocation, so it cannot be expanded,
561 // It's not the most recent allocation, so it cannot be expanded.551 // but it's fine if they want to make it smaller.
562 return new_len <= memory.len;552 return new_len <= memory.len;
563 }553 }
564554
...@@ -566,12 +556,15 @@ fn resize(ctx: *anyopaque, memory: []u8, alignment: Alignment, new_len: usize, r...@@ -566,12 +556,15 @@ fn resize(ctx: *anyopaque, memory: []u8, alignment: Alignment, new_len: usize, r
566 if (memory.len >= new_len) {556 if (memory.len >= new_len) {
567 break :new_end_index cur_end_index - (memory.len - new_len);557 break :new_end_index cur_end_index - (memory.len - new_len);
568 }558 }
569 if (buf.len - cur_end_index >= new_len - memory.len) {559 const cur_buf_len: usize = node.loadBuf().len;
560 // Saturating arithmetic because `end_index` and `size` are not
561 // guaranteed to be in sync.
562 if (cur_buf_len -| cur_end_index >= new_len - memory.len) {
570 break :new_end_index cur_end_index + (new_len - memory.len);563 break :new_end_index cur_end_index + (new_len - memory.len);
571 }564 }
572 return false;565 return false;
573 };566 };
574 assert(buf.ptr + new_end_index == memory.ptr + new_len);567 assert(buf_ptr + new_end_index == memory.ptr + new_len);
575568
576 return null == @cmpxchgStrong(569 return null == @cmpxchgStrong(
577 usize,570 usize,
...@@ -596,22 +589,16 @@ fn free(ctx: *anyopaque, memory: []u8, alignment: Alignment, ret_addr: usize) vo...@@ -596,22 +589,16 @@ fn free(ctx: *anyopaque, memory: []u8, alignment: Alignment, ret_addr: usize) vo
596 assert(memory.len > 0);589 assert(memory.len > 0);
597590
598 const node = arena.loadFirstNode().?;591 const node = arena.loadFirstNode().?;
599 const buf = node.loadBuf();592 const buf_ptr = @as([*]u8, @ptrCast(node)) + @sizeOf(Node);
600
601 if (!sliceContainsSlice(buf, memory)) {
602 // Not within current node; we cannot free it.
603 return;
604 }
605593
606 const cur_end_index = @atomicLoad(usize, &node.end_index, .monotonic);594 const cur_end_index = @atomicLoad(usize, &node.end_index, .monotonic);
607595 if (buf_ptr + cur_end_index != memory.ptr + memory.len) {
608 if (buf.ptr + cur_end_index != memory.ptr + memory.len) {
609 // Not the most recent allocation; we cannot free it.596 // Not the most recent allocation; we cannot free it.
610 return;597 return;
611 }598 }
612599
613 const new_end_index = cur_end_index - memory.len;600 const new_end_index = cur_end_index - memory.len;
614 assert(buf.ptr + new_end_index == memory.ptr);601 assert(buf_ptr + new_end_index == memory.ptr);
615602
616 _ = @cmpxchgStrong(603 _ = @cmpxchgStrong(
617 usize,604 usize,