Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ strict format validation
- Fixed locale-dependent decimal separator in `erlang:float_to_binary` and `erlang:float_to_list`
- Fixed `erlang:binary_to_float/1` and `erlang:list_to_float/1` returning `inf` for overflow instead
of raising `badarg`
- Fixed `erlang:raise/3` with a built stacktrace causing an assertion failure when the re-raised
exception passes through a non-matching catch clause

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

Expand Down
2 changes: 1 addition & 1 deletion src/libAtomVM/context.c
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Context *context_new(GlobalContext *glb)

context_set_exception_class(ctx, term_nil());
ctx->exception_reason = term_nil();
ctx->exception_stacktrace = term_nil();
ctx->exception_stacktrace = term_invalid_term();

ctx->exit_reason = NORMAL_ATOM;

Expand Down
8 changes: 4 additions & 4 deletions src/libAtomVM/jit.c
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,9 @@ static Context *jit_terminate_context(Context *ctx, JITState *jit_state)
static Context *jit_handle_error(Context *ctx, JITState *jit_state, int offset)
{
TRACE("jit_handle_error: ctx->process_id = %" PRId32 ", offset = %d\n", ctx->process_id, offset);
if (offset || term_is_invalid_term(ctx->exception_stacktrace)) {
ctx->exception_stacktrace
= stacktrace_create_raw(ctx, jit_state->module, offset);
if (offset || term_is_invalid_term(ctx->exception_stacktrace)
|| term_is_list(ctx->exception_stacktrace)) {
ctx->exception_stacktrace = stacktrace_create_raw(ctx, jit_state->module, offset);
}

// Copy exception fields to x registers and clear them
Expand All @@ -299,7 +299,7 @@ static Context *jit_handle_error(Context *ctx, JITState *jit_state, int offset)
ctx->x[2] = ctx->exception_stacktrace;
context_set_exception_class(ctx, term_nil());
ctx->exception_reason = term_nil();
ctx->exception_stacktrace = term_nil();
ctx->exception_stacktrace = term_invalid_term();

int target_label = context_get_catch_label(ctx, &jit_state->module);
if (target_label) {
Expand Down
12 changes: 7 additions & 5 deletions src/libAtomVM/opcodesswitch.h
Original file line number Diff line number Diff line change
Expand Up @@ -5245,17 +5245,19 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb)

TRACE("raw_raise/0\n");

// This is an optimization from the compiler where we don't need to call
// stacktrace_create_raw here because the stack trace has already been created
// and set in x[2].
// The compiler emits raw_raise for erlang:raise/3 calls.
// x_regs[2] may hold a built stacktrace (list of {M,F,A,Loc}
// tuples). We use HANDLE_ERROR() instead of a bare goto so
// stacktrace_create_raw_mfa wraps it into a raw 6-tuple that
// OP_RAISE can later process.
term ex_class = x_regs[0];
if (UNLIKELY(ex_class != ERROR_ATOM && ex_class != LOWERCASE_EXIT_ATOM && ex_class != THROW_ATOM)) {
x_regs[0] = BADARG_ATOM;
} else {
context_set_exception_class(ctx, x_regs[0]);
ctx->exception_reason = x_regs[1];
ctx->exception_stacktrace = x_regs[2];
goto handle_error;
HANDLE_ERROR();
}
break;
}
Expand Down Expand Up @@ -6229,7 +6231,7 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb)
x_regs[2] = ctx->exception_stacktrace;
context_set_exception_class(ctx, term_nil());
ctx->exception_reason = term_nil();
ctx->exception_stacktrace = term_nil();
ctx->exception_stacktrace = term_invalid_term();

