Skip to content

std: add AddressSanitizer poisoning to allocators and ArrayList#23

Open
Jarred-Sumner wants to merge 2 commits into
upgrade-0.15.2from
root/asan-stdlib-poison
Open

std: add AddressSanitizer poisoning to allocators and ArrayList#23
Jarred-Sumner wants to merge 2 commits into
upgrade-0.15.2from
root/asan-stdlib-poison

Conversation

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

Adds std.debug.Asan and instruments stdlib allocators/containers so ASAN catches bug classes it currently misses entirely.

What ASAN now catches that it didn't before

container/allocator bug class now detected
ArrayList / ArrayListUnmanaged read/write into [len..capacity] (container overflow)
FixedBufferAllocator use-after-free, use-after-reset
ArenaAllocator use-after-free, use-after-reset(.retain_capacity)
MemoryPool use-after-destroy (past the free-list link word)
DebugAllocator use-after-free of small bucket slots while page is retained
StackFallbackAllocator use-after-free from the fixed buffer

All calls compile to no-ops when builtin.sanitize_address is false.

Design notes

  • Asan.annotateContiguousContainer() wraps __sanitizer_annotate_contiguous_container (the libc++ vector mechanism) for ArrayList.
  • Memory returned to a backing allocator is always unpoisoned first, since the backing allocator may not be ASAN-aware.
  • FBA/StackFallback do not eagerly poison the buffer on init(): Zig doesn't yet emit per-frame ASAN shadow cleanup (no lifetime markers), so poisoning a stack buffer would leave stale poison after the FBA goes out of scope. Only freed regions are poisoned. deinit() is added for explicit cleanup.
  • MemoryPool leaves the first pointer-sized bytes of freed items unpoisoned so the free-list link stays walkable.

Tests

lib/std/debug/asan_test.zig verifies each via Asan.isPoisoned(); skips when ASAN is disabled.

filter no-ASAN with ASAN
ArrayList 76 pass 77 pass / 0 fail
FixedBuffer / Arena / memory_pool / DebugAllocator / StackFallback all pass all pass
asan: detection tests skip 4 pass

Run with ASAN: zig test lib/std/std.zig --zig-lib-dir lib -fsanitize-address -lc <asan_rt.a> -lpthread -ldl -lm -lrt -lunwind --test-filter 'asan:'

Adds std.debug.Asan with inline wrappers around the ASAN runtime's
manual-poison API (__asan_poison_memory_region / unpoison /
__sanitizer_annotate_contiguous_container). All calls are no-ops when
builtin.sanitize_address is false.

Instrumented:
- ArrayList / ArrayListUnmanaged: annotate [len..capacity] as poisoned
  via the contiguous-container API, mirroring libc++ vector. Catches
  out-of-bounds reads/writes into reserved capacity.
- FixedBufferAllocator: poison freed regions and the previously-used
  range on reset(). Adds deinit() to unpoison the backing buffer.
  Does not eagerly poison on init() since Zig does not yet emit
  per-frame shadow cleanup, which would leave stale poison on stack
  buffers.
- ArenaAllocator: poison the unused tail of each buffer node and
  freed/reset regions; unpoison before returning memory to the child
  allocator.
- MemoryPool: poison destroyed items past the free-list link; unpoison
  on create(). Catches use-after-destroy.
- DebugAllocator: poison freed bucket slots and the unallocated tail
  of fresh pages; unpoison before returning pages to the backing
  allocator.
- StackFallbackAllocator: poison on free from the fixed buffer.

Detection tests in std/debug/asan_test.zig verify each via
Asan.isPoisoned(); they skip when ASAN is disabled.
@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown

Walkthrough

Adds AddressSanitizer (ASAN) support across the standard library: a new debug.Asan module and tests, plus ASAN-aware poisoning/unpoisoning and annotation logic in ArrayList variants and multiple allocator implementations to maintain correct ASAN shadow state during alloc/free/resize/reset operations.

Changes

