From bad018c5c64563d31c3c8e08d263024fb3337a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20M=C3=A4nnchen?= Date: Fri, 29 May 2026 11:35:14 +0200 Subject: [PATCH] kernel/erl_boot_server: harden binary_to_term against unknown atoms handle_command/3 decoded raw TCP data from whitelisted clients with binary_to_term/1. Even though boot clients are fully trusted, decoding untrusted-shaped payloads with the default options is unnecessary: any atoms embedded in a payload are permanently interned before pattern matching, so a malformed message could grow the atom table. As a precaution, switch to binary_to_term(Msg, [safe]), which raises badarg instead of interning unknown atoms or creating new funs. The existing catch wraps this as {'EXIT', badarg}, now replied to the client as {error, bad_command}. This is a defence-in-depth hardening change, not a security fix; the module already assumes a complete trust relationship with its clients. --- lib/kernel/src/erl_boot_server.erl | 8 +++-- lib/kernel/test/erl_boot_server_SUITE.erl | 39 +++++++++++++++++++++-- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/lib/kernel/src/erl_boot_server.erl b/lib/kernel/src/erl_boot_server.erl index f35873f9ce1b..1a111f91794a 100644 --- a/lib/kernel/src/erl_boot_server.erl +++ b/lib/kernel/src/erl_boot_server.erl @@ -54,8 +54,7 @@ and `m:erl_prim_loader` in ERTS. [`erts:init`](`m:init`), [`erts:erl_prim_loader`](`m:erl_prim_loader`) """. --compile([{nowarn_possibly_unsafe_function, {erlang, binary_to_term, 1}}, - nowarn_deprecated_catch]). +-compile([nowarn_deprecated_catch]). -include("inet_boot.hrl"). @@ -449,7 +448,7 @@ boot_loop(Socket, PS) -> end. handle_command(S, PS, Msg) -> - case catch binary_to_term(Msg) of + case catch binary_to_term(Msg, [safe]) of {get,File} -> {Res, PS2} = erl_prim_loader:prim_read_file(PS, File), send_file_result(S, get, Res), @@ -474,6 +473,9 @@ handle_command(S, PS, Msg) -> {Res, PS2} = erl_prim_loader:prim_get_cwd(PS, [Drive]), send_file_result(S, get_cwd, Res), PS2; + {'EXIT',{badarg, [{erlang,binary_to_term,_,_}|_]}} -> + send_result(S, {error,bad_command}), + PS; {'EXIT',Reason} -> send_result(S, {error,Reason}), PS; diff --git a/lib/kernel/test/erl_boot_server_SUITE.erl b/lib/kernel/test/erl_boot_server_SUITE.erl index bdff3f468f17..e6d17bb33937 100644 --- a/lib/kernel/test/erl_boot_server_SUITE.erl +++ b/lib/kernel/test/erl_boot_server_SUITE.erl @@ -26,7 +26,8 @@ -export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, init_per_group/2,end_per_group/2]). --export([start/1, start_link/1, stop/1, add/1, delete/1, responses/1]). +-export([start/1, start_link/1, stop/1, add/1, delete/1, responses/1, + unsafe_binary_to_term/1]). %%----------------------------------------------------------------- %% Test suite for erl_boot_server. @@ -41,8 +42,8 @@ suite() -> [{ct_hooks,[ts_install_cth]}, {timetrap,{minutes,1}}]. -all() -> - [start, start_link, stop, add, delete, responses]. +all() -> + [start, start_link, stop, add, delete, responses, unsafe_binary_to_term]. groups() -> []. @@ -388,6 +389,38 @@ responses(Config) when is_list(Config) -> Ret. +%% Tests that sending a binary containing a new atom returns {error,bad_command} +%% and does NOT intern the atom into the atom table. This guards the use of +%% binary_to_term/2 with the [safe] option as a defence-in-depth measure. +%% The payload is a pre-built binary for the atom 'boot_server_new_atom'. +unsafe_binary_to_term(Config) when is_list(Config) -> + process_flag(trap_exit, true), + {ok, BootPid} = erl_boot_server:start_link(["127.0.0.1"]), + + State = sys:get_state(boot_server), + ListenPort = element(7, State), + + %% Binary encodes the atom 'boot_server_new_atom' (never seen by this node). + Payload = <<131, 119, 20, 98, 111, 111, 116, 95, 115, 101, 114, 118, 101, + 114, 95, 110, 101, 119, 95, 97, 116, 111, 109>>, + + AtomsBefore = erlang:system_info(atom_count), + + {ok, Sock} = gen_tcp:connect({127,0,0,1}, ListenPort, + [binary, {packet, 4}, {active, false}], 5000), + ok = gen_tcp:send(Sock, Payload), + {ok, RespBin} = gen_tcp:recv(Sock, 0, 5000), + gen_tcp:close(Sock), + + AtomsAfter = erlang:system_info(atom_count), + + {error, bad_command} = binary_to_term(RespBin), + 0 = AtomsAfter - AtomsBefore, + + shutdown(BootPid), + process_flag(trap_exit, false), + ok. + shutdown(Pid) -> exit(Pid, shutdown), receive