Skip to content

Commit 3447789

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 686aeed commit 3447789

File tree

2 files changed

+263
-20
lines changed

2 files changed

+263
-20
lines changed

test/standalone/stack_iterator/build.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pub fn build(b: *std.Build) void {
3030
});
3131

3232
const run_cmd = b.addRunArtifact(exe);
33+
run_cmd.addCheck(.{ .expect_stderr_match = "Test complete." });
3334
test_step.dependOn(&run_cmd.step);
3435
}
3536

@@ -55,6 +56,7 @@ pub fn build(b: *std.Build) void {
5556
});
5657

5758
const run_cmd = b.addRunArtifact(exe);
59+
run_cmd.addCheck(.{ .expect_stderr_match = "Test complete." });
5860
test_step.dependOn(&run_cmd.step);
5961
}
6062

test/standalone/stack_iterator/unwind.zig

Lines changed: 261 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,161 @@ 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.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
851

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+
var align_ctx: *align(1) posix.ucontext_t = @ptrCast(ctx_ptr);
64+
local_ctx = byte_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);
1188

1289
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();
92+
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;
97+
}
98+
99+
handled_signal = true;
100+
}
101+
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+
}
15140

16-
for (unwound) |*addr| {
17-
if (it.next()) |return_address| addr.* = return_address;
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+
}
18153
}
19154
}
20155

21-
noinline fn frame2(expected: *[4]usize, unwound: *[4]usize) void {
156+
noinline fn frame2(expected: *[4]usize) void {
22157
// Exercise different __unwind_info / DWARF CFI encodings by forcing some registers to be restored
23158
if (builtin.target.ofmt != .c) {
24-
switch (builtin.cpu.arch) {
159+
switch (native_arch) {
25160
.x86 => {
26161
if (builtin.omit_frame_pointer) {
27162
asm volatile (
@@ -67,33 +202,139 @@ noinline fn frame2(expected: *[4]usize, unwound: *[4]usize) void {
67202
}
68203

69204
expected[1] = @returnAddress();
70-
frame3(expected, unwound);
205+
frame3(expected);
71206
}
72207

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

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

81-
frame2(expected, unwound);
216+
frame2(expected);
82217
}
83218

84-
noinline fn frame0(expected: *[4]usize, unwound: *[4]usize) void {
219+
noinline fn frame0(expected: *[4]usize) void {
85220
expected[3] = @returnAddress();
86-
frame1(expected, unwound);
221+
frame1(expected);
87222
}
88223

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

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

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

0 commit comments

Comments
 (0)