authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-04-02 15:57:41+02:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-04-02 15:57:41+02:00
loge9df86aed0b5e52c7fa5ebe51e60602d5a31c351
tree0692d8b30de61e20a3ffc5a805174d7607cb409c
parent43d84420f3bca06be86397049c3b940358131f75
parentd78f096c499093f89c3b8f9ced1beb204c22ebec

Merge pull request 'std.heap.ArenaAllocator: decrease fuzz test workload per run' (#31596) from justusk/zig:fuzz-arena-2 into master

Reviewed-on: https://codeberg.org/ziglang/zig/pulls/31596 Reviewed-by: Andrew Kelley <andrew@ziglang.org>

2 files changed, 139 insertions(+), 146 deletions(-)

lib/std/debug/Info.zig+4
......@@ -99,6 +99,10 @@ pub fn resolveAddresses(
9999 // due to split debug information. For now, we'll just resolve the addreses one by one.
100100 for (sorted_pc_addrs, output) |pc_addr, *src_loc| {
101101 const dwarf, const dwarf_pc_addr = mf.getDwarfForAddress(gpa, io, pc_addr) catch |err| switch (err) {
102 error.MissingDebugInfo => {
103 src_loc.* = .invalid;
104 continue;
105 },
102106 error.InvalidMachO, error.InvalidDwarf => return error.InvalidDebugInfo,
103107 else => |e| return e,
104108 };
lib/std/heap/ArenaAllocator.zig+135-146
......@@ -682,45 +682,40 @@ test "reset while retaining a buffer" {
682682 try std.testing.expectEqual(2, arena_allocator.queryCapacity());
683683}
684684
685test "fuzz" {
685test "fuzz multi threaded" {
686686 @disableInstrumentation();
687687 if (@import("builtin").single_threaded) return error.SkipZigTest;
688688
689689 const gpa = std.heap.smp_allocator;
690690
691 var io_instance: std.Io.Threaded = .init(gpa, .{});
692 defer io_instance.deinit();
693
691694 var arena_state: ArenaAllocator.State = .init;
692695 // No need to deinit arena_state, all allocations are in `sample_buffer`!
693696
694 const control_buffer = try gpa.alloc(u8, 64 << 10 << 10);
697 const buffer_size = FuzzContext.max_alloc_count * FuzzContext.max_alloc_size;
698
699 const control_buffer = try gpa.alloc(u8, buffer_size);
695700 defer gpa.free(control_buffer);
696701 var control_instance: std.heap.FixedBufferAllocator = .init(control_buffer);
697702
698 const sample_buffer = try gpa.alloc(u8, 64 << 10 << 10);
703 const sample_buffer = try gpa.alloc(u8, buffer_size);
699704 defer gpa.free(sample_buffer);
700705 var sample_instance: FuzzAllocator = .init(sample_buffer);
701706
702 var allocs: FuzzContext.Allocs = try .initCapacity(gpa, FuzzContext.max_alloc_count);
703 defer allocs.deinit(gpa);
704
705707 try std.testing.fuzz(FuzzContext.Init{
706 .gpa = gpa,
707 .allocs = &allocs,
708 .threaded_instance = &io_instance,
708709 .arena_state = &arena_state,
709710 .control_instance = &control_instance,
710711 .sample_instance = &sample_instance,
711 }, fuzzArenaAllocator, .{});
712 }, fuzzMultiThreaded, .{});
712713}
713714
714fn fuzzArenaAllocator(fuzz_init: FuzzContext.Init, smith: *std.testing.Smith) anyerror!void {
715fn fuzzMultiThreaded(fuzz_init: FuzzContext.Init, smith: *std.testing.Smith) anyerror!void {
715716 @disableInstrumentation();
716717 const testing = std.testing;
717
718 // We use a 'fresh' `Threaded` instance every time to reset threadlocals to
719 // their default values.
720
721 var io_instance: std.Io.Threaded = .init(fuzz_init.gpa, .{});
722 defer io_instance.deinit();
723 const io = io_instance.io();
718 const io = fuzz_init.threaded_instance.io();
724719
725720 fuzz_init.sample_instance.prepareFailures(smith);
726721
......@@ -731,59 +726,57 @@ fn fuzzArenaAllocator(fuzz_init: FuzzContext.Init, smith: *std.testing.Smith) an
731726 defer fuzz_init.arena_state.* = arena_instance.state;
732727
733728 var ctx: FuzzContext = .init(
734 io,
735729 control_allocator,
736730 arena_instance.allocator(),
737 fuzz_init.allocs,
738731 );
739732 defer ctx.deinit();
740733
741 ctx.rwl.lockUncancelable(io);
742
743734 var group: std.Io.Group = .init;
744735 defer group.cancel(io);
745736
737 var n_allocs: usize = 0;
746738 var n_actions: usize = 0;
747739 while (!smith.eosWeightedSimple(99, 1) and n_actions < FuzzContext.max_action_count) {
748740 errdefer comptime unreachable;
749741
750 const ActionTag = @typeInfo(FuzzContext.Action).@"union".tag_type.?;
751 const weights: []const testing.Smith.Weight = weights: {
752 if (ctx.allocs.len == ctx.allocs.capacity)
753 break :weights &.{
754 .value(ActionTag, .resize, 1),
755 .value(ActionTag, .remap, 1),
756 .value(ActionTag, .free, 1),
757 };
758 break :weights testing.Smith.baselineWeights(ActionTag) ++
759 .{testing.Smith.Weight.value(ActionTag, .alloc, 2)};
760 };
761 const action: FuzzContext.Action = switch (smith.valueWeighted(ActionTag, weights)) {
762 .alloc => action: {
763 const alloc_index = ctx.allocs.addOneBounded() catch continue;
764 ctx.allocs.items(.len)[alloc_index] = .free;
765 break :action .{ .alloc = .{
766 .len = nextLen(smith),
767 .alignment = smith.valueRangeAtMost(
742 const weights: []const testing.Smith.Weight = if (n_allocs == FuzzContext.max_alloc_count)
743 &.{
744 .value(FuzzContext.Action, .resize, 1),
745 .value(FuzzContext.Action, .remap, 1),
746 .value(FuzzContext.Action, .free, 1),
747 }
748 else
749 &.{
750 .value(FuzzContext.Action, .resize, 1),
751 .value(FuzzContext.Action, .remap, 1),
752 .value(FuzzContext.Action, .free, 1),
753 .value(FuzzContext.Action, .alloc, 3),
754 };
755 switch (smith.valueWeighted(FuzzContext.Action, weights)) {
756 .alloc => {
757 const alloc_index = n_allocs;
758 n_allocs += 1;
759 ctx.allocs[alloc_index].common.len = .free;
760 group.concurrent(io, FuzzContext.doOneAlloc, .{
761 &ctx,
762 nextLen(smith),
763 smith.valueRangeAtMost(
768764 Alignment,
769765 .@"1",
770766 .fromByteUnits(2 * std.heap.page_size_max),
771767 ),
772 .index = alloc_index,
773 } };
768 @enumFromInt(alloc_index),
769 }) catch unreachable;
774770 },
775 .resize => .{ .resize = .{ .new_len = nextLen(smith) } },
776 .remap => .{ .remap = .{ .new_len = nextLen(smith) } },
777 .free => .free,
778 };
779 group.concurrent(io, FuzzContext.doOneAction, .{ &ctx, action }) catch break;
771 .resize => group.concurrent(io, FuzzContext.doOneResize, .{ &ctx, nextLen(smith) }) catch unreachable,
772 .remap => group.concurrent(io, FuzzContext.doOneRemap, .{ &ctx, nextLen(smith) }) catch unreachable,
773 .free => group.concurrent(io, FuzzContext.doOneFree, .{&ctx}) catch unreachable,
774 }
780775 n_actions += 1;
781776 }
782777
783 ctx.rwl.unlock(io);
784
785778 try group.await(io);
786 try ctx.check();
779 try ctx.check(n_allocs);
787780
788781 // This also covers the `deinit` logic since `free_all` uses it internally.
789782
......@@ -806,73 +799,71 @@ fn fuzzArenaAllocator(fuzz_init: FuzzContext.Init, smith: *std.testing.Smith) an
806799 }
807800
808801 fuzz_init.control_instance.reset();
809 fuzz_init.allocs.clearRetainingCapacity();
810}
811fn nextLen(smith: *std.testing.Smith) usize {
812 @disableInstrumentation();
813 return usizeRange(smith, 1, 16 << 10 << 10);
814802}
815fn usizeRange(smith: *std.testing.Smith, at_least: usize, at_most: usize) usize {
803fn nextLen(smith: *std.testing.Smith) @typeInfo(FuzzContext.Alloc.Len).@"enum".tag_type {
816804 @disableInstrumentation();
817 const Int = @Int(.unsigned, @min(64, @bitSizeOf(usize)));
818 return smith.valueRangeAtMost(Int, @intCast(at_least), @intCast(at_most));
805 const BackingInt = @typeInfo(FuzzContext.Alloc.Len).@"enum".tag_type;
806 return smith.valueRangeAtMost(BackingInt, 1, FuzzContext.max_alloc_size);
819807}
820808
821809const FuzzContext = struct {
822 io: std.Io,
823 rwl: std.Io.RwLock,
824
825810 control_allocator: Allocator,
826811 sample_allocator: Allocator,
827812
828 allocs: *Allocs,
813 last_alloc_index: Alloc.Index,
814 allocs: [max_alloc_count]Alloc,
829815
830 const max_alloc_count = 4096;
816 const max_alloc_count = 64;
831817 const max_action_count = 2 * max_alloc_count;
832818
833 const Allocs = std.MultiArrayList(struct {
819 const max_alloc_size = 16 << 10;
820
821 const Alloc = struct {
834822 control_ptr: [*]u8,
835823 sample_ptr: [*]u8,
836 len: Len,
837 alignment: Alignment,
838 });
824 common: packed struct(usize) {
825 len: Len,
826 alignment: Alignment,
827 _: @Int(.unsigned, padding_bits) = 0,
828 },
829
830 const Len = enum(@Int(.unsigned, len_bits)) {
831 free = (1 << len_bits) - 1,
832 _,
833 };
834 const len_bits = @min(64, @bitSizeOf(usize)) - @bitSizeOf(Alignment);
835 const padding_bits = @bitSizeOf(usize) - (len_bits + @bitSizeOf(Alignment));
839836
840 const Len = enum(usize) {
841 free = std.math.maxInt(usize),
842 _,
837 const Index = enum(usize) {
838 none = std.math.maxInt(usize),
839 _,
840 };
843841 };
844842
845 const Action = union(enum(u8)) {
846 alloc: struct { len: usize, alignment: Alignment, index: usize },
847 resize: struct { new_len: usize },
848 remap: struct { new_len: usize },
843 const Action = enum {
844 alloc,
845 resize,
846 remap,
849847 free,
850848 };
851849
852 threadlocal var tls_next: u8 = 0;
853 threadlocal var tls_last_index: ?usize = null;
854
855850 const Init = struct {
856 gpa: Allocator,
857 allocs: *FuzzContext.Allocs,
851 threaded_instance: *std.Io.Threaded,
858852 arena_state: *ArenaAllocator.State,
859853 control_instance: *std.heap.FixedBufferAllocator,
860854 sample_instance: *FuzzAllocator,
861855 };
862856
863857 fn init(
864 io: std.Io,
865858 control_allocator: Allocator,
866859 sample_allocator: Allocator,
867 allocs: *Allocs,
868860 ) FuzzContext {
869861 @disableInstrumentation();
870862 return .{
871 .io = io,
872 .rwl = .init,
873863 .control_allocator = control_allocator,
874864 .sample_allocator = sample_allocator,
875 .allocs = allocs,
865 .last_alloc_index = .none,
866 .allocs = undefined,
876867 };
877868 }
878869
......@@ -881,35 +872,22 @@ const FuzzContext = struct {
881872 ctx.* = undefined;
882873 }
883874
884 fn check(ctx: *const FuzzContext) !void {
875 fn check(ctx: *const FuzzContext, n_allocs: usize) !void {
885876 @disableInstrumentation();
886 for (0..ctx.allocs.len) |index| {
887 const len: usize = switch (ctx.allocs.items(.len)[index]) {
877 for (ctx.allocs[0..n_allocs]) |allocation| {
878 const len: usize = switch (allocation.common.len) {
888879 .free => continue,
889880 _ => |len| @intFromEnum(len),
890881 };
891 const control = ctx.allocs.items(.control_ptr)[index][0..len];
892 const sample = ctx.allocs.items(.sample_ptr)[index][0..len];
882 const control = allocation.control_ptr[0..len];
883 const sample = allocation.sample_ptr[0..len];
893884 try std.testing.expectEqualSlices(u8, control, sample);
894885 }
895886 }
896887
897 fn doOneAction(ctx: *FuzzContext, action: Action) std.Io.Cancelable!void {
898 @disableInstrumentation();
899 ctx.rwl.lockSharedUncancelable(ctx.io);
900 defer ctx.rwl.unlockShared(ctx.io);
901
902 switch (action) {
903 .alloc => |act| ctx.doOneAlloc(act.len, act.alignment, act.index),
904 .resize => |act| ctx.doOneResize(act.new_len),
905 .remap => |act| ctx.doOneRemap(act.new_len),
906 .free => ctx.doOneFree(),
907 }
908 }
909
910 fn doOneAlloc(ctx: *FuzzContext, len: usize, alignment: Alignment, index: usize) void {
888 fn doOneAlloc(ctx: *FuzzContext, len: usize, alignment: Alignment, index: Alloc.Index) void {
911889 @disableInstrumentation();
912 assert(ctx.allocs.items(.len)[index] == .free);
890 assert(ctx.allocs[@intFromEnum(index)].common.len == .free);
913891
914892 const control_ptr = ctx.control_allocator.rawAlloc(len, alignment, @returnAddress()) orelse
915893 return;
......@@ -918,38 +896,42 @@ const FuzzContext = struct {
918896 return;
919897 };
920898
921 ctx.allocs.set(index, .{
899 ctx.allocs[@intFromEnum(index)] = .{
922900 .control_ptr = control_ptr,
923901 .sample_ptr = sample_ptr,
924 .len = @enumFromInt(len),
925 .alignment = alignment,
926 });
927
928 for (control_ptr[0..len], sample_ptr[0..len]) |*control, *sample| {
929 control.* = tls_next;
930 sample.* = tls_next;
931 tls_next +%= 1;
902 .common = .{
903 .len = @enumFromInt(len),
904 .alignment = alignment,
905 },
906 };
907
908 for (control_ptr[0..len], sample_ptr[0..len], 0..) |*control, *sample, i| {
909 control.* = @truncate(i);
910 sample.* = @truncate(i);
932911 }
933912
934 tls_last_index = index;
913 @atomicStore(Alloc.Index, &ctx.last_alloc_index, index, .release);
935914 }
936915 fn doOneResize(ctx: *FuzzContext, new_len: usize) void {
937916 @disableInstrumentation();
938 const index = tls_last_index orelse return;
939 const len = ctx.allocs.items(.len)[index];
940 assert(len != .free);
941 const memory = ctx.allocs.items(.sample_ptr)[index][0..@intFromEnum(len)];
942 const alignment = ctx.allocs.items(.alignment)[index];
943917
944 assert(alignment.check(@intFromPtr(ctx.allocs.items(.control_ptr)[index])));
945 assert(alignment.check(@intFromPtr(ctx.allocs.items(.sample_ptr)[index])));
918 const index = @atomicRmw(Alloc.Index, &ctx.last_alloc_index, .Xchg, .none, .acquire);
919 if (index == .none) return;
920
921 const allocation = &ctx.allocs[@intFromEnum(index)];
922 assert(allocation.common.len != .free);
923 const memory = allocation.sample_ptr[0..@intFromEnum(allocation.common.len)];
924 const alignment = allocation.common.alignment;
925
926 assert(alignment.check(@intFromPtr(allocation.control_ptr)));
927 assert(alignment.check(@intFromPtr(allocation.sample_ptr)));
946928
947929 // Since `resize` is fallible, we have to ensure that `control_allocator`
948930 // is always successful by reserving the memory we need beforehand.
949931 const new_control_ptr = ctx.control_allocator.rawAlloc(new_len, alignment, @returnAddress()) orelse
950932 return;
951933 if (ctx.sample_allocator.rawResize(memory, alignment, new_len, @returnAddress())) {
952 const old_control = ctx.allocs.items(.control_ptr)[index][0..memory.len];
934 const old_control = allocation.control_ptr[0..memory.len];
953935 const overlap = @min(memory.len, new_len);
954936 @memcpy(new_control_ptr[0..overlap], old_control[0..overlap]);
955937 ctx.control_allocator.rawFree(old_control, alignment, @returnAddress());
......@@ -958,23 +940,27 @@ const FuzzContext = struct {
958940 return;
959941 }
960942
961 ctx.allocs.set(index, .{
943 ctx.allocs[@intFromEnum(index)] = .{
962944 .control_ptr = new_control_ptr,
963945 .sample_ptr = memory.ptr,
964 .len = @enumFromInt(new_len),
965 .alignment = alignment,
966 });
946 .common = .{
947 .len = @enumFromInt(new_len),
948 .alignment = alignment,
949 },
950 };
967951
968952 if (new_len > memory.len) {
969953 for (
970 ctx.allocs.items(.control_ptr)[index][memory.len..new_len],
971 ctx.allocs.items(.sample_ptr)[index][memory.len..new_len],
972 ) |*control, *sample| {
973 control.* = tls_next;
974 sample.* = tls_next;
975 tls_next +%= 1;
954 allocation.control_ptr[memory.len..new_len],
955 allocation.sample_ptr[memory.len..new_len],
956 0..,
957 ) |*control, *sample, i| {
958 control.* = @truncate(i);
959 sample.* = @truncate(i);
976960 }
977961 }
962
963 @atomicStore(Alloc.Index, &ctx.last_alloc_index, index, .release);
978964 }
979965 fn doOneRemap(ctx: *FuzzContext, new_len: usize) void {
980966 @disableInstrumentation();
......@@ -982,26 +968,29 @@ const FuzzContext = struct {
982968 }
983969 fn doOneFree(ctx: *FuzzContext) void {
984970 @disableInstrumentation();
985 const index = tls_last_index orelse return;
986 const len = ctx.allocs.items(.len)[index];
987 assert(len != .free);
988 const memory = ctx.allocs.items(.sample_ptr)[index][0..@intFromEnum(len)];
989 const alignment = ctx.allocs.items(.alignment)[index];
990971
991 assert(alignment.check(@intFromPtr(ctx.allocs.items(.control_ptr)[index])));
992 assert(alignment.check(@intFromPtr(ctx.allocs.items(.sample_ptr)[index])));
972 const index = @atomicRmw(Alloc.Index, &ctx.last_alloc_index, .Xchg, .none, .acquire);
973 if (index == .none) return;
993974
994 ctx.control_allocator.rawFree(ctx.allocs.items(.control_ptr)[index][0..memory.len], alignment, @returnAddress());
995 ctx.sample_allocator.rawFree(ctx.allocs.items(.sample_ptr)[index][0..memory.len], alignment, @returnAddress());
975 const allocation = &ctx.allocs[@intFromEnum(index)];
976 assert(allocation.common.len != .free);
977 const len: usize = @intFromEnum(allocation.common.len);
978 const alignment = allocation.common.alignment;
996979
997 ctx.allocs.set(index, .{
980 assert(alignment.check(@intFromPtr(allocation.control_ptr)));
981 assert(alignment.check(@intFromPtr(allocation.sample_ptr)));
982
983 ctx.control_allocator.rawFree(allocation.control_ptr[0..len], alignment, @returnAddress());
984 ctx.sample_allocator.rawFree(allocation.sample_ptr[0..len], alignment, @returnAddress());
985
986 ctx.allocs[@intFromEnum(index)] = .{
998987 .control_ptr = undefined,
999988 .sample_ptr = undefined,
1000 .len = .free,
1001 .alignment = undefined,
1002 });
1003
1004 tls_last_index = null;
989 .common = .{
990 .len = .free,
991 .alignment = .@"1",
992 },
993 };
1005994 }
1006995};
1007996