forked from ziglang/zig
-
Notifications
You must be signed in to change notification settings - Fork 13
std: add AddressSanitizer poisoning to allocators and ArrayList #23
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
Open
Jarred-Sumner
wants to merge
2
commits into
upgrade-0.15.2
Choose a base branch
from
root/asan-stdlib-poison
base: upgrade-0.15.2
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| //! AddressSanitizer manual-poisoning helpers. | ||
| //! | ||
| //! These wrap the ASAN runtime's manual-poison API so that allocators and | ||
| //! containers can mark freed/unused regions as inaccessible. All functions | ||
| //! compile to no-ops when `builtin.sanitize_address` is false, so call sites | ||
| //! need no `if` guard. | ||
| //! | ||
| //! Poisoning is shadow-byte granular (8 bytes on most targets). A region | ||
| //! whose start is not 8-aligned may leave the first partial qword unpoisoned; | ||
| //! this is a limitation of the ASAN shadow encoding, not of these wrappers. | ||
|
|
||
| const builtin = @import("builtin"); | ||
|
|
||
| /// Whether the current compilation has AddressSanitizer instrumentation. | ||
| /// Guarded with @hasDecl so the bootstrap stage1 (whose builtin predates | ||
| /// sanitize_address) can still compile std. | ||
| pub const enabled = @hasDecl(builtin, "sanitize_address") and builtin.sanitize_address; | ||
|
|
||
| /// Mark `[ptr, ptr+len)` as inaccessible. Any subsequent load or store in | ||
| /// that range triggers an ASAN `use-after-poison` report. | ||
| pub inline fn poison(ptr: [*]const u8, len: usize) void { | ||
| if (!enabled) return; | ||
| __asan_poison_memory_region(ptr, len); | ||
| } | ||
|
|
||
| /// Mark `[ptr, ptr+len)` as accessible again. | ||
| pub inline fn unpoison(ptr: [*]const u8, len: usize) void { | ||
| if (!enabled) return; | ||
| __asan_unpoison_memory_region(ptr, len); | ||
| } | ||
|
|
||
| /// Convenience overload taking any slice. | ||
| pub inline fn poisonSlice(slice: anytype) void { | ||
| if (!enabled) return; | ||
| if (slice.len == 0) return; | ||
| const bytes = @import("std").mem.sliceAsBytes(slice); | ||
| __asan_poison_memory_region(bytes.ptr, bytes.len); | ||
| } | ||
|
|
||
| /// Convenience overload taking any slice. | ||
| pub inline fn unpoisonSlice(slice: anytype) void { | ||
| if (!enabled) return; | ||
| if (slice.len == 0) return; | ||
| const bytes = @import("std").mem.sliceAsBytes(slice); | ||
| __asan_unpoison_memory_region(bytes.ptr, bytes.len); | ||
| } | ||
|
|
||
| /// Tell ASAN about a contiguous container's `[storage, storage+capacity)` | ||
| /// where only `[storage, storage+new_len)` is valid. Equivalent to libc++'s | ||
| /// vector annotation. `old_len` must be the value passed as `new_len` on the | ||
| /// previous call (or `capacity` on first call). All lengths are in bytes. | ||
| 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, | ||
| ); | ||
| } | ||
|
|
||
| /// Returns true if the byte at `ptr` is currently poisoned. Intended for | ||
| /// assertions in tests. | ||
| pub inline fn isPoisoned(ptr: *const anyopaque) bool { | ||
| if (!enabled) return false; | ||
| return __asan_address_is_poisoned(ptr) != 0; | ||
| } | ||
|
|
||
| extern fn __asan_poison_memory_region(addr: [*]const u8, size: usize) void; | ||
| extern fn __asan_unpoison_memory_region(addr: [*]const u8, size: usize) void; | ||
| extern fn __asan_address_is_poisoned(addr: *const anyopaque) c_int; | ||
| extern fn __sanitizer_annotate_contiguous_container( | ||
| beg: [*]const u8, | ||
| end: [*]const u8, | ||
| old_mid: [*]const u8, | ||
| new_mid: [*]const u8, | ||
| ) void; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| //! Tests that the manual ASAN poisoning applied by std allocators and | ||
| //! containers actually marks the right bytes. These tests query the ASAN | ||
| //! shadow via `Asan.isPoisoned` rather than dereferencing poisoned memory, | ||
| //! so they pass cleanly under `-fsanitize-address` instead of crashing. | ||
| //! | ||
| //! Every test is gated on `Asan.enabled` and skipped otherwise so that the | ||
| //! file can be built into the regular (non-sanitized) test binary without | ||
| //! producing false failures. | ||
| //! | ||
| //! Run directly with: | ||
| //! zig test lib/std/debug/asan_test.zig --zig-lib-dir lib -fsanitize-address -lc | ||
|
|
||
| const std = @import("std"); | ||
| const testing = std.testing; | ||
| const Asan = std.debug.Asan; | ||
|
|
||
| test "asan: ArrayList spare capacity is poisoned" { | ||
| if (!Asan.enabled) return error.SkipZigTest; | ||
|
|
||
| const gpa = testing.allocator; | ||
| var list: std.ArrayList(u64) = try .initCapacity(gpa, 8); | ||
| defer list.deinit(gpa); | ||
|
|
||
| try testing.expectEqual(@as(usize, 8), list.capacity); | ||
|
|
||
| try list.append(gpa, 1); | ||
| try list.append(gpa, 2); | ||
| try list.append(gpa, 3); | ||
|
|
||
| // Base of the backing buffer (items.ptr is a [*]u64; treat as bytes for | ||
| // shadow queries so we are explicit about granularity). | ||
| const base: [*]const u8 = @ptrCast(list.items.ptr); | ||
|
|
||
| // items[0..3] live, items[3..8] poisoned spare capacity. | ||
| try testing.expect(!Asan.isPoisoned(&base[0 * @sizeOf(u64)])); | ||
| try testing.expect(!Asan.isPoisoned(&base[2 * @sizeOf(u64)])); | ||
| try testing.expect(Asan.isPoisoned(&base[3 * @sizeOf(u64)])); | ||
| try testing.expect(Asan.isPoisoned(&base[7 * @sizeOf(u64)])); | ||
|
|
||
| // Popping shrinks the live region; the just-vacated slot becomes poison. | ||
| _ = list.pop(); | ||
| try testing.expect(!Asan.isPoisoned(&base[1 * @sizeOf(u64)])); | ||
| try testing.expect(Asan.isPoisoned(&base[2 * @sizeOf(u64)])); | ||
| } | ||
|
|
||
| test "asan: FixedBufferAllocator poisons freed regions" { | ||
| if (!Asan.enabled) return error.SkipZigTest; | ||
|
|
||
| // 8-byte alignment so the ASAN shadow boundaries line up exactly with | ||
| // the buffer; otherwise the first/last partial qword may be left | ||
| // unpoisoned by the runtime. | ||
| var buf: [256]u8 align(8) = undefined; | ||
| var fba = std.heap.FixedBufferAllocator.init(&buf); | ||
| defer fba.deinit(); | ||
| const a = fba.allocator(); | ||
|
|
||
| // init() does not poison eagerly (Zig has no per-frame shadow cleanup yet). | ||
| try testing.expect(!Asan.isPoisoned(&buf[0])); | ||
| try testing.expect(!Asan.isPoisoned(&buf[100])); | ||
|
|
||
| const slab = try a.alloc(u8, 64); | ||
| try testing.expect(!Asan.isPoisoned(&slab[0])); | ||
| try testing.expect(!Asan.isPoisoned(&slab[32])); | ||
|
|
||
| // free() poisons the returned region so use-after-free is caught. | ||
| a.free(slab); | ||
| try testing.expect(Asan.isPoisoned(&buf[0])); | ||
| try testing.expect(Asan.isPoisoned(&buf[32])); | ||
|
|
||
| // A fresh allocation past the high-water mark is still addressable. | ||
| const slab2 = try a.alloc(u8, 64); | ||
| try testing.expect(!Asan.isPoisoned(&slab2[0])); | ||
|
|
||
| // reset() poisons everything that was handed out so far. | ||
| fba.reset(); | ||
| try testing.expect(Asan.isPoisoned(&slab2[0])); | ||
| } | ||
|
|
||
| test "asan: ArenaAllocator reset(.retain_capacity) re-poisons" { | ||
| if (!Asan.enabled) return error.SkipZigTest; | ||
|
|
||
| var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); | ||
| defer arena.deinit(); | ||
| const a = arena.allocator(); | ||
|
|
||
| const slab = try a.alloc(u64, 16); | ||
| const base: [*]const u8 = @ptrCast(slab.ptr); | ||
| try testing.expect(!Asan.isPoisoned(&base[0])); | ||
| try testing.expect(!Asan.isPoisoned(&base[15 * @sizeOf(u64)])); | ||
|
|
||
| // Keep the buffer but rewind end_index to 0; the previously handed-out | ||
| // bytes must now be inaccessible. | ||
| _ = arena.reset(.retain_capacity); | ||
| try testing.expect(Asan.isPoisoned(&base[0])); | ||
| try testing.expect(Asan.isPoisoned(&base[8 * @sizeOf(u64)])); | ||
| } | ||
|
|
||
| test "asan: MemoryPool destroy poisons item tail" { | ||
| if (!Asan.enabled) return error.SkipZigTest; | ||
|
|
||
| // Item large enough that there is a poisonable tail past the free-list | ||
| // `next` pointer (one usize). | ||
| const Item = struct { data: [32]u8 }; | ||
|
|
||
| var pool = std.heap.MemoryPool(Item).init(testing.allocator); | ||
| defer pool.deinit(); | ||
|
|
||
| const item = try pool.create(); | ||
| const bytes: [*]const u8 = @ptrCast(item); | ||
|
|
||
| // Freshly created: whole item is addressable. | ||
| try testing.expect(!Asan.isPoisoned(&bytes[0])); | ||
| try testing.expect(!Asan.isPoisoned(&bytes[@sizeOf(Item) - 1])); | ||
|
|
||
| pool.destroy(item); | ||
|
|
||
| // First @sizeOf(usize) bytes hold the free-list link and stay unpoisoned; | ||
| // everything after is poison. | ||
| const link_end = @sizeOf(usize); | ||
| try testing.expect(!Asan.isPoisoned(&bytes[0])); | ||
| try testing.expect(Asan.isPoisoned(&bytes[link_end])); | ||
| try testing.expect(Asan.isPoisoned(&bytes[@sizeOf(Item) - 1])); | ||
|
|
||
| // Re-create pulls from the free list and unpoisons the full item again. | ||
| const item2 = try pool.create(); | ||
| try testing.expectEqual(@intFromPtr(item), @intFromPtr(item2)); | ||
| try testing.expect(!Asan.isPoisoned(&bytes[link_end])); | ||
| try testing.expect(!Asan.isPoisoned(&bytes[@sizeOf(Item) - 1])); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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_lenagainstcapacitywould 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
🤖 Prompt for AI Agents