Skip to content

Commit 007efde

Browse files
authored
Fix reentrancy of Code.eval_* (#15306)
Keep env and dbg callbacks on stacks in process dict Closes #15303
1 parent eb8bbc9 commit 007efde

2 files changed

Lines changed: 60 additions & 12 deletions

File tree

lib/elixir/src/elixir.erl

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,9 @@ eval_forms(Tree, Binding, OrigE) ->
304304
eval_forms(Tree, Binding, OrigE, []).
305305
eval_forms(Tree, Binding, OrigE, Opts) ->
306306
Prune = proplists:get_value(prune_binding, Opts, false),
307+
%% We save and restore the previous dbg_callback so nested eval calls in
308+
%% the same process do not clobber the outer callback.
309+
PreviousDbg = erlang:get({elixir, dbg_callback}),
307310
case proplists:get_value(dbg_callback, Opts) of
308311
undefined -> ok;
309312
DbgCallback -> erlang:put({elixir, dbg_callback}, DbgCallback)
@@ -332,7 +335,10 @@ eval_forms(Tree, Binding, OrigE, Opts) ->
332335
{Value, DumpedBinding, NewE#{versioned_vars := DumpedVars}}
333336
end
334337
after
335-
erlang:erase({elixir, dbg_callback})
338+
case PreviousDbg of
339+
undefined -> erlang:erase({elixir, dbg_callback});
340+
_ -> erlang:put({elixir, dbg_callback}, PreviousDbg)
341+
end
336342
end.
337343

338344
%% Evaluate Erlang code with careful handling of local and external functions
@@ -341,20 +347,27 @@ erl_eval(Expr, Binding, Env) ->
341347
LocalHandler = {value, fun ?MODULE:eval_local_handler/2},
342348
ExternalHandler = {value, fun ?MODULE:eval_external_handler/3},
343349

350+
%% ?elixir_eval_env is used by the external handler.
351+
%%
352+
%% The reason why we use the process dictionary to pass the environment
353+
%% is because we want to avoid passing closures to erl_eval, as that
354+
%% would effectively tie the eval code to the Elixir version and it is
355+
%% best if it depends solely on Erlang/OTP.
356+
%%
357+
%% The downside is that functions that escape the eval context will no
358+
%% longer have the original environment they came from.
359+
%%
360+
%% We save and restore the previous env so nested eval calls in the same
361+
%% process do not clobber the outer env.
362+
PreviousEvalEnv = erlang:get(?elixir_eval_env),
363+
erlang:put(?elixir_eval_env, Env),
344364
try
345-
%% ?elixir_eval_env is used by the external handler.
346-
%%
347-
%% The reason why we use the process dictionary to pass the environment
348-
%% is because we want to avoid passing closures to erl_eval, as that
349-
%% would effectively tie the eval code to the Elixir version and it is
350-
%% best if it depends solely on Erlang/OTP.
351-
%%
352-
%% The downside is that functions that escape the eval context will no
353-
%% longer have the original environment they came from.
354-
erlang:put(?elixir_eval_env, Env),
355365
erl_eval:expr(Expr, Binding, LocalHandler, ExternalHandler)
356366
after
357-
erlang:erase(?elixir_eval_env)
367+
case PreviousEvalEnv of
368+
undefined -> erlang:erase(?elixir_eval_env);
369+
_ -> erlang:put(?elixir_eval_env, PreviousEvalEnv)
370+
end
358371
end.
359372

360373
eval_local_handler(FunName, Args) ->

lib/elixir/test/elixir/code_test.exs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,41 @@ defmodule CodeTest do
288288
assert {1, _binding} = Code.eval_string("dbg(1)", [])
289289
end)
290290
end
291+
292+
test "nested eval preserves outer :dbg_callback" do
293+
opts = [dbg_callback: {__MODULE__, :dbg_callback_add_one, []}]
294+
295+
assert {{2, []}, _binding} =
296+
Code.eval_string(
297+
"""
298+
Code.eval_string("dbg(1)")
299+
""",
300+
[],
301+
opts
302+
)
303+
end
304+
305+
test "nested eval preserves outer env in exception stacktrace" do
306+
env = %{Code.env_for_eval([]) | file: "outer_file.ex"}
307+
308+
stacktrace =
309+
try do
310+
Code.eval_string(
311+
"""
312+
Code.eval_string("1 + 1")
313+
raise "boom"
314+
""",
315+
[],
316+
env
317+
)
318+
rescue
319+
_ -> __STACKTRACE__
320+
end
321+
322+
assert Enum.any?(stacktrace, fn
323+
{_, _, _, meta} -> Keyword.get(meta, :file) == ~c"outer_file.ex"
324+
end)
325+
end
291326
end
292327

293328
describe "eval_quoted/1" do

0 commit comments

Comments
 (0)