int target_label = context_get_catch_label(ctx, &mod);
if (target_label) {
Expand Down
47 changes: 33 additions & 14 deletions src/libAtomVM/stacktrace.c
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,30 @@ term stacktrace_create_raw_mfa(Context *ctx, Module *mod, int current_offset, te
{
term exception_class = context_exception_class(ctx);

if (term_is_nonempty_list(ctx->exception_stacktrace)) {
// there is already a built stacktrace, nothing to do here
// (this happens when re-raising with raise/3
return ctx->exception_stacktrace;
if (term_is_list(ctx->exception_stacktrace)) {
// Already a built stacktrace (possibly empty) from erlang:raise/3
// NIF (via RAISE_WITH_STACKTRACE) or OP_RAW_RAISE. Wrap it in a raw
// 6-tuple so OP_RAISE can extract the exception class and
// stacktrace_build can return the list as-is.
ctx->x[0] = ctx->exception_stacktrace;
ctx->x[1] = ctx->exception_reason;
// NOLINT(term-use-after-gc) exception_class is always an atom
if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(6), 2, ctx->x, MEMORY_CAN_SHRINK)
!= MEMORY_GC_OK)) {
fprintf(stderr, "WARNING: Unable to allocate heap space for raw stacktrace\n");
return OUT_OF_MEMORY_ATOM;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't log here but below we do:

        fprintf(stderr, "WARNING: Unable to allocate heap space for raw stacktrace\n");

Arguably, this OOM is less likely.

}
ctx->exception_stacktrace = ctx->x[0];
ctx->exception_reason = ctx->x[1];
term built_stacktrace = ctx->exception_stacktrace;
term stack_info = term_alloc_tuple(6, &ctx->heap);
term_put_tuple_element(stack_info, 0, term_from_int(0));
term_put_tuple_element(stack_info, 1, term_from_int(0));
term_put_tuple_element(stack_info, 2, term_from_int(0));
term_put_tuple_element(stack_info, 3, term_from_int(0));
term_put_tuple_element(stack_info, 4, built_stacktrace);
term_put_tuple_element(stack_info, 5, exception_class);
return stack_info;
}

// Check if EXCEPTION_USE_LIVE_REGS_FLAG is set
Expand Down Expand Up @@ -235,10 +255,6 @@ term stacktrace_create_raw_mfa(Context *ctx, Module *mod, int current_offset, te

term frame_info;

// on OTP <= 22 module_atom is set to erlang, when calling a function such as erlang:throw
// making this heuristic unreliable since hides the throw caller from the stacktrace
// this means that either this heuristic is not 100% correct, or something changed in OTP-23
// anyway on OTP >= 23 seem to work as expected.
if (module_atom == UNDEFINED_ATOM) {
// module_atom has not been provided, let's use mod->module_index

Expand Down Expand Up @@ -365,12 +381,6 @@ term stacktrace_build(Context *ctx, term *stack_info, uint32_t live)
{
GlobalContext *glb = ctx->global;

if (term_is_nonempty_list(*stack_info)) {
// stacktrace has been already built. Nothing to do here
// This may happen when re-raising with raise/3
return *stack_info;
}

if (*stack_info == OUT_OF_MEMORY_ATOM) {
return *stack_info;
}
Expand All @@ -383,6 +393,15 @@ term stacktrace_build(Context *ctx, term *stack_info, uint32_t live)
int filename_lens = term_to_int(term_get_tuple_element(*stack_info, 2));
int num_mods = term_to_int(term_get_tuple_element(*stack_info, 3));

// Pre-built stacktrace from erlang:raise/3: element 4 already holds
// the built list, num_frames == 0. Return the list directly.
if (num_frames == 0) {
term raw_stacktrace = term_get_tuple_element(*stack_info, 4);
if (term_is_list(raw_stacktrace)) {
return raw_stacktrace;
}
}

struct ModulePathPair *module_paths = malloc(num_mods * sizeof(struct ModulePathPair));
if (IS_NULL_PTR(module_paths)) {
fprintf(stderr, "Unable to allocate space for module paths. Returning raw stacktrace.\n");
Expand Down
2 changes: 2 additions & 0 deletions tests/erlang_tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,7 @@ compile_erlang(test_lists_keyfind)
compile_erlang(test_reraise)
compile_erlang(reraise_reraiser)
compile_erlang(reraise_raiser)
compile_erlang(test_raise_built_stacktrace)

compile_erlang(stacktrace_function_args)
compile_erlang(test_multi_value_comprehension)
Expand Down Expand Up @@ -1209,6 +1210,7 @@ set(erlang_test_beams
test_reraise.beam
reraise_reraiser.beam
reraise_raiser.beam
test_raise_built_stacktrace.beam

stacktrace_function_args.beam

Expand Down
Loading
Loading