Skip to content

Commit 37ebc96

Browse files
committed
unwind test: Add testing of signal ucontext and dumpCurrentStackTrace()
This test creates three nested stack frames and then tests stack trace creation. Add some additional tests of stack traces by invoking "dumpCurrentStackTrace()" and by using a signal handler's "context" parameter to feed backtrace construction. Make the test case at least runnable on a wide variety of systems (including Windows, and WASI). Because `ucontext_t` and `getcontext` are not evenly supported everywhere, some systems are expected only get through parts of the test.
1 parent 44f476c commit 37ebc96

File tree

1 file changed

+257
-20
lines changed

1 file changed

+257
-20
lines changed

test/standalone/stack_iterator/unwind.zig

Lines changed: 257 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,157 @@ const std = @import("std");
22
const builtin = @import("builtin");
33
const debug = std.debug;
44
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;
59

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 != .wasm32) and
33+
(native_arch != .wasm64); // wasm has no introspection
34+
35+
// Getting the backtrace inside the signal handler (with the ucontext_t)
36+
// gets stuck in a loop on some systems:
37+
const expect_signal_frame_overflow =
38+
(native_arch == .arm and link_libc) or // loops above main()
39+
(native_arch == .aarch64); // non-deterministic, sometimes overflows, sometimes not
40+
41+
// Getting the backtrace inside the signal handler (with the ucontext_t)
42+
// does not contain the expected content on some systems:
43+
const expect_signal_frame_useless =
44+
(native_arch == .x86_64 and link_libc and builtin.abi.isGnu()) or // stuck on pthread_kill?
45+
(native_arch == .x86_64 and link_libc and builtin.abi.isMusl() and builtin.omit_frame_pointer) or // immediately confused backtrace
46+
(native_arch == .x86_64 and builtin.os.tag.isDarwin()) or // immediately confused backtrace
47+
(native_arch == .aarch64 or native_arch == .aarch64_be) or // non-deterministic, sometimes overflows, sometimes confused
48+
(native_arch == .riscv64 and link_libc) or // `ucontext_t` not defined yet
49+
native_arch == .mips or // Missing ucontext_t. Most stack traces are empty ... (with or without libc)
50+
native_arch == .mipsel or // same as .mips
51+
native_arch == .mips64 or // same as .mips
52+
native_arch == .mips64el or // same as .mips
53+
native_arch == .powerpc64 or // dumpCurrent* useless, StackIterator empty, ctx-based trace empty (with or without libc)
54+
native_arch == .powerpc64le; // same as .powerpc64
855

