@@ -2,26 +2,161 @@ const std = @import("std");
2
2
const builtin = @import ("builtin" );
3
3
const debug = std .debug ;
4
4
const testing = std .testing ;
5
+ const posix = std .posix ;
6
+ const native_arch = builtin .cpu .arch ;
7
+ const native_os = builtin .os .tag ;
8
+ const link_libc = builtin .link_libc ;
5
9
6
- noinline fn frame3 (expected : * [4 ]usize , unwound : * [4 ]usize ) void {
7
- expected [0 ] = @returnAddress ();
10
+ const max_stack_trace_depth = 32 ;
11
+
12
+ const do_signal = switch (native_os ) {
13
+ .wasi , .windows = > false ,
14
+ else = > true ,
15
+ };
16
+
17
+ var installed_signal_handler = false ;
18
+ var handled_signal = false ;
19
+ var captured_frames = false ;
20
+
21
+ const AddrArray = std .BoundedArray (usize , max_stack_trace_depth );
22
+
23
+ // Global variables to capture different stack traces in. Compared at the end of main() against "expected" array
24
+ var signal_frames : AddrArray = undefined ;
25
+ var full_frames : AddrArray = undefined ;
26
+ var skip_frames : AddrArray = undefined ;
27
+
28
+ // StackIterator is the core of this test, but still worth executing the dumpCurrentStackTrace* functions
29
+ // (These platforms don't fail on StackIterator, they just return empty traces.)
30
+ const supports_stack_iterator =
31
+ (native_os != .windows ) and // StackIterator is (currently?) POSIX/DWARF centered.
32
+ ! native_arch .isWasm (); // wasm has no introspection
33
+
34
+ // Getting the backtrace inside the signal handler (with the ucontext_t)
35
+ // gets stuck in a loop on some systems:
36
+ const expect_signal_frame_overflow =
37
+ (native_arch .isArm () and link_libc ) or // loops above main()
38
+ native_arch .isAARCH64 (); // non-deterministic, sometimes overflows, sometimes not
39
+
40
+ // Getting the backtrace inside the signal handler (with the ucontext_t)
41
+ // does not contain the expected content on some systems:
42
+ const expect_signal_frame_useless =
43
+ (native_arch == .x86_64 and link_libc and builtin .abi .isGnu ()) or // stuck on pthread_kill?
44
+ (native_arch == .x86_64 and link_libc and builtin .abi .isMusl () and builtin .omit_frame_pointer ) or // immediately confused backtrace
45
+ (native_arch == .x86_64 and builtin .os .tag .isDarwin ()) or // immediately confused backtrace
46
+ native_arch .isAARCH64 () or // non-deterministic, sometimes overflows, sometimes confused
47
+ native_arch .isRISCV () or // `ucontext_t` not defined yet
48
+ native_arch .isMIPS () or // Missing ucontext_t. Most stack traces are empty ... (with or without libc)
49
+ native_arch .isPowerPC () or // dumpCurrent* useless, StackIterator empty, ctx-based trace empty (with or without libc)
50
+ (native_arch .isThumb () and ! link_libc ); // stops on first element of trace
8
51
9
- var context : debug.ThreadContext = undefined ;
10
- testing .expect (debug .getContext (& context )) catch @panic ("failed to getContext" );
52
+ // Signal handler to gather stack traces from the given signal context.
53
+ fn testFromSigUrg (sig : i32 , info : * const posix.siginfo_t , ctx_ptr : ? * anyopaque ) callconv (.c ) void {
54
+ // std.debug.print("sig={} info={*} ctx_ptr={*}\n", .{ sig, info, ctx_ptr });
55
+ _ = info ;
56
+ _ = sig ;
57
+
58
+ var ctx : * posix.ucontext_t = undefined ;
59
+ var local_ctx : posix.ucontext_t = undefined ;
60
+
61
+ // Darwin kernels don't align `ctx_ptr` properly. Handle this defensively.
62
+ if (builtin .os .tag .isDarwin () and builtin .cpu .arch == .aarch64 ) {
63
+ const align_ctx : * align (1 ) posix.ucontext_t = @ptrCast (ctx_ptr );
64
+ local_ctx = align_ctx .* ;
65
+
66
+ // The kernel incorrectly writes the contents of `__mcontext_data` right after `mcontext`,
67
+ // rather than after the 8 bytes of padding that are supposed to sit between the two. Copy the
68
+ // contents to the right place so that the `mcontext` pointer will be correct after the
69
+ // `relocateContext` call below.
70
+ local_ctx .__mcontext_data = @as (* align (1 ) extern struct {
71
+ onstack : c_int ,
72
+ sigmask : std.c.sigset_t ,
73
+ stack : std.c.stack_t ,
74
+ link : ? * std.c.ucontext_t ,
75
+ mcsize : u64 ,
76
+ mcontext : * std.c.mcontext_t ,
77
+ __mcontext_data : std .c .mcontext_t align (@sizeOf (usize )), // Disable padding after `mcontext`.
78
+ }, @ptrCast (align_ctx )).__mcontext_data ;
79
+
80
+ debug .relocateContext (& local_ctx );
81
+ ctx = & local_ctx ;
82
+ } else {
83
+ ctx = @ptrCast (@alignCast (ctx_ptr ));
84
+ }
85
+
86
+ std .debug .print ("(from signal handler) dumpStackTraceFromBase({*} => {*}):\n " , .{ ctx_ptr , ctx });
87
+ debug .dumpStackTraceFromBase (ctx );
11
88
12
89
const debug_info = debug .getSelfDebugInfo () catch @panic ("failed to openSelfDebugInfo" );
13
- var it = debug .StackIterator .initWithContext (expected [ 0 ] , debug_info , & context ) catch @panic ("failed to initWithContext" );
14
- defer it .deinit ();
90
+ var sig_it = debug .StackIterator .initWithContext (null , debug_info , ctx ) catch @panic ("failed StackIterator. initWithContext" );
91
+ defer sig_it .deinit ();
15
92
16
- for (unwound ) | * addr | {
17
- if (it .next ()) | return_address | addr .* = return_address ;
93
+ // Save the backtrace from 'ctx' into the 'signal_frames' array
94
+ while (sig_it .next ()) | return_address | {
95
+ signal_frames .append (return_address ) catch @panic ("signal_frames.append()" );
96
+ if (signal_frames .len == signal_frames .capacity ()) break ;
18
97
}
98
+
99
+ handled_signal = true ;
19
100
}
20
101
21
- noinline fn frame2 (expected : * [4 ]usize , unwound : * [4 ]usize ) void {
102
+ // Leaf test function. Gather backtraces for comparison with "expected".
103
+ noinline fn frame3 (expected : * [4 ]usize ) void {
104
+ expected [0 ] = @returnAddress ();
105
+
106
+ // Test the print-current-stack trace functions
107
+ std .debug .print ("dumpCurrentStackTrace(null):\n " , .{});
108
+ debug .dumpCurrentStackTrace (null );
109
+
110
+ std .debug .print ("dumpCurrentStackTrace({x}):\n " , .{expected [0 ]});
111
+ debug .dumpCurrentStackTrace (expected [0 ]);
112
+
113
+ // Trigger signal handler here and see that it's ctx is a viable start for unwinding
114
+ if (do_signal and installed_signal_handler ) {
115
+ posix .raise (posix .SIG .URG ) catch @panic ("failed to raise posix.SIG.URG" );
116
+ }
117
+
118
+ // Capture stack traces directly, two ways, if supported
119
+ if (std .debug .ThreadContext != void and native_os != .windows ) {
120
+ var context : debug.ThreadContext = undefined ;
121
+
122
+ const gotContext = debug .getContext (& context );
123
+
124
+ if (! std .debug .have_getcontext ) {
125
+ testing .expectEqual (false , gotContext ) catch @panic ("getContext unexpectedly succeeded" );
126
+ } else {
127
+ testing .expectEqual (true , gotContext ) catch @panic ("failed to getContext" );
128
+
129
+ const debug_info = debug .getSelfDebugInfo () catch @panic ("failed to openSelfDebugInfo" );
130
+
131
+ // Run the "full" iterator
132
+ testing .expect (debug .getContext (& context )) catch @panic ("failed to getContext" );
133
+ var full_it = debug .StackIterator .initWithContext (null , debug_info , & context ) catch @panic ("failed StackIterator.initWithContext" );
134
+ defer full_it .deinit ();
135
+
136
+ while (full_it .next ()) | return_address | {
137
+ full_frames .append (return_address ) catch @panic ("full_frames.append()" );
138
+ if (full_frames .len == full_frames .capacity ()) break ;
139
+ }
140
+
141
+ // Run the iterator that skips until `expected[0]` is seen
142
+ testing .expect (debug .getContext (& context )) catch @panic ("failed 2nd getContext" );
143
+ var skip_it = debug .StackIterator .initWithContext (expected [0 ], debug_info , & context ) catch @panic ("failed StackIterator.initWithContext" );
144
+ defer skip_it .deinit ();
145
+
146
+ while (skip_it .next ()) | return_address | {
147
+ skip_frames .append (return_address ) catch @panic ("skip_frames.append()" );
148
+ if (skip_frames .len == skip_frames .capacity ()) break ;
149
+ }
150
+
151
+ captured_frames = true ;
152
+ }
153
+ }
154
+ }
155
+
156
+ noinline fn frame2 (expected : * [4 ]usize ) void {
22
157
// Exercise different __unwind_info / DWARF CFI encodings by forcing some registers to be restored
23
158
if (builtin .target .ofmt != .c ) {
24
- switch (builtin . cpu . arch ) {
159
+ switch (native_arch ) {
25
160
.x86 = > {
26
161
if (builtin .omit_frame_pointer ) {
27
162
asm volatile (
@@ -67,33 +202,142 @@ noinline fn frame2(expected: *[4]usize, unwound: *[4]usize) void {
67
202
}
68
203
69
204
expected [1 ] = @returnAddress ();
70
- frame3 (expected , unwound );
205
+ frame3 (expected );
71
206
}
72
207
73
- noinline fn frame1 (expected : * [4 ]usize , unwound : * [ 4 ] usize ) void {
208
+ noinline fn frame1 (expected : * [4 ]usize ) void {
74
209
expected [2 ] = @returnAddress ();
75
210
76
211
// Use a stack frame that is too big to encode in __unwind_info's stack-immediate encoding
77
212
// to exercise the stack-indirect encoding path
78
213
var pad : [std .math .maxInt (u8 ) * @sizeOf (usize ) + 1 ]u8 = undefined ;
79
214
_ = std .mem .doNotOptimizeAway (& pad );
80
215
81
- frame2 (expected , unwound );
216
+ frame2 (expected );
82
217
}
83
218
84
- noinline fn frame0 (expected : * [4 ]usize , unwound : * [ 4 ] usize ) void {
219
+ noinline fn frame0 (expected : * [4 ]usize ) void {
85
220
expected [3 ] = @returnAddress ();
86
- frame1 (expected , unwound );
221
+ frame1 (expected );
87
222
}
88
223
89
224
pub fn main () ! void {
225
+ try run_tests ();
226
+ std .debug .print ("Test complete.\n " , .{});
227
+ }
228
+
229
+ fn run_tests () ! void {
90
230
// Disabled until the DWARF unwinder bugs on .aarch64 are solved
91
- if (builtin .omit_frame_pointer and comptime builtin .target .os .tag .isDarwin () and builtin . cpu . arch == .aarch64 ) return ;
231
+ if (builtin .omit_frame_pointer and comptime builtin .target .os .tag .isDarwin () and native_arch == .aarch64 ) return ;
92
232
93
- if (! std .debug .have_ucontext or ! std .debug .have_getcontext ) return ;
233
+ if (do_signal ) {
234
+ std .debug .print ("Installing SIGURG handler ...\n " , .{});
235
+ posix .sigaction (posix .SIG .URG , &.{
236
+ .handler = .{ .sigaction = testFromSigUrg },
237
+ .mask = posix .sigemptyset (),
238
+ .flags = (posix .SA .SIGINFO | posix .SA .RESTART ),
239
+ }, null );
240
+ installed_signal_handler = true ;
241
+ } else {
242
+ std .debug .print ("(No signal-based backtrace on this configuration.)\n " , .{});
243
+ installed_signal_handler = false ;
244
+ }
245
+ handled_signal = false ;
246
+
247
+ signal_frames = try AddrArray .init (0 );
248
+ skip_frames = try AddrArray .init (0 );
249
+ full_frames = try AddrArray .init (0 );
250
+
251
+ std .debug .print ("Running...\n " , .{});
94
252
95
253
var expected : [4 ]usize = undefined ;
96
- var unwound : [4 ]usize = undefined ;
97
- frame0 (& expected , & unwound );
98
- try testing .expectEqual (expected , unwound );
254
+ frame0 (& expected );
255
+
256
+ std .debug .print ("Verification: arch={s} link_libc={} have_ucontext={} have_getcontext={} ...\n " , .{
257
+ @tagName (native_arch ), link_libc , std .debug .have_ucontext , std .debug .have_getcontext ,
258
+ });
259
+ std .debug .print (" expected={any}\n " , .{expected });
260
+ std .debug .print (" full_frames={any}\n " , .{full_frames .slice ()});
261
+ std .debug .print (" skip_frames={any}\n " , .{skip_frames .slice ()});
262
+ std .debug .print (" signal_frames={any}\n " , .{signal_frames .slice ()});
263
+
264
+ var fail_count : usize = 0 ;
265
+
266
+ if (do_signal and installed_signal_handler ) {
267
+ try testing .expectEqual (true , handled_signal );
268
+ }
269
+
270
+ // None of the backtraces should overflow max_stack_trace_depth
271
+
272
+ if (skip_frames .len == skip_frames .capacity ()) {
273
+ std .debug .print ("skip_frames contains too many frames: {}\n " , .{skip_frames .len });
274
+ fail_count += 1 ;
275
+ }
276
+
277
+ if (full_frames .len == full_frames .capacity ()) {
278
+ std .debug .print ("full_frames contains too many frames: {}\n " , .{full_frames .len });
279
+ fail_count += 1 ;
280
+ }
281
+
282
+ if (signal_frames .len == signal_frames .capacity ()) {
283
+ if (expect_signal_frame_overflow ) {
284
+ // The signal_frames backtrace overflows. Ignore this for now.
285
+ std .debug .print ("(expected) signal_frames overflow: {}\n " , .{signal_frames .len });
286
+ } else {
287
+ std .debug .print ("signal_frames contains too many frames: {}\n " , .{signal_frames .len });
288
+ fail_count += 1 ;
289
+ }
290
+ }
291
+
292
+ if (supports_stack_iterator ) {
293
+ if (captured_frames ) {
294
+ // Saved 'skip_frames' should start with the expected frames, exactly.
295
+ try testing .expectEqual (skip_frames .slice ()[0.. 4].* , expected );
296
+
297
+ // The return addresses in "expected[]" should show up, in order, in the "full_frames" array
298
+ var found = false ;
299
+ for (0.. full_frames .len ) | i | {
300
+ const addr = full_frames .get (i );
301
+ if (addr == expected [0 ]) {
302
+ try testing .expectEqual (full_frames .get (i + 1 ), expected [1 ]);
303
+ try testing .expectEqual (full_frames .get (i + 2 ), expected [2 ]);
304
+ try testing .expectEqual (full_frames .get (i + 3 ), expected [3 ]);
305
+ found = true ;
306
+ }
307
+ }
308
+ if (! found ) {
309
+ std .debug .print ("full_frames[...] does not include expected[0..4]\n " , .{});
310
+ fail_count += 1 ;
311
+ }
312
+ }
313
+
314
+ if (installed_signal_handler and handled_signal ) {
315
+ // The return addresses in "expected[]" should show up, in order, in the "signal_frames" array
316
+ var found = false ;
317
+ for (0.. signal_frames .len ) | i | {
318
+ const signal_addr = signal_frames .get (i );
319
+ if (signal_addr == expected [0 ]) {
320
+ try testing .expectEqual (signal_frames .get (i + 1 ), expected [1 ]);
321
+ try testing .expectEqual (signal_frames .get (i + 2 ), expected [2 ]);
322
+ try testing .expectEqual (signal_frames .get (i + 3 ), expected [3 ]);
323
+ found = true ;
324
+ }
325
+ }
326
+ if (! found ) {
327
+ if (expect_signal_frame_useless ) {
328
+ std .debug .print ("(expected) signal_frames[...] does not include expected[0..4]\n " , .{});
329
+ } else {
330
+ std .debug .print ("signal_frames[...] does not include expected[0..4]\n " , .{});
331
+ fail_count += 1 ;
332
+ }
333
+ }
334
+ }
335
+ } else {
336
+ // If these tests fail, then this platform now supports StackIterator
337
+ try testing .expectEqual (0 , skip_frames .len );
338
+ try testing .expectEqual (0 , full_frames .len );
339
+ try testing .expectEqual (0 , signal_frames .len );
340
+ }
341
+
342
+ try testing .expectEqual (0 , fail_count );
99
343
}
0 commit comments