| ... | @@ -656,3 +656,406 @@ test "reset while retaining a buffer" { | ... | @@ -656,3 +656,406 @@ test "reset while retaining a buffer" { |
| 656 | try std.testing.expect(arena_allocator.state.used_list.?.next == null); | 656 | try std.testing.expect(arena_allocator.state.used_list.?.next == null); |
| 657 | try std.testing.expectEqual(2, arena_allocator.queryCapacity()); | 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 | }; |