authorgravatar for justus@klausecker.deJustus Klausecker <justus@klausecker.de> 2026-03-24 04:07:41+01:00
committergravatar for alex@alexrp.comAlex Rønne Petersen <alex@alexrp.com> 2026-03-25 00:54:44+01:00
log589bcb2544fed9a3454908adba4a2f97f1405e9c
tree9449d3f5c2e3a9bb0f9e0f129851b2530392fd90
parent5861afb1897872c9054ffbc36a221090a2228863

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

This prevents the following scenario where an allocation is wrongly assumed to be part of the current head node (`node0`): ``` | node0 - - - - | node1 - - - - - - - - - - - - | | | | | | | | | end_index0 end_index1 | | | | alloc0 alloc1 free(alloc1): load node0 buf0.ptr + end_index0 == alloc1.ptr + alloc1.len ? yes! end_index0 -= alloc1.len | node0 - - - - | node1 - - - - - - - - - - - | | | | | | end_index0 end_index1 | | alloc0 ``` which could move `end_index0` *into* `alloc0` and make it possible for any subsequent calls to `alloc` to overwrite its contents!

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

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