Skip to content

Commit cc68ad9

Browse files
authored
Add shallow validation when unquoting AST (#13950)
1 parent 9162de7 commit cc68ad9

File tree

7 files changed

+81
-14
lines changed

7 files changed

+81
-14
lines changed

lib/elixir/lib/macro.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,8 @@ defmodule Macro do
814814
815815
iex> value = {:a, :b, :c}
816816
iex> quote do: unquote(value)
817-
{:a, :b, :c}
817+
** (ArgumentError) tried to unquote invalid AST: {:a, :b, :c}
818+
Did you forget to escape term using Macro.escape/1?
818819
819820
`escape/2` is used to escape *values* (either directly passed or variable
820821
bound), while `quote/2` produces syntax trees for

lib/elixir/src/elixir_quote.erl

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
-module(elixir_quote).
22
-export([escape/3, linify/3, linify_with_context_counter/3, build/7, quote/2, has_unquotes/1, fun_to_quoted/1]).
3-
-export([dot/5, tail_list/3, list/2, validate_runtime/2]). %% Quote callbacks
3+
-export([dot/5, tail_list/3, list/2, validate_runtime/2, shallow_validate_ast/1]). %% Quote callbacks
44

55
-include("elixir.hrl").
66
-define(defs(Kind), Kind == def; Kind == defp; Kind == defmacro; Kind == defmacrop; Kind == '@').
@@ -15,7 +15,8 @@
1515
aliases_hygiene=nil,
1616
imports_hygiene=nil,
1717
unquote=true,
18-
generated=false
18+
generated=false,
19+
shallow_validate=false
1920
}).
2021

2122
%% fun_to_quoted
@@ -215,7 +216,8 @@ build(Meta, Line, File, Context, Unquote, Generated, E) ->
215216
file=VFile,
216217
unquote=Unquote,
217218
context=VContext,
218-
generated=Generated
219+
generated=Generated,
220+
shallow_validate=true
219221
},
220222

221223
{Q, VContext, Acc3}.
@@ -254,6 +256,27 @@ is_valid(context, Context) -> is_atom(Context) andalso (Context /= nil);
254256
is_valid(generated, Generated) -> is_boolean(Generated);
255257
is_valid(unquote, Unquote) -> is_boolean(Unquote).
256258

