| ... | ... | @@ -656,3 +656,406 @@ test "reset while retaining a buffer" { |
| 656 | 656 | try std.testing.expect(arena_allocator.state.used_list.?.next == null); |
| 657 | 657 | try std.testing.expectEqual(2, arena_allocator.queryCapacity()); |
| 658 | 658 | } |
| 659 | |
| 660 | test "fuzz" { |
| 661 | @disableInstrumentation(); |
| 662 | if (@import("builtin").single_threaded) return error.SkipZigTest; |
| 663 | |
| 664 | const gpa = std.heap.smp_allocator; |
| 665 | |
| 666 | var arena_state: ArenaAllocator.State = .init; |
| 667 | // No need to deinit arena_state, all allocations are in `sample_buffer`! |
| 668 | |
| 669 | const control_buffer = try gpa.alloc(u8, 64 << 10 << 10); |
| 670 | defer gpa.free(control_buffer); |
| 671 | var control_instance: std.heap.FixedBufferAllocator = .init(control_buffer); |
| 672 | |
| 673 | const sample_buffer = try gpa.alloc(u8, 64 << 10 << 10); |
| 674 | defer gpa.free(sample_buffer); |
| 675 | var sample_instance: FuzzAllocator = .init(sample_buffer); |
| 676 | |
| 677 | var allocs: FuzzContext.Allocs = try .initCapacity(gpa, FuzzContext.max_alloc_count); |
| 678 | defer allocs.deinit(gpa); |
| 679 | |
| 680 | try std.testing.fuzz(FuzzContext.Init{ |
| 681 | .gpa = gpa, |
| 682 | .allocs = &allocs, |
| 683 | .arena_state = &arena_state, |
| 684 | .control_instance = &control_instance, |
| 685 | .sample_instance = &sample_instance, |
| 686 | }, fuzzArenaAllocator, .{}); |
| 687 | } |
| 688 | |
| 689 | fn fuzzArenaAllocator(fuzz_init: FuzzContext.Init, smith: *std.testing.Smith) anyerror!void { |
| 690 | @disableInstrumentation(); |
| 691 | const testing = std.testing; |
| 692 | |
| 693 | // We use a 'fresh' `Threaded` instance every time to reset threadlocals to |
| 694 | // their default values. |
| 695 | |
| 696 | var io_instance: std.Io.Threaded = .init(fuzz_init.gpa, .{}); |
| 697 | defer io_instance.deinit(); |
| 698 | const io = io_instance.io(); |
| 699 | |
| 700 | fuzz_init.sample_instance.prepareFailures(smith); |
| 701 | |
| 702 | const control_allocator = fuzz_init.control_instance.threadSafeAllocator(); |
| 703 | const sample_child_allocator = fuzz_init.sample_instance.allocator(); |
| 704 | |
| 705 | var arena_instance = fuzz_init.arena_state.*.promote(sample_child_allocator); |
| 706 | defer fuzz_init.arena_state.* = arena_instance.state; |
| 707 | |
| 708 | var ctx: FuzzContext = .init( |
| 709 | io, |
| 710 | control_allocator, |
| 711 | arena_instance.allocator(), |
| 712 | fuzz_init.allocs, |
| 713 | ); |
| 714 | defer ctx.deinit(); |
| 715 | |
| 716 | ctx.rwl.lockUncancelable(io); |
| 717 | |
| 718 | var group: std.Io.Group = .init; |
| 719 | defer group.cancel(io); |
| 720 | |
| 721 | var n_actions: usize = 0; |
| 722 | while (!smith.eosWeightedSimple(99, 1) and n_actions < FuzzContext.max_action_count) { |
| 723 | errdefer comptime unreachable; |
| 724 | |
| 725 | const ActionTag = @typeInfo(FuzzContext.Action).@"union".tag_type.?; |
| 726 | const weights: []const testing.Smith.Weight = weights: { |
| 727 | if (ctx.allocs.len == ctx.allocs.capacity) |
| 728 | break :weights &.{ |
| 729 | .value(ActionTag, .resize, 1), |
| 730 | .value(ActionTag, .remap, 1), |
| 731 | .value(ActionTag, .free, 1), |
| 732 | }; |
| 733 | break :weights testing.Smith.baselineWeights(ActionTag) ++ |
| 734 | .{testing.Smith.Weight.value(ActionTag, .alloc, 2)}; |
| 735 | }; |
| 736 | const action: FuzzContext.Action = switch (smith.valueWeighted(ActionTag, weights)) { |
| 737 | .alloc => action: { |
| 738 | const alloc_index = ctx.allocs.addOneBounded() catch continue; |
| 739 | ctx.allocs.items(.len)[alloc_index] = .free; |
| 740 | break :action .{ .alloc = .{ |
| 741 | .len = nextLen(smith), |
| 742 | .alignment = smith.valueRangeAtMost( |
| 743 | Alignment, |
| 744 | .@"1", |
| 745 | .fromByteUnits(2 * std.heap.page_size_max), |
| 746 | ), |
| 747 | .index = alloc_index, |
| 748 | } }; |
| 749 | }, |
| 750 | .resize => .{ .resize = .{ .new_len = nextLen(smith) } }, |
| 751 | .remap => .{ .remap = .{ .new_len = nextLen(smith) } }, |
| 752 | .free => .free, |
| 753 | }; |
| 754 | group.concurrent(io, FuzzContext.doOneAction, .{ &ctx, action }) catch break; |
| 755 | n_actions += 1; |
| 756 | } |
| 757 | |
| 758 | ctx.rwl.unlock(io); |
| 759 | |
| 760 | try group.await(io); |
| 761 | try ctx.check(); |
| 762 | |
| 763 | // This also covers the `deinit` logic since `free_all` uses it internally. |
| 764 | |
| 765 | const old_capacity = arena_instance.queryCapacity(); |
| 766 | const reset_mode: ResetMode = switch (smith.value(@typeInfo(ResetMode).@"union".tag_type.?)) { |
| 767 | .free_all => .free_all, |
| 768 | .retain_capacity => .retain_capacity, |
| 769 | .retain_with_limit => .{ .retain_with_limit = smith.value(usize) }, |
| 770 | }; |
| 771 | const ok = arena_instance.reset(reset_mode); |
| 772 | const new_capacity = arena_instance.queryCapacity(); |
| 773 | switch (reset_mode) { |
| 774 | .free_all => { |
| 775 | try testing.expect(ok); |
| 776 | try testing.expectEqual(0, new_capacity); |
| 777 | fuzz_init.sample_instance.reset(); |
| 778 | }, |
| 779 | .retain_with_limit => |limit| if (ok) try testing.expect(new_capacity <= limit), |
| 780 | .retain_capacity => if (ok) try testing.expectEqual(old_capacity, new_capacity), |
| 781 | } |
| 782 | |
| 783 | fuzz_init.control_instance.reset(); |
| 784 | fuzz_init.allocs.clearRetainingCapacity(); |
| 785 | } |
| 786 | fn nextLen(smith: *std.testing.Smith) usize { |
| 787 | @disableInstrumentation(); |
| 788 | return usizeRange(smith, 1, 16 << 10 << 10); |
| 789 | } |
| 790 | fn usizeRange(smith: *std.testing.Smith, at_least: usize, at_most: usize) usize { |
| 791 | @disableInstrumentation(); |
| 792 | const Int = @Int(.unsigned, @min(64, @bitSizeOf(usize))); |
| 793 | return smith.valueRangeAtMost(Int, @intCast(at_least), @intCast(at_most)); |
| 794 | } |
| 795 | |
| 796 | const FuzzContext = struct { |
| 797 | io: std.Io, |
| 798 | rwl: std.Io.RwLock, |
| 799 | |
| 800 | control_allocator: Allocator, |
| 801 | sample_allocator: Allocator, |
| 802 | |
| 803 | allocs: *Allocs, |
| 804 | |
| 805 | const max_alloc_count = 4096; |
| 806 | const max_action_count = 2 * max_alloc_count; |
| 807 | |
| 808 | const Allocs = std.MultiArrayList(struct { |
| 809 | control_ptr: [*]u8, |
| 810 | sample_ptr: [*]u8, |
| 811 | len: Len, |
| 812 | alignment: Alignment, |
| 813 | }); |
| 814 | |
| 815 | const Len = enum(usize) { |
| 816 | free = std.math.maxInt(usize), |
| 817 | _, |
| 818 | }; |
| 819 | |
| 820 | const Action = union(enum(u8)) { |
| 821 | alloc: struct { len: usize, alignment: Alignment, index: usize }, |
| 822 | resize: struct { new_len: usize }, |
| 823 | remap: struct { new_len: usize }, |
| 824 | free, |
| 825 | }; |
| 826 | |
| 827 | threadlocal var tls_next: u8 = 0; |
| 828 | threadlocal var tls_last_index: ?usize = null; |
| 829 | |
| 830 | const Init = struct { |
| 831 | gpa: Allocator, |
| 832 | allocs: *FuzzContext.Allocs, |
| 833 | arena_state: *ArenaAllocator.State, |
| 834 | control_instance: *std.heap.FixedBufferAllocator, |
| 835 | sample_instance: *FuzzAllocator, |
| 836 | }; |
| 837 | |
| 838 | fn init( |
| 839 | io: std.Io, |
| 840 | control_allocator: Allocator, |
| 841 | sample_allocator: Allocator, |
| 842 | allocs: *Allocs, |
| 843 | ) FuzzContext { |
| 844 | @disableInstrumentation(); |
| 845 | return .{ |
| 846 | .io = io, |
| 847 | .rwl = .init, |
| 848 | .control_allocator = control_allocator, |
| 849 | .sample_allocator = sample_allocator, |
| 850 | .allocs = allocs, |
| 851 | }; |
| 852 | } |
| 853 | |
| 854 | fn deinit(ctx: *FuzzContext) void { |
| 855 | @disableInstrumentation(); |
| 856 | ctx.* = undefined; |
| 857 | } |
| 858 | |
| 859 | fn check(ctx: *const FuzzContext) !void { |
| 860 | @disableInstrumentation(); |
| 861 | for (0..ctx.allocs.len) |index| { |
| 862 | const len: usize = switch (ctx.allocs.items(.len)[index]) { |
| 863 | .free => continue, |
| 864 | _ => |len| @intFromEnum(len), |
| 865 | }; |
| 866 | const control = ctx.allocs.items(.control_ptr)[index][0..len]; |
| 867 | const sample = ctx.allocs.items(.sample_ptr)[index][0..len]; |
| 868 | try std.testing.expectEqualSlices(u8, control, sample); |
| 869 | } |
| 870 | } |
| 871 | |
| 872 | fn doOneAction(ctx: *FuzzContext, action: Action) std.Io.Cancelable!void { |
| 873 | @disableInstrumentation(); |
| 874 | ctx.rwl.lockSharedUncancelable(ctx.io); |
| 875 | defer ctx.rwl.unlockShared(ctx.io); |
| 876 | |
| 877 | switch (action) { |
| 878 | .alloc => |act| ctx.doOneAlloc(act.len, act.alignment, act.index), |
| 879 | .resize => |act| ctx.doOneResize(act.new_len), |
| 880 | .remap => |act| ctx.doOneRemap(act.new_len), |
| 881 | .free => ctx.doOneFree(), |
| 882 | } |
| 883 | } |
| 884 | |
| 885 | fn doOneAlloc(ctx: *FuzzContext, len: usize, alignment: Alignment, index: usize) void { |
| 886 | @disableInstrumentation(); |
| 887 | assert(ctx.allocs.items(.len)[index] == .free); |
| 888 | |
| 889 | const control_ptr = ctx.control_allocator.rawAlloc(len, alignment, @returnAddress()) orelse |
| 890 | return; |
| 891 | const sample_ptr = ctx.sample_allocator.rawAlloc(len, alignment, @returnAddress()) orelse { |
| 892 | ctx.control_allocator.rawFree(control_ptr[0..len], alignment, @returnAddress()); |
| 893 | return; |
| 894 | }; |
| 895 | |
| 896 | ctx.allocs.set(index, .{ |
| 897 | .control_ptr = control_ptr, |
| 898 | .sample_ptr = sample_ptr, |
| 899 | .len = @enumFromInt(len), |
| 900 | .alignment = alignment, |
| 901 | }); |
| 902 | |
| 903 | for (control_ptr[0..len], sample_ptr[0..len]) |*control, *sample| { |
| 904 | control.* = tls_next; |
| 905 | sample.* = tls_next; |
| 906 | tls_next +%= 1; |
| 907 | } |
| 908 | |
| 909 | tls_last_index = index; |
| 910 | } |
| 911 | fn doOneResize(ctx: *FuzzContext, new_len: usize) void { |
| 912 | @disableInstrumentation(); |
| 913 | const index = tls_last_index orelse return; |
| 914 | const len = ctx.allocs.items(.len)[index]; |
| 915 | assert(len != .free); |
| 916 | const memory = ctx.allocs.items(.sample_ptr)[index][0..@intFromEnum(len)]; |
| 917 | const alignment = ctx.allocs.items(.alignment)[index]; |
| 918 | |
| 919 | assert(alignment.check(@intFromPtr(ctx.allocs.items(.control_ptr)[index]))); |
| 920 | assert(alignment.check(@intFromPtr(ctx.allocs.items(.sample_ptr)[index]))); |
| 921 | |
| 922 | // Since `resize` is fallible, we have to ensure that `control_allocator` |
| 923 | // is always successful by reserving the memory we need beforehand. |
| 924 | const new_control_ptr = ctx.control_allocator.rawAlloc(new_len, alignment, @returnAddress()) orelse |
| 925 | return; |
| 926 | if (ctx.sample_allocator.rawResize(memory, alignment, new_len, @returnAddress())) { |
| 927 | const old_control = ctx.allocs.items(.control_ptr)[index][0..memory.len]; |
| 928 | const overlap = @min(memory.len, new_len); |
| 929 | @memcpy(new_control_ptr[0..overlap], old_control[0..overlap]); |
| 930 | ctx.control_allocator.rawFree(old_control, alignment, @returnAddress()); |
| 931 | } else { |
| 932 | ctx.control_allocator.rawFree(new_control_ptr[0..new_len], alignment, @returnAddress()); |
| 933 | return; |
| 934 | } |
| 935 | |
| 936 | ctx.allocs.set(index, .{ |
| 937 | .control_ptr = new_control_ptr, |
| 938 | .sample_ptr = memory.ptr, |
| 939 | .len = @enumFromInt(new_len), |
| 940 | .alignment = alignment, |
| 941 | }); |
| 942 | |
| 943 | if (new_len > memory.len) { |
| 944 | for ( |
| 945 | ctx.allocs.items(.control_ptr)[index][memory.len..new_len], |
| 946 | ctx.allocs.items(.sample_ptr)[index][memory.len..new_len], |
| 947 | ) |*control, *sample| { |
| 948 | control.* = tls_next; |
| 949 | sample.* = tls_next; |
| 950 | tls_next +%= 1; |
| 951 | } |
| 952 | } |
| 953 | } |
| 954 | fn doOneRemap(ctx: *FuzzContext, new_len: usize) void { |
| 955 | @disableInstrumentation(); |
| 956 | return doOneResize(ctx, new_len); |
| 957 | } |
| 958 | fn doOneFree(ctx: *FuzzContext) void { |
| 959 | @disableInstrumentation(); |
| 960 | const index = tls_last_index orelse return; |
| 961 | const len = ctx.allocs.items(.len)[index]; |
| 962 | assert(len != .free); |
| 963 | const memory = ctx.allocs.items(.sample_ptr)[index][0..@intFromEnum(len)]; |
| 964 | const alignment = ctx.allocs.items(.alignment)[index]; |
| 965 | |
| 966 | assert(alignment.check(@intFromPtr(ctx.allocs.items(.control_ptr)[index]))); |
| 967 | assert(alignment.check(@intFromPtr(ctx.allocs.items(.sample_ptr)[index]))); |
| 968 | |
| 969 | ctx.control_allocator.rawFree(ctx.allocs.items(.control_ptr)[index][0..memory.len], alignment, @returnAddress()); |
| 970 | ctx.sample_allocator.rawFree(ctx.allocs.items(.sample_ptr)[index][0..memory.len], alignment, @returnAddress()); |
| 971 | |
| 972 | ctx.allocs.set(index, .{ |
| 973 | .control_ptr = undefined, |
| 974 | .sample_ptr = undefined, |
| 975 | .len = .free, |
| 976 | .alignment = undefined, |
| 977 | }); |
| 978 | |
| 979 | tls_last_index = null; |
| 980 | } |
| 981 | }; |
| 982 | |
| 983 | const FuzzAllocator = struct { |
| 984 | fba: std.heap.FixedBufferAllocator, |
| 985 | spurious_failures: [256]u8, |
| 986 | index: u8, |
| 987 | |
| 988 | fn init(buffer: []u8) FuzzAllocator { |
| 989 | @disableInstrumentation(); |
| 990 | return .{ |
| 991 | .fba = .init(buffer), |
| 992 | .spurious_failures = undefined, // set with `preprepareFailures` |
| 993 | .index = 0, |
| 994 | }; |
| 995 | } |
| 996 | |
| 997 | fn prepareFailures(fa: *FuzzAllocator, smith: *std.testing.Smith) void { |
| 998 | @disableInstrumentation(); |
| 999 | const bool_weights: []const std.testing.Smith.Weight = &.{ |
| 1000 | .value(u8, 0, 10), |
| 1001 | .value(u8, 1, 1), |
| 1002 | }; |
| 1003 | smith.bytesWeighted(&fa.spurious_failures, bool_weights); |
| 1004 | fa.index = 0; |
| 1005 | } |
| 1006 | |
| 1007 | fn reset(fa: *FuzzAllocator) void { |
| 1008 | @disableInstrumentation(); |
| 1009 | fa.fba.reset(); |
| 1010 | } |
| 1011 | |
| 1012 | fn allocator(fa: *FuzzAllocator) Allocator { |
| 1013 | @disableInstrumentation(); |
| 1014 | return .{ |
| 1015 | .ptr = fa, |
| 1016 | .vtable = &.{ |
| 1017 | .alloc = FuzzAllocator.alloc, |
| 1018 | .resize = FuzzAllocator.resize, |
| 1019 | .remap = FuzzAllocator.remap, |
| 1020 | .free = FuzzAllocator.free, |
| 1021 | }, |
| 1022 | }; |
| 1023 | } |
| 1024 | |
| 1025 | fn alloc(ctx: *anyopaque, len: usize, alignment: Alignment, ret_addr: usize) ?[*]u8 { |
| 1026 | @disableInstrumentation(); |
| 1027 | const fa: *FuzzAllocator = @ptrCast(@alignCast(ctx)); |
| 1028 | _ = ret_addr; |
| 1029 | |
| 1030 | const index = @atomicRmw(u8, &fa.index, .Add, 1, .monotonic); |
| 1031 | if (fa.spurious_failures[index] != 0) return null; |
| 1032 | return fa.fba.threadSafeAllocator().rawAlloc(len, alignment, @returnAddress()); |
| 1033 | } |
| 1034 | |
| 1035 | fn resize(ctx: *anyopaque, memory: []u8, alignment: Alignment, new_len: usize, ret_addr: usize) bool { |
| 1036 | @disableInstrumentation(); |
| 1037 | const fa: *FuzzAllocator = @ptrCast(@alignCast(ctx)); |
| 1038 | _ = ret_addr; |
| 1039 | |
| 1040 | const index = @atomicRmw(u8, &fa.index, .Add, 1, .monotonic); |
| 1041 | if (fa.spurious_failures[index] != 0) return false; |
| 1042 | return fa.fba.threadSafeAllocator().rawResize(memory, alignment, new_len, @returnAddress()); |
| 1043 | } |
| 1044 | |
| 1045 | fn remap(ctx: *anyopaque, memory: []u8, alignment: Alignment, new_len: usize, ret_addr: usize) ?[*]u8 { |
| 1046 | @disableInstrumentation(); |
| 1047 | const fa: *FuzzAllocator = @ptrCast(@alignCast(ctx)); |
| 1048 | _ = ret_addr; |
| 1049 | |
| 1050 | const index = @atomicRmw(u8, &fa.index, .Add, 1, .monotonic); |
| 1051 | if (fa.spurious_failures[index] != 0) return null; |
| 1052 | return fa.fba.threadSafeAllocator().rawRemap(memory, alignment, new_len, @returnAddress()); |
| 1053 | } |
| 1054 | |
| 1055 | fn free(ctx: *anyopaque, memory: []u8, alignment: Alignment, ret_addr: usize) void { |
| 1056 | @disableInstrumentation(); |
| 1057 | const fa: *FuzzAllocator = @ptrCast(@alignCast(ctx)); |
| 1058 | _ = ret_addr; |
| 1059 | return fa.fba.threadSafeAllocator().rawFree(memory, alignment, @returnAddress()); |
| 1060 | } |
| 1061 | }; |