Cohort / File(s) Summary
ASAN Core Module
lib/std/debug.zig, lib/std/debug/Asan.zig, lib/std/debug/asan_test.zig
New debug.Asan export and module providing enabled, poison/unpoison wrappers, contiguous-container annotation, isPoisoned and extern ASAN bindings; adds compile-time test coverage exercising ASAN behaviors.
ArrayList ASAN bookkeeping
lib/std/array_list.zig
Adds internal helpers and calls to unpoison backing buffer before handing memory out and to re-annotate container bounds after any change to items.len/capacity; direct-write entry points unpoison spare capacity.
FixedBuffer & Stack allocators
lib/std/heap.zig, lib/std/heap/FixedBufferAllocator.zig
Unpoisons allocated ranges on alloc, poisons freed regions and truncated tails on shrink, unpoisons grown regions on expand; adds deinit to clear/unpoison backing buffer and documents stack allocator poison behavior.
Arena allocator
lib/std/heap/arena_allocator.zig
Unpoisons returned slices, poisons freed/truncated tails, handles retain_capacity reset paths with appropriate unpoison/poison around resizing and deinit.
Debug allocator (small objects)
lib/std/heap/debug_allocator.zig
Unpoisons slots when handed out, poisons freed slots and non-returned slots on new pages, and unpoisons pages before returning them to a non-ASAN-aware backing allocator.
Memory pool
lib/std/heap/memory_pool.zig
Poisoned payload tails of freed/preheated items to catch stale-use; unpoisons items when created/returned and before reset/deinit; adds traversal helper to unpoison free-list payloads.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'std: add AddressSanitizer poisoning to allocators and ArrayList' directly describes the primary change: adding ASAN poisoning support to stdlib allocators and container types.
Description check ✅ Passed The description comprehensively explains what was added (std.debug.Asan module), which bug classes are now detected across different allocators, design rationale, and test coverage, all directly related to the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/std/debug/Asan.zig`:
- Around line 52-66: annotateContiguousContainer trusts callers; add fail-fast
debug precondition checks to validate bounds before calling
__sanitizer_annotate_contiguous_container: assert that capacity != 0 (already
checked), that old_len <= capacity, new_len <= capacity, and that old_len <=
new_len (or vice‑versa if the function expects shrink/expand semantics), and
optionally assert storage is non-null when enabled; place these checks in
annotateContiguousContainer near the top so misuse is caught in debug builds
before invoking __sanitizer_annotate_contiguous_container.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6f0cc3da-d78e-4798-af82-52ba06b2d6c3

📥 Commits

Reviewing files that changed from the base of the PR and between bdd6ecb and 5dd82fe.

📒 Files selected for processing (1)
  • lib/std/debug/Asan.zig

Comment thread lib/std/debug/Asan.zig
Comment on lines +52 to +66
pub inline fn annotateContiguousContainer(
storage: [*]const u8,
capacity: usize,
old_len: usize,
new_len: usize,
) void {
if (!enabled) return;
if (capacity == 0) return;
__sanitizer_annotate_contiguous_container(
storage,
storage + capacity,
storage + old_len,
storage + new_len,
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Add fail-fast precondition checks for container annotation bounds.

The docstring defines the contract, but Line 60 currently trusts callers completely. Adding debug assertions for old_len/new_len against capacity would catch misuse earlier.

Suggested diff
 pub inline fn annotateContiguousContainer(
     storage: [*]const u8,
     capacity: usize,
     old_len: usize,
     new_len: usize,
 ) void {
     if (!enabled) return;
     if (capacity == 0) return;
+    `@import`("std").debug.assert(old_len <= capacity);
+    `@import`("std").debug.assert(new_len <= capacity);
     __sanitizer_annotate_contiguous_container(
         storage,
         storage + capacity,
         storage + old_len,
         storage + new_len,
     );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub inline fn annotateContiguousContainer(
storage: [*]const u8,
capacity: usize,
old_len: usize,
new_len: usize,
) void {
if (!enabled) return;
if (capacity == 0) return;
__sanitizer_annotate_contiguous_container(
storage,
storage + capacity,
storage + old_len,
storage + new_len,
);
}
pub inline fn annotateContiguousContainer(
storage: [*]const u8,
capacity: usize,
old_len: usize,
new_len: usize,
) void {
if (!enabled) return;
if (capacity == 0) return;
`@import`("std").debug.assert(old_len <= capacity);
`@import`("std").debug.assert(new_len <= capacity);
__sanitizer_annotate_contiguous_container(
storage,
storage + capacity,
storage + old_len,
storage + new_len,
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/std/debug/Asan.zig` around lines 52 - 66, annotateContiguousContainer
trusts callers; add fail-fast debug precondition checks to validate bounds
before calling __sanitizer_annotate_contiguous_container: assert that capacity
!= 0 (already checked), that old_len <= capacity, new_len <= capacity, and that
old_len <= new_len (or vice‑versa if the function expects shrink/expand
semantics), and optionally assert storage is non-null when enabled; place these
checks in annotateContiguousContainer near the top so misuse is caught in debug
builds before invoking __sanitizer_annotate_contiguous_container.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant