File 1387-erts-stdlib-Fix-ets-update_counter-4.patch of Package erlang
From 1e9ed964687e967b04d273ce7c7d6334e1eeab05 Mon Sep 17 00:00:00 2001
From: Jan Uhlig <juhlig@hnc-agency.org>
Date: Mon, 2 Feb 2026 13:30:17 +0100
Subject: [PATCH] erts,stdlib: Fix ets:update_counter/4
Reject a too small default tuple without key element
and thereby avoid dangerous out of bound key accesses.
For backward (bug)-compatibility reason we only badarg
if the key does not exist and bad default tuple is actually used.
---
erts/emulator/beam/erl_db.c | 3 ++-
erts/emulator/beam/erl_db_hash.c | 5 ++++-
erts/emulator/beam/erl_db_tree.c | 4 +++-
lib/stdlib/src/erl_stdlib_errors.erl | 9 ++++++++-
lib/stdlib/test/ets_SUITE.erl | 23 ++++++++++++++++++++++-
5 files changed, 39 insertions(+), 5 deletions(-)
diff --git a/erts/emulator/beam/erl_db.c b/erts/emulator/beam/erl_db.c
index 8f5e1a9543..fae34e5274 100644
--- a/erts/emulator/beam/erl_db.c
+++ b/erts/emulator/beam/erl_db.c
@@ -58,6 +58,7 @@
#define EXI_POSITION am_position /* The position is out of range. */
#define EXI_OWNER am_owner /* The receiving process is already the owner. */
#define EXI_NOT_OWNER am_not_owner /* The current process is not the owner. */
+#define EXI_DEFAULT am_default /* Invalid default tuple */
erts_atomic_t erts_ets_misc_mem_size;
@@ -1335,7 +1336,7 @@ do_update_counter(Process *p, DbTable* tb,
}
if (!tb->common.meth->db_lookup_dbterm(p, tb, arg2, arg4, &handle)) {
- p->fvalue = EXI_BAD_KEY;
+ p->fvalue = is_value(arg4) ? EXI_DEFAULT : EXI_BAD_KEY;
cret = DB_ERROR_BADPARAM;
goto bail_out; /* key not found */
}
diff --git a/erts/emulator/beam/erl_db_hash.c b/erts/emulator/beam/erl_db_hash.c
index fa2fa7e67e..1c52bd2282 100644
--- a/erts/emulator/beam/erl_db_hash.c
+++ b/erts/emulator/beam/erl_db_hash.c
@@ -3839,7 +3839,10 @@ db_lookup_dbterm_hash(Process *p, DbTable *tbl, Eterm key, Eterm obj,
int arity = arityval(*objp);
Eterm *htop, *hend;
- ASSERT(arity >= tb->common.keypos);
+ if (arity < tb->common.keypos) {
+ WUNLOCK_HASH_LCK_CTR(lck_ctr);
+ return 0;
+ }
htop = HAlloc(p, arity + 1);
hend = htop + arity + 1;
sys_memcpy(htop, objp, sizeof(Eterm) * (arity + 1));
diff --git a/erts/emulator/beam/erl_db_tree.c b/erts/emulator/beam/erl_db_tree.c
index c1ad86945f..f36b1c6c3c 100644
--- a/erts/emulator/beam/erl_db_tree.c
+++ b/erts/emulator/beam/erl_db_tree.c
@@ -3422,7 +3422,9 @@ int db_lookup_dbterm_tree_common(Process *p, DbTable *tbl, TreeDbTerm **root,
int arity = arityval(*objp);
Eterm *htop, *hend;
- ASSERT(arity >= tbl->common.keypos);
+ if (arity < tbl->common.keypos) {
+ return 0;
+ }
htop = HAlloc(p, arity + 1);
hend = htop + arity + 1;
sys_memcpy(htop, objp, sizeof(Eterm) * (arity + 1));
diff --git a/lib/stdlib/src/erl_stdlib_errors.erl b/lib/stdlib/src/erl_stdlib_errors.erl
index 45005047bc..1b4528989f 100644
--- a/lib/stdlib/src/erl_stdlib_errors.erl
+++ b/lib/stdlib/src/erl_stdlib_errors.erl
@@ -745,7 +745,7 @@ format_ets_error(update_counter, [_,_,UpdateOp,Default]=Args, Cause) ->
"" ->
%% The table is OK. The error is in one or more of the
%% other arguments.
- TupleCause = format_tuple(Default),
+ TupleCause = format_default_tuple(Default, Cause),
case Cause of
badkey ->
["", bad_key, format_update_op(UpdateOp) | TupleCause];
@@ -828,6 +828,11 @@ format_non_negative_integer(N) ->
format_object([_,Object|_]=Args, Cause) ->
[format_cause(Args, Cause) | format_tuple(Object)].
+format_default_tuple(Tuple, default) when is_tuple(Tuple) ->
+ [<<"default tuple too small">>];
+format_default_tuple(Term, _Cause) ->
+ format_tuple(Term).
+
format_tuple(Term) ->
if tuple_size(Term) > 0 -> [""];
is_tuple(Term) -> [empty_tuple];
@@ -877,6 +882,8 @@ format_cause(Args, Cause) ->
owner ->
"";
not_owner ->
+ "";
+ default ->
""
end.
diff --git a/lib/stdlib/test/ets_SUITE.erl b/lib/stdlib/test/ets_SUITE.erl
index 142076bde0..ecf1b36e22 100644
--- a/lib/stdlib/test/ets_SUITE.erl
+++ b/lib/stdlib/test/ets_SUITE.erl
@@ -64,7 +64,8 @@
fixtable_insert/1, rename/1, rename_unnamed/1, evil_rename/1,
update_element/1, update_counter/1, evil_update_counter/1, partly_bound/1, match_heavy/1]).
-export([update_counter_with_default/1]).
--export([update_counter_with_default_bad_pos/1]).
+-export([update_counter_with_default_bad_pos/1,
+ update_counter_with_default_bad_default/1]).
-export([update_counter_table_growth/1]).
-export([member/1]).
-export([memory/1]).
@@ -152,6 +153,7 @@ all() ->
update_counter, evil_update_counter,
update_counter_with_default,
update_counter_with_default_bad_pos,
+ update_counter_with_default_bad_default,
partly_bound,
update_counter_table_growth,
match_heavy, {group, fold}, member, t_delete_object,
@@ -3033,6 +3035,25 @@ update_counter_with_default_bad_pos_do(Opts) ->
0 = ets:info(T, size),
ok.
+update_counter_with_default_bad_default(Config) when is_list(Config) ->
+ repeat_for_opts_all_set_table_types(fun update_counter_with_default_bad_default_do/1).
+
+update_counter_with_default_bad_default_do(Opts) ->
+ T = ets_new(a, [{keypos, 3} | Opts]),
+ 0 = ets:info(T, size),
+ ok = try ets:update_counter(T, key, {2, 1}, {0, 0})
+ catch
+ error:badarg -> ok;
+ Class:Reason -> {Class, Reason}
+ end,
+ 0 = ets:info(T, size),
+
+ %% Ignore bad default object if key exist (for backward bug-compat)
+ true = ets:insert(T, {0,0,key}),
+ 1 = ets:update_counter(T, key, {2, 1}, {0, 0}),
+ 1 = ets:info(T, size),
+ ok.
+
update_counter_table_growth(_Config) ->
repeat_for_opts(fun update_counter_table_growth_do/1).
--
2.51.0