lib/stabilizer/src/properties/runtime.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const hypothesis = @import("hypothesis");
  3 const stabilizer = @import("stabilizer");
  4 
  5 const Allocator = std.mem.Allocator;
  6 
  7 const ConstructorFn = stabilizer.ConstructorFn;
  8 const FunctionEntryState = stabilizer.FunctionEntryState;
  9 const FunctionId = stabilizer.FunctionId;
 10 const Marsaglia = stabilizer.Marsaglia;
 11 const Runtime = stabilizer.Runtime;
 12 
 13 fn settings() hypothesis.Settings {
 14     return hypothesis.Settings.quick()
 15         .withSeed(0x57ab_11e5)
 16         .withDatabase("zig-out/hypothesis-failures/stabilizer");
 17 }
 18 
 19 fn drawUsize(data: *hypothesis.ConjectureData, min: usize, max: usize, shrink_towards: usize) !usize {
 20     return @intCast(try data.drawInteger(
 21         @intCast(min),
 22         @intCast(max),
 23         @intCast(shrink_towards),
 24     ));
 25 }
 26 
 27 var constructor_trace: [64]u8 = undefined;
 28 var constructor_trace_len: usize = 0;
 29 
 30 fn resetConstructorTrace() void {
 31     @memset(&constructor_trace, 0);
 32     constructor_trace_len = 0;
 33 }
 34 
 35 fn appendConstructorTrace(value: u8) void {
 36     constructor_trace[constructor_trace_len] = value;
 37     constructor_trace_len += 1;
 38 }
 39 
 40 fn traceConstructor0() callconv(.c) void {
 41     appendConstructorTrace(0);
 42 }
 43 
 44 fn traceConstructor1() callconv(.c) void {
 45     appendConstructorTrace(1);
 46 }
 47 
 48 fn traceConstructor2() callconv(.c) void {
 49     appendConstructorTrace(2);
 50 }
 51 
 52 fn traceConstructor3() callconv(.c) void {
 53     appendConstructorTrace(3);
 54 }
 55 
 56 fn traceProgramMain() u8 {
 57     appendConstructorTrace(9);
 58     return 42;
 59 }
 60 
 61 const trace_constructors = [_]ConstructorFn{
 62     traceConstructor0,
 63     traceConstructor1,
 64     traceConstructor2,
 65     traceConstructor3,
 66 };
 67 
 68 const StackPadRegistryProperty = struct {
 69     pub fn property(data: *hypothesis.ConjectureData, allocator: Allocator) !void {
 70         const seed = try data.drawInteger(1, std.math.maxInt(u32), 1);
 71         const entries = try drawUsize(data, 1, 8, 1);
 72         const registered = try drawUsize(data, 0, 8, 2);
 73 
 74         var expected_rng = Marsaglia.init(seed);
 75         var index: usize = 0;
 76         while (index < entries) : (index += 1) _ = expected_rng.nextByte();
 77 
 78         var runtime = try Runtime.init(allocator, .{
 79             .seed = seed,
 80             .stack = .{ .entries = entries },
 81             .code = .{ .enabled = false },
 82         });
 83         defer runtime.deinit();
 84 
 85         var slots = @as([8]u8, @splat(0));
 86         index = 0;
 87         while (index < registered) : (index += 1) {
 88             try runtime.registerStackPad(&slots[index]);
 89             try runtime.registerStackPad(&slots[index]);
 90         }
 91         try std.testing.expectEqual(registered, runtime.registeredStackPadCount());
 92 
 93         var expected_entries: [8]u8 = undefined;
 94         index = 0;
 95         while (index < entries) : (index += 1) expected_entries[index] = expected_rng.nextByte();
 96         var expected_slots = @as([8]u8, @splat(0));
 97         index = 0;
 98         while (index < registered) : (index += 1) expected_slots[index] = expected_rng.nextByte();
 99 
100         runtime.rerandomize();
101 
102         index = 0;
103         while (index < entries) : (index += 1) {
104             try std.testing.expectEqual(expected_entries[index], runtime.nextStackPad().unit);
105         }
106         index = 0;
107         while (index < slots.len) : (index += 1) {
108             try std.testing.expectEqual(expected_slots[index], slots[index]);
109         }
110     }
111 };
112 
113 test "pbt: registered stack pad slots follow stack-only rerandomization" {
114     try hypothesis.checkNamed(StackPadRegistryProperty, "stabilizer-stack-pad-registry", settings());
115 }
116 
117 const ConstructorRegistryProperty = struct {
118     pub fn property(data: *hypothesis.ConjectureData, allocator: Allocator) !void {
119         resetConstructorTrace();
120 
121         var runtime = try Runtime.init(allocator, .{});
122         defer runtime.deinit();
123 
124         const count = try drawUsize(data, 0, 32, 0);
125         var expected: [32]u8 = undefined;
126         var index: usize = 0;
127         while (index < count) : (index += 1) {
128             const slot = try drawUsize(data, 0, trace_constructors.len - 1, 0);
129             expected[index] = @intCast(slot);
130             try runtime.registerConstructor(trace_constructors[slot]);
131         }
132         try std.testing.expectEqual(count, runtime.constructorCount());
133 
134         runtime.runConstructors();
135 
136         try std.testing.expectEqual(count, constructor_trace_len);
137         try std.testing.expectEqualSlices(u8, expected[0..count], constructor_trace[0..constructor_trace_len]);
138     }
139 };
140 
141 test "pbt: constructor registry preserves insertion order" {
142     try hypothesis.checkNamed(ConstructorRegistryProperty, "stabilizer-constructors", settings());
143 }
144 
145 const RuntimeLifecycleProperty = struct {
146     pub fn property(data: *hypothesis.ConjectureData, allocator: Allocator) !void {
147         resetConstructorTrace();
148 
149         var runtime = try Runtime.init(allocator, .{
150             .code = .{ .enabled = true },
151         });
152         defer runtime.deinit();
153 
154         const functions = try drawUsize(data, 1, 8, 2);
155         var ids: [8]FunctionId = undefined;
156         var index: usize = 0;
157         while (index < functions) : (index += 1) {
158             ids[index] = try runtime.code.registerFunction(.{
159                 .code_size = try drawUsize(data, 1, 256, 64),
160             });
161             if (try data.drawBoolean()) _ = try runtime.enterFunction(ids[index], &.{});
162         }
163 
164         const constructor_count = try drawUsize(data, 0, 16, 0);
165         var expected: [17]u8 = undefined;
166         index = 0;
167         while (index < constructor_count) : (index += 1) {
168             const slot = try drawUsize(data, 0, trace_constructors.len - 1, 0);
169             expected[index] = @intCast(slot);
170             try runtime.registerConstructor(trace_constructors[slot]);
171         }
172         expected[constructor_count] = 9;
173 
174         try std.testing.expectEqual(@as(u8, 42), runtime.runProgram(traceProgramMain, .{}));
175 
176         index = 0;
177         while (index < functions) : (index += 1) {
178             try std.testing.expectEqual(FunctionEntryState.trap, runtime.code.entryState(ids[index]).?);
179         }
180         try std.testing.expectEqual(@as(usize, 0), runtime.code.live.items.len);
181         try std.testing.expect(!runtime.code.rerandomizing);
182         try std.testing.expectEqualSlices(u8, expected[0 .. constructor_count + 1], constructor_trace[0..constructor_trace_len]);
183     }
184 };
185 
186 test "pbt: runtime lifecycle arms code before constructors and main" {
187     try hypothesis.checkNamed(RuntimeLifecycleProperty, "stabilizer-runtime-lifecycle", settings());
188 }
189 
190 const RuntimeRerandomizeProperty = struct {
191     pub fn property(data: *hypothesis.ConjectureData, allocator: Allocator) !void {
192         var runtime = try Runtime.init(allocator, .{
193             .code = .{ .enabled = true },
194         });
195         defer runtime.deinit();
196 
197         const functions = try drawUsize(data, 1, 8, 2);
198         var ids: [8]FunctionId = undefined;
199         var index: usize = 0;
200         while (index < functions) : (index += 1) {
201             ids[index] = try runtime.code.registerFunction(.{
202                 .code_size = try drawUsize(data, 1, 256, 64),
203             });
204         }
205 
206         const active_index = try drawUsize(data, 0, functions - 1, 0);
207         const active_base = (try runtime.enterFunction(ids[active_index], &.{})).?;
208         const active_original = runtime.code.originalBase(ids[active_index]).?;
209         try std.testing.expectEqual(active_base, runtime.rerandomizeAt(active_original));
210 
211         index = 0;
212         while (index < functions) : (index += 1) {
213             try std.testing.expectEqual(FunctionEntryState.trap, runtime.code.entryState(ids[index]).?);
214         }
215         try std.testing.expect(runtime.code.rerandomizing);
216         try std.testing.expectEqual(@as(usize, 0), runtime.code.live.items.len);
217 
218         const non_entry = active_original + try drawUsize(data, 1, 128, 1);
219         try std.testing.expectEqual(non_entry, runtime.rerandomizeAt(non_entry));
220     }
221 };
222 
223 test "pbt: runtime rerandomizeAt forwards only live function entries" {
224     try hypothesis.checkNamed(RuntimeRerandomizeProperty, "stabilizer-runtime-rerandomize-at", settings());
225 }
226 
227 const RuntimeAddressAdjustmentProperty = struct {
228     pub fn property(data: *hypothesis.ConjectureData, allocator: Allocator) !void {
229         var runtime = try Runtime.init(allocator, .{
230             .code = .{ .shuffle_slots = 16 },
231         });
232         defer runtime.deinit();
233 
234         const function = try runtime.code.registerFunction(.{
235             .code_size = try drawUsize(data, 1, 512, 64),
236             .table_size = try drawUsize(data, 0, 128, 0),
237             .table_adjacent = try data.drawBoolean(),
238         });
239         const original = runtime.code.originalBase(function).?;
240         const allocation_size = runtime.code.allocationSize(function).?;
241         const relocations = try drawUsize(data, 1, 6, 2);
242         var bases: [6]usize = undefined;
243         var index: usize = 0;
244         while (index < relocations) : (index += 1) {
245             const location = try runtime.code.relocate(function);
246             bases[index] = location.base;
247         }
248 
249         const offset = try drawUsize(data, 0, allocation_size - 1, 0);
250         index = 0;
251         while (index < relocations) : (index += 1) {
252             try std.testing.expectEqual(original + offset, runtime.adjustAddress(bases[index] + offset));
253         }
254 
255         const unrelated = bases[relocations - 1] + allocation_size + try drawUsize(data, 1, 4096, 1);
256         try std.testing.expectEqual(unrelated, runtime.adjustAddress(unrelated));
257 
258         var real_backtrace: [7]usize = undefined;
259         var expected_backtrace: [7]usize = undefined;
260         index = 0;
261         while (index < relocations) : (index += 1) {
262             real_backtrace[index] = bases[index] + offset;
263             expected_backtrace[index] = original + offset;
264         }
265         real_backtrace[relocations] = unrelated;
266         expected_backtrace[relocations] = unrelated;
267 
268         var adjusted_backtrace: [7]usize = undefined;
269         const adjusted = runtime.adjustBacktrace(adjusted_backtrace[0..], real_backtrace[0 .. relocations + 1]);
270         try std.testing.expectEqual(relocations + 1, adjusted.len);
271         try std.testing.expectEqualSlices(usize, expected_backtrace[0 .. relocations + 1], adjusted);
272     }
273 };
274 
275 test "pbt: runtime address adjustment preserves original code offsets" {
276     try hypothesis.checkNamed(RuntimeAddressAdjustmentProperty, "stabilizer-runtime-address-adjustment", settings());
277 }