Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
sb2bg opened this issue Jan 13, 2025 · 0 comments
Open

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

sb2bg opened this issue Jan 13, 2025 · 0 comments
Labels
bug Observed behavior contradicts documented or intended behavior contributor friendly This issue is limited in scope and/or knowledge of Zig internals. standard library This issue involves writing Zig code for the standard library.
Milestone

Comments

@sb2bg
Copy link

sb2bg commented Jan 13, 2025

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
@sb2bg sb2bg added the bug Observed behavior contradicts documented or intended behavior label Jan 13, 2025
@sb2bg sb2bg changed the title toOwnedSlice fails with empty struct in std.ArrayList using GeneralPurposeAllocator toOwnedSlice fails with zero-sized types in std.ArrayList using GeneralPurposeAllocator Jan 13, 2025
@sb2bg sb2bg changed the title toOwnedSlice fails with zero-sized types in std.ArrayList using GeneralPurposeAllocator ArrayList.toOwnedSlice assertion error with zero-sized types Jan 13, 2025
@andrewrk andrewrk added standard library This issue involves writing Zig code for the standard library. contributor friendly This issue is limited in scope and/or knowledge of Zig internals. labels Jan 25, 2025
@andrewrk andrewrk added this to the 0.15.0 milestone Jan 25, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Observed behavior contradicts documented or intended behavior contributor friendly This issue is limited in scope and/or knowledge of Zig internals. standard library This issue involves writing Zig code for the standard library.
Projects
None yet
Development

No branches or pull requests

2 participants