Skip to content

Commit 9650244

Browse files
committed
Merge pull request atomvm#518 from pguyot/w16/fix-call_ext_last-throw-stack_base
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 These changes are made under both the "Apache 2.0" and the "GNU Lesser General Public License 2.1 or later" license terms (dual license). SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
2 parents fc49821 + b9ba453 commit 9650244

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)