Skip to content

Commit 86ff1db

Browse files
committed
Fix erlang:raise/3 assert with built stacktrace
When erlang:raise/3 is called with a built stacktrace (list of {M,F,A,Loc} tuples) and the re-raised exception passes through a try/catch whose clauses do not match, the compiler-generated catch-all (OP_RAISE) calls stacktrace_exception_class, which asserts on a 6-tuple. The built list was stored as-is, never wrapped. Wrap the built stacktrace into a raw 6-tuple in stacktrace_create_raw_mfa so OP_RAISE and stacktrace_build can process it. Route OP_RAW_RAISE through HANDLE_ERROR() so it also hits the wrapping path. Signed-off-by: Davide Bettio <davide@uninstall.it>
1 parent 4f64b52 commit 86ff1db

File tree

6 files changed

+107
-4
lines changed

6 files changed

+107
-4
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ strict format validation
2929
- Fixed locale-dependent decimal separator in `erlang:float_to_binary` and `erlang:float_to_list`
3030
- Fixed `erlang:binary_to_float/1` and `erlang:list_to_float/1` returning `inf` for overflow instead
3131
of raising `badarg`
32+
- Fixed `erlang:raise/3` with a built stacktrace causing an assert when the re-raised exception
33+
passes through a non-matching catch clause
3234

3335
## [0.7.0-alpha.0] - 2026-03-20
3436

src/libAtomVM/opcodesswitch.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5255,7 +5255,12 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb)
52555255
context_set_exception_class(ctx, x_regs[0]);
52565256
ctx->exception_reason = x_regs[1];
52575257
ctx->exception_stacktrace = x_regs[2];
5258-
goto handle_error;
5258+
// Use HANDLE_ERROR() instead of goto handle_error so that
5259+
// stacktrace_create_raw_mfa is called. When x_regs[2] holds
5260+
// a built stacktrace (list of {M,F,A,Loc} tuples from
5261+
// erlang:raise/3), stacktrace_create_raw_mfa wraps it into
5262+
// a raw 6-tuple that OP_RAISE can process.
5263+
HANDLE_ERROR();
52595264
}
52605265
break;
52615266
}

src/libAtomVM/stacktrace.c

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,23 @@ term stacktrace_create_raw_mfa(Context *ctx, Module *mod, int current_offset, te
112112
term exception_class = context_exception_class(ctx);
113113

114114
if (term_is_nonempty_list(ctx->exception_stacktrace)) {
115-
// there is already a built stacktrace, nothing to do here
116-
// (this happens when re-raising with raise/3
117-
return ctx->exception_stacktrace;
115+
// Already a built stacktrace (list of {M,F,A,Loc} tuples) from
116+
// erlang:raise/3 NIF (via RAISE_WITH_STACKTRACE) or OP_RAW_RAISE.
117+
// Wrap it in a raw 6-tuple so OP_RAISE can extract the exception class
118+
// and stacktrace_build can return the list as-is.
119+
ctx->x[0] = ctx->exception_stacktrace;
120+
if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(6), 1, ctx->x, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) {
121+
return OUT_OF_MEMORY_ATOM;
122+
}
123+
term built_stacktrace = ctx->x[0];
124+
term stack_info = term_alloc_tuple(6, &ctx->heap);
125+
term_put_tuple_element(stack_info, 0, term_from_int(0));
126+
term_put_tuple_element(stack_info, 1, term_from_int(0));
127+
term_put_tuple_element(stack_info, 2, term_from_int(0));
128+
term_put_tuple_element(stack_info, 3, term_from_int(0));
129+
term_put_tuple_element(stack_info, 4, built_stacktrace);
130+
term_put_tuple_element(stack_info, 5, exception_class);
131+
return stack_info;
118132
}
119133

120134
// Check if EXCEPTION_USE_LIVE_REGS_FLAG is set
@@ -383,6 +397,15 @@ term stacktrace_build(Context *ctx, term *stack_info, uint32_t live)
383397
int filename_lens = term_to_int(term_get_tuple_element(*stack_info, 2));
384398
int num_mods = term_to_int(term_get_tuple_element(*stack_info, 3));
385399

