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 }