9-
var context: debug.ThreadContext = undefined;
10-
testing.expect(debug.getContext(&context)) catch @panic("failed to getContext");
56+
// Signal handler to gather stack traces from the given signal context.
57+
fn testFromSigUrg(sig: i32, info: *const posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void {
58+
// std.debug.print("sig={} info={*} ctx_ptr={*}\n", .{ sig, info, ctx_ptr });
59+
_ = info;
60+
_ = sig;
61+
62+
// Some kernels don't align `ctx_ptr` properly. Handle this defensively.
63+
const ctx: *align(1) posix.ucontext_t = @ptrCast(ctx_ptr);
64+
var new_ctx: posix.ucontext_t = ctx.*;
65+
if (builtin.os.tag.isDarwin() and builtin.cpu.arch == .aarch64) {
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+
new_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(ctx)).__mcontext_data;
79+
}
80+
debug.relocateContext(&new_ctx);
81+
82+
std.debug.print("(from signal handler) dumpStackTraceFromBase({*} => {*}):\n", .{ ctx_ptr, &new_ctx });
83+
debug.dumpStackTraceFromBase(&new_ctx);
1184

1285
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();
86+
var sig_it = debug.StackIterator.initWithContext(null, debug_info, &new_ctx) catch @panic("failed StackIterator.initWithContext");
87+
defer sig_it.deinit();
1588

16-
for (unwound) |*addr| {
17-
if (it.next()) |return_address| addr.* = return_address;
89+
// Save the backtrace from 'ctx' into the 'signal_frames' array
90+
while (sig_it.next()) |return_address| {
91+
signal_frames.append(return_address) catch @panic("signal_frames.append()");
92+
if (signal_frames.len == signal_frames.capacity()) break;
1893
}
94+
95+
handled_signal = true;
1996
}
2097

21-
noinline fn frame2(expected: *[4]usize, unwound: *[4]usize) void {
98+
// Leaf test function. Gather backtraces for comparison with "expected".
99+
noinline fn frame3(expected: *[4]usize) void {
100+
expected[0] = @returnAddress();
101+
102+
// Test the print-current-stack trace functions
103+
std.debug.print("dumpCurrentStackTrace(null):\n", .{});
104+
debug.dumpCurrentStackTrace(null);
105+
106+
std.debug.print("dumpCurrentStackTrace({x}):\n", .{expected[0]});
107+
debug.dumpCurrentStackTrace(expected[0]);
108+
109+
// Trigger signal handler here and see that it's ctx is a viable start for unwinding
110+
if (do_signal and installed_signal_handler) {
111+
posix.raise(posix.SIG.URG) catch @panic("failed to raise posix.SIG.URG");
112+
}
113+
114+
// Capture stack traces directly, two ways, if supported
115+
if (std.debug.ThreadContext != void and native_os != .windows) {
116+
var context: debug.ThreadContext = undefined;
117+
118+
const gotContext = debug.getContext(&context);
119+
120+
if (!std.debug.have_getcontext) {
121+
testing.expectEqual(false, gotContext) catch @panic("getContext unexpectedly succeeded");
122+
} else {
123+
testing.expectEqual(true, gotContext) catch @panic("failed to getContext");
124+
125+
const debug_info = debug.getSelfDebugInfo() catch @panic("failed to openSelfDebugInfo");
126+
127+
// Run the "full" iterator
128+
testing.expect(debug.getContext(&context)) catch @panic("failed to getContext");
129+
var full_it = debug.StackIterator.initWithContext(null, debug_info, &context) catch @panic("failed StackIterator.initWithContext");
130+
defer full_it.deinit();
131+
132+
while (full_it.next()) |return_address| {
133+
full_frames.append(return_address) catch @panic("full_frames.append()");
134+
if (full_frames.len == full_frames.capacity()) break;
135+
}
136+
137+
// Run the iterator that skips until `expected[0]` is seen
138+
testing.expect(debug.getContext(&context)) catch @panic("failed 2nd getContext");
139+
var skip_it = debug.StackIterator.initWithContext(expected[0], debug_info, &context) catch @panic("failed StackIterator.initWithContext");
140+
defer skip_it.deinit();
141+
142+
while (skip_it.next()) |return_address| {
143+
skip_frames.append(return_address) catch @panic("skip_frames.append()");
144+
if (skip_frames.len == skip_frames.capacity()) break;
145+
}
146+
147+
captured_frames = true;
148+
}
149+
}
150+
}
151+
152+
noinline fn frame2(expected: *[4]usize) void {
22153
// Exercise different __unwind_info / DWARF CFI encodings by forcing some registers to be restored
23154
if (builtin.target.ofmt != .c) {
24-
switch (builtin.cpu.arch) {
155+
switch (native_arch) {
25156
.x86 => {
26157
if (builtin.omit_frame_pointer) {
27158
asm volatile (
@@ -67,33 +198,139 @@ noinline fn frame2(expected: *[4]usize, unwound: *[4]usize) void {
67198
}
68199

69200
expected[1] = @returnAddress();
70-
frame3(expected, unwound);
201+
frame3(expected);
71202
}
72203

73-
noinline fn frame1(expected: *[4]usize, unwound: *[4]usize) void {
204+
noinline fn frame1(expected: *[4]usize) void {
74205
expected[2] = @returnAddress();
75206

76207
// Use a stack frame that is too big to encode in __unwind_info's stack-immediate encoding
77208
// to exercise the stack-indirect encoding path
78209
var pad: [std.math.maxInt(u8) * @sizeOf(usize) + 1]u8 = undefined;
79210
_ = std.mem.doNotOptimizeAway(&pad);
80211

81-
frame2(expected, unwound);
212+
frame2(expected);
82213
}
83214

84-
noinline fn frame0(expected: *[4]usize, unwound: *[4]usize) void {
215+
noinline fn frame0(expected: *[4]usize) void {
85216
expected[3] = @returnAddress();
86-
frame1(expected, unwound);
217+
frame1(expected);
87218
}
88219

89220
pub fn main() !void {
90221
// 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;
222+
if (builtin.omit_frame_pointer and comptime builtin.target.os.tag.isDarwin() and native_arch == .aarch64) return;
223+
224+
if (do_signal) {
225+
std.debug.print("Installing SIGURG handler ...\n", .{});
226+
posix.sigaction(posix.SIG.URG, &.{
227+
.handler = .{ .sigaction = testFromSigUrg },
228+
.mask = posix.sigemptyset(),
229+
.flags = (posix.SA.SIGINFO | posix.SA.RESTART),
230+
}, null);
231+
installed_signal_handler = true;
232+
} else {
233+
std.debug.print("(No signal-based backtrace on this configuration.)\n", .{});
234+
installed_signal_handler = false;
235+
}
236+
handled_signal = false;
237+
238+
signal_frames = try AddrArray.init(0);
239+
skip_frames = try AddrArray.init(0);
240+
full_frames = try AddrArray.init(0);
92241

93-
if (!std.debug.have_ucontext or !std.debug.have_getcontext) return;
242+
std.debug.print("Running...\n", .{});
94243

95244
var expected: [4]usize = undefined;
96-
var unwound: [4]usize = undefined;
97-
frame0(&expected, &unwound);
98-
try testing.expectEqual(expected, unwound);
245+
frame0(&expected);
246+
247+
std.debug.print("Verification: arch={s} link_libc={} have_ucontext={} have_getcontext={} ...\n", .{
248+
@tagName(native_arch), link_libc, std.debug.have_ucontext, std.debug.have_getcontext,
249+
});
250+
std.debug.print(" expected={any}\n", .{expected});
251+
std.debug.print(" full_frames={any}\n", .{full_frames.slice()});
252+
std.debug.print(" skip_frames={any}\n", .{skip_frames.slice()});
253+
std.debug.print(" signal_frames={any}\n", .{signal_frames.slice()});
254+
255+
var fail_count: usize = 0;
256+
257+
if (do_signal and installed_signal_handler) {
258+
try testing.expectEqual(true, handled_signal);
259+
}
260+
261+
// None of the backtraces should overflow max_stack_trace_depth
262+
263+
if (skip_frames.len == skip_frames.capacity()) {
264+
std.debug.print("skip_frames contains too many frames: {}\n", .{skip_frames.len});
265+
fail_count += 1;
266+
}
267+
268+
if (full_frames.len == full_frames.capacity()) {
269+
std.debug.print("full_frames contains too many frames: {}\n", .{full_frames.len});
270+
fail_count += 1;
271+
}
272+
273+
if (signal_frames.len == signal_frames.capacity()) {
274+
if (expect_signal_frame_overflow) {
275+
// The signal_frames backtrace overflows. Ignore this for now.
276+
std.debug.print("(expected) signal_frames overflow: {}\n", .{signal_frames.len});
277+
} else {
278+
std.debug.print("signal_frames contains too many frames: {}\n", .{signal_frames.len});
279+
fail_count += 1;
280+
}
281+
}
282+
283+
if (supports_stack_iterator) {
284+
if (captured_frames) {
285+
// Saved 'skip_frames' should start with the expected frames, exactly.
286+
try testing.expectEqual(skip_frames.slice()[0..4].*, expected);
287+
288+
// The return addresses in "expected[]" should show up, in order, in the "full_frames" array
289+
var found = false;
290+
for (0..full_frames.len) |i| {
291+
const addr = full_frames.get(i);
292+
if (addr == expected[0]) {
293+
try testing.expectEqual(full_frames.get(i + 1), expected[1]);
294+
try testing.expectEqual(full_frames.get(i + 2), expected[2]);
295+
try testing.expectEqual(full_frames.get(i + 3), expected[3]);
296+
found = true;
297+
}
298+
}
299+
if (!found) {
300+
std.debug.print("full_frames[...] does not include expected[0..4]\n", .{});
301+
fail_count += 1;
302+
}
303+
}
304+
305+
if (installed_signal_handler and handled_signal) {
306+
// The return addresses in "expected[]" should show up, in order, in the "signal_frames" array
307+
var found = false;
308+
for (0..signal_frames.len) |i| {
309+
const signal_addr = signal_frames.get(i);
310+
if (signal_addr == expected[0]) {
311+
try testing.expectEqual(signal_frames.get(i + 1), expected[1]);
312+
try testing.expectEqual(signal_frames.get(i + 2), expected[2]);
313+
try testing.expectEqual(signal_frames.get(i + 3), expected[3]);
314+
found = true;
315+
}
316+
}
317+
if (!found) {
318+
if (expect_signal_frame_useless) {
319+
std.debug.print("(expected) signal_frames[...] does not include expected[0..4]\n", .{});
320+
} else {
321+
std.debug.print("signal_frames[...] does not include expected[0..4]\n", .{});
322+
fail_count += 1;
323+
}
324+
}
325+
}
326+
} else {
327+
// If these tests fail, then this platform now supports StackIterator
328+
try testing.expectEqual(0, skip_frames.len);
329+
try testing.expectEqual(0, full_frames.len);
330+
try testing.expectEqual(0, signal_frames.len);
331+
}
332+
333+
std.debug.print("Test complete.\n", .{});
334+
335+
try testing.expectEqual(0, fail_count);
99336
}

0 commit comments

Comments
 (0)