259+
shallow_validate_ast(Expr) ->
260+
case shallow_valid_ast(Expr) of
261+
true -> Expr;
262+
false -> argument_error(
263+
<<"tried to unquote invalid AST: ", ('Elixir.Kernel':inspect(Expr))/binary,
264+
"\nDid you forget to escape term using Macro.escape/1?">>)
265+
end.
266+
267+
shallow_valid_ast(Expr) when is_list(Expr) -> valid_ast_list(Expr);
268+
shallow_valid_ast(Expr) -> valid_ast_elem(Expr).
269+
270+
valid_ast_list([]) -> true;
271+
valid_ast_list([Head | Tail]) -> valid_ast_elem(Head) andalso valid_ast_list(Tail);
272+
valid_ast_list(_Improper) -> false.
273+
274+
valid_ast_elem(Expr) when is_list(Expr); is_atom(Expr); is_binary(Expr); is_number(Expr); is_pid(Expr) -> true;
275+
valid_ast_elem({Left, Right}) -> valid_ast_elem(Left) andalso valid_ast_elem(Right);
276+
valid_ast_elem({Atom, Meta, Args}) when is_atom(Atom), is_list(Meta), is_atom(Args) orelse is_list(Args) -> true;
277+
valid_ast_elem({Call, Meta, Args}) when is_list(Meta), is_list(Args) -> shallow_valid_ast(Call);
278+
valid_ast_elem(_Term) -> false.
279+
257280
quote({unquote_splicing, _, [_]}, #elixir_quote{unquote=true}) ->
258281
argument_error(<<"unquote_splicing only works inside arguments and block contexts, "
259282
"wrap it in parens if you want it to work with one-liners">>);
@@ -283,8 +306,12 @@ do_quote({quote, Meta, [Opts, Arg]}, Q) when is_list(Meta) ->
283306

284307
{'{}', [], [quote, meta(NewMeta, Q), [TOpts, TArg]]};
285308

286-
do_quote({unquote, Meta, [Expr]}, #elixir_quote{unquote=true}) when is_list(Meta) ->
287-
Expr;
309+
%
310+
do_quote({unquote, Meta, [Expr]}, #elixir_quote{unquote=true, shallow_validate=Validate}) when is_list(Meta) ->
311+
case Validate of
312+
true -> {{'.', Meta, [?MODULE, shallow_validate_ast]}, Meta, [Expr]};
313+
false -> Expr
314+
end;
288315

289316
%% Aliases
290317

lib/elixir/test/elixir/code_normalizer/quoted_ast_test.exs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -405,12 +405,16 @@ defmodule Code.Normalizer.QuotedASTTest do
405405
end
406406

407407
test "range" do
408-
assert quoted_to_string(quote(do: unquote(-1..+2))) == "-1..2"
408+
assert quoted_to_string(quote(do: -1..+2)) == "-1..+2"
409409
assert quoted_to_string(quote(do: Foo.integer()..3)) == "Foo.integer()..3"
410-
assert quoted_to_string(quote(do: unquote(-1..+2//-3))) == "-1..2//-3"
410+
assert quoted_to_string(quote(do: -1..+2//-3)) == "-1..+2//-3"
411411

412412
assert quoted_to_string(quote(do: Foo.integer()..3//Bar.bat())) ==
413413
"Foo.integer()..3//Bar.bat()"
414+
415+
# invalid AST
416+
assert quoted_to_string(-1..+2) == "-1..2"
417+
assert quoted_to_string(-1..+2//-3) == "-1..2//-3"
414418
end
415419

416420
test "when" do

lib/elixir/test/elixir/kernel/expansion_test.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2871,11 +2871,11 @@ defmodule Kernel.ExpansionTest do
28712871

28722872
test "handles invalid expressions" do
28732873
assert_compile_error(~r"invalid quoted expression: {1, 2, 3}", fn ->
2874-
expand_env(quote(do: unquote({1, 2, 3})), __ENV__)
2874+
expand_env({1, 2, 3}, __ENV__)
28752875
end)
28762876

28772877
assert_compile_error(~r"invalid quoted expression: #Function\<", fn ->
2878-
expand(quote(do: unquote({:sample, fn -> nil end})))
2878+
expand({:sample, fn -> nil end})
28792879
end)
28802880

28812881
assert_compile_error(~r"invalid pattern in match", fn ->

lib/elixir/test/elixir/kernel/quote_test.exs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,4 +789,29 @@ defmodule Kernel.QuoteTest.HasUnquoteTest do
789789

790790
refute :elixir_quote.has_unquotes(ast)
791791
end
792+
793+
test "unquote with invalid AST (shallow check)" do
794+
for term <- [
795+
%{unescaped: :map},
796+
1..10,
797+
{:bad_meta, nil, []},
798+
{:bad_arg, nil, 1},
799+
{:bad_tuple},
800+
make_ref(),
801+
[:improper | :list],
802+
[nested: {}]
803+
] do
804+
message = """
805+
tried to unquote invalid AST: #{inspect(term)}
806+
Did you forget to escape term using Macro.escape/1?\
807+
"""
808+
809+
assert_raise ArgumentError, message, fn -> quote do: unquote(term) end
810+
end
811+
end
812+
813+
test "unquote with invalid AST is not checked deeply" do
814+
assert quote do: unquote(foo: [1 | 2]) == [foo: [1 | 2]]
815+
assert quote do: unquote(foo: [bar: %{}]) == [foo: [bar: %{}]]
816+
end
792817
end

lib/elixir/test/elixir/macro_test.exs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -839,7 +839,7 @@ defmodule MacroTest do
839839
end
840840

841841
test "converts invalid AST with inspect" do
842-
assert Macro.to_string(quote do: unquote(1..3)) == "1..3"
842+
assert Macro.to_string(1..3) == "1..3"
843843
end
844844
end
845845

@@ -1172,12 +1172,16 @@ defmodule MacroTest do
11721172
end
11731173

11741174
test "range" do
1175-
assert macro_to_string(quote(do: unquote(-1..+2))) == "-1..2"
1175+
assert macro_to_string(quote(do: -1..+2)) == "-1..+2"
11761176
assert macro_to_string(quote(do: Foo.integer()..3)) == "Foo.integer()..3"
1177-
assert macro_to_string(quote(do: unquote(-1..+2//-3))) == "-1..2//-3"
1177+
assert macro_to_string(quote(do: -1..+2//-3)) == "-1..+2//-3"
11781178

11791179
assert macro_to_string(quote(do: Foo.integer()..3//Bar.bat())) ==
11801180
"Foo.integer()..3//Bar.bat()"
1181+
1182+
# invalid AST
1183+
assert macro_to_string(-1..+2) == "-1..2"
1184+
assert macro_to_string(-1..+2//-3) == "-1..2//-3"
11811185
end
11821186

11831187
test "when" do

lib/mix/test/mix/tasks/xref_test.exs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,13 @@ defmodule Mix.Tasks.XrefTest do
6363
}
6464

6565
output = [
66-
%{callee: {A, :b, 1}, caller_module: B, file: "lib/b.ex", line: 3}
66+
%{callee: {A, :b, 1}, caller_module: B, file: "lib/b.ex", line: 3},
67+
%{
68+
callee: {:elixir_quote, :shallow_validate_ast, 1},
69+
caller_module: A,
70+
file: "lib/a.ex",
71+
line: 4
72+
}
6773
]
6874

6975
assert_all_calls(files, output)

0 commit comments

Comments
 (0)