Skip to content

Commit b9ba453

Browse files
committed
Add workaround for crash related to call_ext_last/3
BEAM's compiler and especially beam_trim optimization pass can generate an incorrect deallocation `n_words` parameter for `call_ext_last/3`. As a workaround, deallocate after the nif call, and before any error handling that may need the stack to find catch handlers. See: erlang/otp#7152 Add test that currently crashes on master without this change Signed-off-by: Paul Guyot <[email protected]>
1 parent 67aab91 commit b9ba453

File tree

4 files changed

+150
-3
lines changed

4 files changed

+150
-3
lines changed

src/libAtomVM/opcodesswitch.h

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,9 +1595,6 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f
15951595

15961596
TRACE_CALL_EXT(ctx, mod, "call_ext_last", index, arity);
15971597

1598-
ctx->cp = ctx->e[n_words];
1599-
ctx->e += (n_words + 1);
1600-
16011598
const struct ExportedFunction *func = mod->imported_funcs[index].func;
16021599

16031600
if (func->type == UnresolvedFunctionCall) {
@@ -1617,11 +1614,25 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f
16171614
}
16181615
ctx->x[0] = return_value;
16191616

1617+
// We deallocate after (instead of before) as a
1618+
// workaround for issue
1619+
// https://github.com/erlang/otp/issues/7152
1620+
1621+
ctx->cp = ctx->e[n_words];
1622+
ctx->e += (n_words + 1);
1623+
16201624
DO_RETURN();
16211625

16221626
break;
16231627
}
16241628
case ModuleFunction: {
1629+
// In the non-nif case, we can deallocate before
1630+
// (and it doesn't matter as the code below does
1631+
// not access ctx->e or ctx->cp)
1632+
1633+
ctx->cp = ctx->e[n_words];
1634+
ctx->e += (n_words + 1);
1635+
16251636
const struct ModuleFunction *jump = EXPORTED_FUNCTION_TO_MODULE_FUNCTION(func);
16261637

16271638
mod = jump->target;

tests/erlang_tests/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@ compile_erlang(bs_context_to_binary_with_offset)
425425
compile_erlang(bs_restore2_start_offset)
426426
compile_erlang(test_refc_binaries)
427427
compile_erlang(test_sub_binaries)
428+
compile_erlang(test_throw_call_ext_last)
428429
compile_erlang(bs_append_extra_words)
429430

430431
compile_erlang(test_monotonic_time)
@@ -839,6 +840,7 @@ add_custom_target(erlang_test_modules DEPENDS
839840

840841
test_refc_binaries.beam
841842
test_sub_binaries.beam
843+
test_throw_call_ext_last.beam
842844
test_function_exported.beam
843845
test_list_to_tuple.beam
844846

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
%
2+
% This file is part of AtomVM.
3+
%
4+
% Copyright 2023 Paul Guyot <[email protected]>
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_throw_call_ext_last).
22+
23+
-export([start/0, loop/1]).
24+
25+
-record(state, {
26+
bin
27+
}).
28+
29+
start() ->
30+
ok = run_test(fun() -> test_count_binary() end),
31+
{error, {heap_delta, _Delta}} = run_test(fun() -> test_spawn_fun_sub_binary() end),
32+
0.
33+
34+
test_spawn_fun_sub_binary() ->
35+
Bin = create_binary(1024),
36+
BinarySize = erlang:byte_size(Bin),
37+
%%
38+
%% Spawn a function, passing a refc binary through the args
39+
%%
40+
LargeSubBin = binary:part(Bin, 1, BinarySize - 1),
41+
Pid = erlang:spawn(fun() -> loop(#state{bin = LargeSubBin}) end),
42+
PidHeapSize0 = get_heap_size(Pid),
43+
%%
44+
%% Make sure we can get what we spawned
45+
%%
46+
LargeSubBin = send(Pid, get),
47+
%%
48+
%% Free the refc binary; heap should decrease
49+
%%
50+
ok = send(Pid, free),
51+
PidHeapSize2 = get_heap_size(Pid),
52+
case PidHeapSize2 - PidHeapSize0 of
53+
0 -> ok;
54+
% should be call_ext_last
55+
Delta -> throw({heap_delta, Delta})
56+
end,
57+
ok = send(Pid, halt),
58+
ok.
59+
60+
test_count_binary() ->
61+
_ = create_binary(1024),
62+
ok.
63+
64+
%%
65+
%% helper functions
66+
%%
67+
68+
get_heap_size() ->
69+
erlang:garbage_collect(),
70+
{heap_size, Size} = erlang:process_info(self(), heap_size),
71+
Size * erlang:system_info(wordsize).
72+
73+
get_heap_size(Pid) ->
74+
send(Pid, get_heap_size).
75+
76+
send(Pid, Msg) ->
77+
Ref = erlang:make_ref(),
78+
Pid ! {self(), Ref, Msg},
79+
receive
80+
{Ref, Reply} -> Reply
81+
end.
82+
83+
loop(State) ->
84+
erlang:garbage_collect(),
85+
receive
86+
{Pid, Ref, get} ->
87+
Pid ! {Ref, State#state.bin},
88+
loop(State);
89+
{Pid, Ref, free} ->
90+
Pid ! {Ref, ok},
91+
loop(State#state{bin = undefined});
92+
{Pid, Ref, get_heap_size} ->
93+
Pid ! {Ref, get_heap_size()},
94+
loop(State);
95+
{Pid, Ref, {ref, Bin}} ->
96+
Pid ! {Ref, ok},
97+
loop(State#state{bin = Bin});
98+
{Pid, Ref, halt} ->
99+
Pid ! {Ref, ok}
100+
end.
101+
102+
create_binary(N) when is_integer(N) ->
103+
S = create_string(N, []),
104+
R = erlang:list_to_binary(S),
105+
R;
106+
create_binary(S) when is_list(S) ->
107+
list_to_binary(S).
108+
109+
create_string(0, Accum) ->
110+
Accum;
111+
create_string(N, Accum) ->
112+
create_string(N - 1, [N rem 256 | Accum]).
113+
114+
run_test(Fun) ->
115+
Self = self(),
116+
_Pid = spawn(fun() -> execute(Self, Fun) end),
117+
receive
118+
ok ->
119+
ok;
120+
Error ->
121+
Error
122+
end.
123+
124+
execute(Pid, Fun) ->
125+
Result =
126+
try
127+
Fun(),
128+
ok
129+
catch
130+
_:Error ->
131+
{error, Error}
132+
end,
133+
Pid ! Result.

tests/test.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ struct Test tests[] = {
354354
TEST_CASE(test_map),
355355
TEST_CASE_ATOMVM_ONLY(test_refc_binaries, 0),
356356
TEST_CASE(test_sub_binaries),
357+
TEST_CASE_ATOMVM_ONLY(test_throw_call_ext_last, 0),
357358

358359
TEST_CASE_EXPECTED(ceilint, 1),
359360
TEST_CASE_EXPECTED(ceilbadarg, -1),

0 commit comments

Comments
 (0)