Skip to content

ArrayList.toOwnedSlice assertion error with zero-sized types #22483

@sb2bg

Description

@sb2bg

Zig Version

0.14.0-dev.2613+0bf44c309

Description

Calling ArrayList.toOwnedSlice() with a zero-sized type triggers an assertion error in the allocator's resize implementation when using certain allocators.

Steps to Reproduce

Minimal Example

const std = @import("std");
const Foo = struct {};
pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var arr = std.ArrayList(Foo).init(allocator);
    try arr.append(.{});
    try arr.append(.{});

    const slice = try arr.toOwnedSlice(); // assertion error occurs here
    std.debug.print("{any}\n", .{slice});
}

Observed Behavior

The program panics with an assertion error when calling toOwnedSlice().

Stack Trace

thread 1 panic: reached unreachable code
/usr/lib/zig/std/debug.zig:403:14: 0x103987d in assert (main)
    if (!ok) unreachable; // assertion failure
             ^
/usr/lib/zig/std/heap/general_purpose_allocator.zig:709:19: 0x103a4a9 in resize (main)
            assert(old_mem.len != 0);
                  ^
/usr/lib/zig/std/mem/Allocator.zig:92:30: 0x103c608 in resize__anon_4164 (main)
    return self.vtable.resize(self.ptr, buf, log2_buf_align, new_len, ret_addr);
                             ^
/usr/lib/zig/std/array_list.zig:114:33: 0x1036ba1 in toOwnedSlice (main)
            if (allocator.resize(old_memory, self.items.len)) {
                                ^
/sandbox/src/main.zig:12:43: 0x10367db in main (main)
        const slice = try arr.toOwnedSlice();

Allocator-Specific Behavior

  • Fails with assertion error:
    • std.heap.GeneralPurposeAllocator
    • std.heap.FixedBufferAllocator
  • Works correctly:
    • std.heap.page_allocator
    • std.heap.ArenaAllocator (with any underlying allocator including GPA, page_allocator, and FixedBufferAllocator)

Expected Behavior

The program should complete without any assertion errors and print:

{ main.Foo{ }, main.Foo{ } }

Technical Analysis

Zero-Sized Type Behavior

  • The size of Foo (@sizeOf(Foo)) is 0
  • Slices of Foo have a conceptual length but occupy no actual memory, which leads to mismatch described below:

Implementation Details

  1. toOwnedSlice calls allocator.resize(old_memory, self.items.len) to attempt an in-place resize
  2. Inside Allocator.resize, mem.sliceAsBytes is called to convert the slice to bytes:
    const old_byte_slice = mem.sliceAsBytes(old_mem);
  3. The sliceAsBytes function has special handling for zero-sized types:
    if (@sizeOf(std.meta.Elem(Slice)) == 0)
        return &[0]u8{};
    This converts any slice of zero-sized types into an empty byte slice, regardless of the original slice's length
  4. The empty byte slice is passed through Allocator.rawResize to the allocator's vtable implementation
  5. In the allocator's resize implementation, there's an assertion:
    fn resize(...) bool {
        ...
        assert(old_mem.len != 0);  // Fails because old_mem is now empty
    This assertion fails because the byte slice length is 0, even though the original slice had a non-zero length

Root Cause

std.ArrayList.toOwnedSlice does not account for zero-sized types correctly, resulting in an invalid call to allocator.resize. The mismatch between the conceptual length of the zero-sized type slice and the actual byte slice length leads to the assertion failure.

Proposed solution

Since zero-sized types require no actual memory allocation, we can short-circuit the resize operation for them. Here's my proposed modification to toOwnedSlice:

pub fn toOwnedSlice(self: *Self) Allocator.Error!Slice {
    const allocator = self.allocator;
    
    if (@sizeOf(T) == 0 or allocator.resize(self.allocatedSlice(), self.items.len)) {
        const result = self.items;
        self.* = init(allocator);
        return result;
    }
    const new_memory = try allocator.alignedAlloc(T, alignment, self.items.len);
    @memcpy(new_memory, self.items);
    @memset(self.items, undefined);
    self.clearAndFree();
    return new_memory;
}

This:

  1. Short-circuits evaluation to handle both zero-sized types and successful resize operations
  2. Maintains the existing behavior for types that require memory allocation
  3. Preserves the contract that the ArrayList is emptied after the call
  4. Avoids the problematic resize call entirely for zero-sized types

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugObserved behavior contradicts documented or intended behaviorcontributor friendlyThis issue is limited in scope and/or knowledge of Zig internals.standard libraryThis issue involves writing Zig code for the standard library.

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions