authorgravatar for justus@klausecker.deJustus Klausecker <justus@klausecker.de> 2026-03-24 16:32:10+01:00
committergravatar for justus@klausecker.deJustus Klausecker <justus@klausecker.de> 2026-03-25 11:48:43+01:00
log3af5f81e11e2fd88fe227b44753e3df0c4dab094
tree6de22ec05c35cb32e4fa8eda219da9550588fd11
parent9bfe827adedeffea591232fb713bea7b145c0add

std.heap.ArenaAllocator: fix `end_index` memory ordering

This prevents a race between `alloc` and `free` where T1 receives memory from `alloc` that is semantically about to be freed by T2 and still being accessed, but the `free` is already visible to T1. Using acquire-release here guarantees that any `free` is only published after all accesses to the memory being freed have already happened. Co-authored-by: Jacob Young <amazingjacob@gmail.com>

1 files changed, 56 insertions(+), 33 deletions(-)

lib/std/heap/ArenaAllocator.zig+56-33
......@@ -207,17 +207,20 @@ pub fn reset(arena: *ArenaAllocator, mode: ResetMode) bool {
207207
208208/// Concurrent accesses to node pointers generally have to have acquire/release
209209/// semantics to guarantee that newly allocated notes are in a valid state when
210/// being inserted into a list. Exceptions are possible, e.g. a CAS loop that
210/// being inserted into a list. Exceptions are possible, e.g. a cmpxchg loop that
211211/// never accesses the node returned on failure can use monotonic semantics on
212212/// failure, but must still use release semantics on success to protect the node
213213/// it's trying to push.
214214const Node = struct {
215215 /// Only meant to be accessed indirectly via the methods supplied by this type,
216216 /// except if the node is owned by the thread accessing it.
217 /// Must always be an even number to accomodate `resize` bit.
217 /// Must always be an even number to accommodate `resize` bit.
218218 size: Size,
219 /// Concurrent accesses to `end_index` can be monotonic as long as its value
220 /// is compared to a version of `size` before using it to access memory.
219 /// Any increase of `end_index` has to use acquire semantics;
220 /// any decrease of `end_index` that invalidates (formerly) active allocations
221 /// has to use release semantics.
222 /// This guarantees that all accesses to memory that's about to be freed
223 /// happen-before the free is published.
221224 /// Since `size` can only grow and never shrink, memory access depending on
222225 /// any `end_index` <= any `size` can never be OOB.
223226 end_index: usize,
......@@ -352,10 +355,17 @@ fn alloc(ctx: *anyopaque, n: usize, alignment: Alignment, ret_addr: usize) ?[*]u
352355 // with a single cmpxchg afterwards, which may fail.
353356
354357 const alignable = n + alignment.toByteUnits() - 1;
355 const end_index = @atomicRmw(usize, &node.end_index, .Add, alignable, .monotonic);
358 const end_index = @atomicRmw(usize, &node.end_index, .Add, alignable, .acquire); // acquire any memory that may have been freed
356359 const aligned_index = alignedIndex(buf.ptr, end_index, alignment);
357360 assert(end_index + alignable >= aligned_index + n);
358 _ = @cmpxchgStrong(usize, &node.end_index, end_index + alignable, aligned_index + n, .monotonic, .monotonic);
361 _ = @cmpxchgStrong(
362 usize,
363 &node.end_index,
364 end_index + alignable,
365 aligned_index + n,
366 .monotonic, // no need to release alignment padding; there's no one accessing it!
367 .monotonic,
368 );
359369
360370 if (aligned_index + n > buf.len) break :first_node .{ node, buf.len };
361371 return buf[aligned_index..][0..n].ptr;
......@@ -377,7 +387,7 @@ fn alloc(ctx: *anyopaque, n: usize, alignment: Alignment, ret_addr: usize) ?[*]u
377387 const new_size = mem.alignForward(usize, @sizeOf(Node) + aligned_index + n, 2);
378388
379389 if (new_size <= allocated_slice.len) {
380 // a `resize` or `free` call managed to sneak in and we need to
390 // A `resize` or `free` call managed to sneak in and we need to
381391 // guarantee that `size` is only ever increased; retry!
382392 continue :retry;
383393 }
......@@ -385,14 +395,16 @@ fn alloc(ctx: *anyopaque, n: usize, alignment: Alignment, ret_addr: usize) ?[*]u
385395 if (arena.child_allocator.rawResize(allocated_slice, .of(Node), new_size, @returnAddress())) {
386396 size = new_size;
387397
388 if (@cmpxchgStrong( // strong because a spurious failure could result in suboptimal usage of this node
398 // strong because a spurious failure could result in suboptimal
399 // usage of this node
400 if (null == @cmpxchgStrong(
389401 usize,
390402 &node.end_index,
391403 end_index,
392404 aligned_index + n,
405 .acquire, // acquire any memory that may have been freed
393406 .monotonic,
394 .monotonic,
395 ) == null) {
407 )) {
396408 const new_buf = allocated_slice.ptr[0..new_size][@sizeOf(Node)..];
397409 return new_buf[aligned_index..][0..n].ptr;
398410 }
......@@ -546,35 +558,45 @@ fn resize(ctx: *anyopaque, memory: []u8, alignment: Alignment, new_len: usize, r
546558 const buf_ptr = @as([*]u8, @ptrCast(node)) + @sizeOf(Node);
547559
548560 const cur_end_index = @atomicLoad(usize, &node.end_index, .monotonic);
561
549562 if (buf_ptr + cur_end_index != memory.ptr + memory.len) {
550563 // It's not the most recent allocation, so it cannot be expanded,
551564 // but it's fine if they want to make it smaller.
552565 return new_len <= memory.len;
553566 }
554567
555 const new_end_index: usize = new_end_index: {
556 if (memory.len >= new_len) {
557 break :new_end_index cur_end_index - (memory.len - new_len);
558 }
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) {
563 break :new_end_index cur_end_index + (new_len - memory.len);
564 }
565 return false;
566 };
567 assert(buf_ptr + new_end_index == memory.ptr + new_len);
568 if (new_len <= memory.len) {
569 const new_end_index = cur_end_index - (memory.len - new_len);
570 assert(buf_ptr + new_end_index == memory.ptr + new_len);
571
572 _ = @cmpxchgStrong(
573 usize,
574 &node.end_index,
575 cur_end_index,
576 new_end_index,
577 .release, // release freed memory
578 .monotonic,
579 );
580 return true; // Shrinking allocations should always succeed.
581 }
568582
569 return null == @cmpxchgStrong(
570 usize,
571 &node.end_index,
572 cur_end_index,
573 new_end_index,
574 .monotonic,
575 .monotonic,
576 ) or
577 new_len <= memory.len; // Shrinking allocations should always succeed.
583 // Saturating arithmetic because `end_index` is not guaranteed to be `<= size`.
584 // The allocation we're trying to resize *could* belong to a different node!
585 if (node.loadBuf().len -| cur_end_index >= new_len - memory.len) {
586 const new_end_index = cur_end_index + (new_len - memory.len);
587 assert(buf_ptr + new_end_index == memory.ptr + new_len);
588
589 return null == @cmpxchgStrong(
590 usize,
591 &node.end_index,
592 cur_end_index,
593 new_end_index,
594 .acquire, // acquire any memory that may have been freed
595 .monotonic,
596 );
597 }
598
599 return false;
578600}
579601
580602fn remap(ctx: *anyopaque, memory: []u8, alignment: Alignment, new_len: usize, ret_addr: usize) ?[*]u8 {
......@@ -592,6 +614,7 @@ fn free(ctx: *anyopaque, memory: []u8, alignment: Alignment, ret_addr: usize) vo
592614 const buf_ptr = @as([*]u8, @ptrCast(node)) + @sizeOf(Node);
593615
594616 const cur_end_index = @atomicLoad(usize, &node.end_index, .monotonic);
617
595618 if (buf_ptr + cur_end_index != memory.ptr + memory.len) {
596619 // Not the most recent allocation; we cannot free it.
597620 return;
......@@ -605,7 +628,7 @@ fn free(ctx: *anyopaque, memory: []u8, alignment: Alignment, ret_addr: usize) vo
605628 &node.end_index,
606629 cur_end_index,
607630 new_end_index,
608 .monotonic,
631 .release, // release freed memory
609632 .monotonic,
610633 );
611634}