400+
// Pre-built stacktrace from erlang:raise/3: element 4 already holds
401+
// the built list, num_frames == 0. Return the list directly.
402+
if (num_frames == 0) {
403+
term raw_stacktrace = term_get_tuple_element(*stack_info, 4);
404+
if (term_is_nonempty_list(raw_stacktrace)) {
405+
return raw_stacktrace;
406+
}
407+
}
408+
386409
struct ModulePathPair *module_paths = malloc(num_mods * sizeof(struct ModulePathPair));
387410
if (IS_NULL_PTR(module_paths)) {
388411
fprintf(stderr, "Unable to allocate space for module paths. Returning raw stacktrace.\n");

tests/erlang_tests/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,7 @@ compile_erlang(test_lists_keyfind)
653653
compile_erlang(test_reraise)
654654
compile_erlang(reraise_reraiser)
655655
compile_erlang(reraise_raiser)
656+
compile_erlang(test_raise_built_stacktrace)
656657

657658
compile_erlang(stacktrace_function_args)
658659
compile_erlang(test_multi_value_comprehension)
@@ -1209,6 +1210,7 @@ set(erlang_test_beams
12091210
test_reraise.beam
12101211
reraise_reraiser.beam
12111212
reraise_raiser.beam
1213+
test_raise_built_stacktrace.beam
12121214

12131215
stacktrace_function_args.beam
12141216

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
%
2+
% This file is part of AtomVM.
3+
%
4+
% Copyright 2026 Davide Bettio <davide@uninstall.it>
5+
%
6+
% Licensed under the Apache License, Version 2.0 (the "License");
7+
% you may not use this file except in compliance with the License.
8+
% You may obtain a copy of the License at
9+
%
10+
% http://www.apache.org/licenses/LICENSE-2.0
11+
%
12+
% Unless required by applicable law or agreed to in writing, software
13+
% distributed under the License is distributed on an "AS IS" BASIS,
14+
% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
% See the License for the specific language governing permissions and
16+
% limitations under the License.
17+
%
18+
% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
19+
%
20+
21+
-module(test_raise_built_stacktrace).
22+
23+
-export([start/0, id/1]).
24+
25+
start() ->
26+
ok = test_raise_direct(),
27+
ok = test_raise_dynamic(),
28+
0.
29+
30+
%% Tests the raw_raise opcode path (erlang:raise/3 compiled to raw_raise).
31+
test_raise_direct() ->
32+
try
33+
try
34+
do_raise_direct()
35+
catch
36+
throw:_ -> should_not_happen
37+
end
38+
catch
39+
error:badarg -> ok
40+
end.
41+
42+
%% Tests the NIF path (dynamic apply bypasses raw_raise opcode).
43+
test_raise_dynamic() ->
44+
try
45+
try
46+
do_raise_dynamic()
47+
catch
48+
throw:_ -> should_not_happen
49+
end
50+
catch
51+
error:badarg -> ok
52+
end.
53+
54+
do_raise_direct() ->
55+
try
56+
erlang:error(badarg)
57+
catch
58+
error:badarg:Stacktrace ->
59+
erlang:raise(error, badarg, Stacktrace)
60+
end.
61+
62+
do_raise_dynamic() ->
63+
try
64+
erlang:error(badarg)
65+
catch
66+
error:badarg:Stacktrace ->
67+
apply(?MODULE:id(erlang), ?MODULE:id(raise), [error, badarg, Stacktrace])
68+
end.
69+
70+
id(X) -> X.

tests/test.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,7 @@ struct Test tests[] = {
633633
TEST_CASE(test_lists_keyfind),
634634

635635
TEST_CASE_COND(test_reraise, 0, SKIP_STACKTRACES),
636+
TEST_CASE_COND(test_raise_built_stacktrace, 0, SKIP_STACKTRACES),
636637
TEST_CASE_COND(stacktrace_function_args, 0, SKIP_STACKTRACES),
637638

638639
TEST_CASE(test_inline_arith),

0 commit comments

Comments
 (0)