From 716b2155ec129567e9e509872412d986549270cb Mon Sep 17 00:00:00 2001 From: Alex Naser Date: Thu, 18 Jun 2026 13:59:44 +0200 Subject: [PATCH] erts: Make module and export table limits tunable Add the +zmml and +zmel emulator flags to set the maximum number of entries in the module_code and export_list loader index tables, raising the previously hardcoded MODULE_LIMIT (64K) and EXPORT_LIMIT (512K) caps. Expose the current limits and counts via erlang:system_info/1 with the new module_limit, module_count, export_limit and export_count keys, and document the flags and system_info keys. --- erts/doc/references/erl_cmd.md | 18 +++ erts/doc/src/erlang_system_info.md | 27 +++++ erts/emulator/beam/erl_bif_info.c | 12 ++ erts/emulator/beam/erl_init.c | 40 ++++++- erts/emulator/beam/export.c | 14 ++- erts/emulator/beam/export.h | 3 +- erts/emulator/beam/module.c | 17 ++- erts/emulator/beam/module.h | 3 +- erts/emulator/test/system_info_SUITE.erl | 144 ++++++++++++++++++++++- erts/etc/common/erlexec.c | 2 + erts/preloaded/src/erlang.erl | 4 + 11 files changed, 271 insertions(+), 13 deletions(-) diff --git a/erts/doc/references/erl_cmd.md b/erts/doc/references/erl_cmd.md index ab5c80f5fcdf..e9fa1137baca 100644 --- a/erts/doc/references/erl_cmd.md +++ b/erts/doc/references/erl_cmd.md @@ -1455,6 +1455,24 @@ behavior of earlier flags. Since: OTP 27.0 + - **`+zmml limit`{: #+zmml }** - Sets the maximum number of entries in the + `module_code` loader index table, that is, the maximum number of modules + that can be loaded into the runtime system. Valid range of this limit is + `[1, 2147483647]` (on 32-bit systems the maximum is `134217727`). Defaults + to 65536. + + The current value can be read by Erlang code by calling + [`erlang:system_info(module_limit)`](`m:erlang#system_info_module_limit`). + + - **`+zmel limit`{: #+zmel }** - Sets the maximum number of entries in the + `export_list` loader index table, that is, the maximum number of exported + functions that can be loaded into the runtime system. Valid range of this + limit is `[1, 2147483647]` (on 32-bit systems the maximum is `134217727`). + Defaults to 524288. + + The current value can be read by Erlang code by calling + [`erlang:system_info(export_limit)`](`m:erlang#system_info_export_limit`). + ## Environment Variables - **`ERL_CRASH_DUMP`** - If the emulator needs to write a crash dump, the value diff --git a/erts/doc/src/erlang_system_info.md b/erts/doc/src/erlang_system_info.md index 72a51dd7bfd0..2ef81aaa3bbe 100644 --- a/erts/doc/src/erlang_system_info.md +++ b/erts/doc/src/erlang_system_info.md @@ -51,6 +51,10 @@ order to make it easier to navigate. [`atom_limit`](`m:erlang#system_info_atom_limit`), [`ets_count`](`m:erlang#system_info_ets_count`), [`ets_limit`](`m:erlang#system_info_ets_limit`), + [`export_count`](`m:erlang#system_info_export_count`), + [`export_limit`](`m:erlang#system_info_export_limit`), + [`module_count`](`m:erlang#system_info_module_count`), + [`module_limit`](`m:erlang#system_info_module_limit`), [`port_count`](`m:erlang#system_info_port_count`), [`port_limit`](`m:erlang#system_info_port_limit`), [`process_count`](`m:erlang#system_info_process_count`), @@ -374,6 +378,29 @@ Returns information about the current system (emulator) limits as specified by ` Since: OTP R16B03 +- `export_count`{: #system_info_export_count } - Returns the number of exported + functions currently loaded at the local node. The value is given as an + integer. + + Since: OTP 30.0 + +- `export_limit`{: #system_info_export_limit } - Returns the maximum number of + exported functions allowed. This limit can be changed at startup by passing + command-line flag [`+zmel`](erl_cmd.md#+zmel) to `erl(1)`. + + Since: OTP 30.0 + +- `module_count`{: #system_info_module_count } - Returns the number of modules + currently loaded at the local node. The value is given as an integer. + + Since: OTP 30.0 + +- `module_limit`{: #system_info_module_limit } - Returns the maximum number of + modules allowed. This limit can be changed at startup by passing command-line + flag [`+zmml`](erl_cmd.md#+zmml) to `erl(1)`. + + Since: OTP 30.0 + - `port_count`{: #system_info_port_count } - Returns the number of ports currently existing at the local node. The value is given as an integer. This is the same value as returned by `length(erlang:ports())`, but more diff --git a/erts/emulator/beam/erl_bif_info.c b/erts/emulator/beam/erl_bif_info.c index 7bfd05aa1b55..0954840c6af9 100644 --- a/erts/emulator/beam/erl_bif_info.c +++ b/erts/emulator/beam/erl_bif_info.c @@ -3521,6 +3521,18 @@ BIF_RETTYPE system_info_1(BIF_ALIST_1) else if (ERTS_IS_ATOM_STR("atom_count",BIF_ARG_1)) { BIF_RET(make_small(atom_table_size())); } + else if (ERTS_IS_ATOM_STR("module_limit",BIF_ARG_1)) { + BIF_RET(make_small(erts_module_table_limit())); + } + else if (ERTS_IS_ATOM_STR("module_count",BIF_ARG_1)) { + BIF_RET(make_small(module_code_size(erts_active_code_ix()))); + } + else if (ERTS_IS_ATOM_STR("export_limit",BIF_ARG_1)) { + BIF_RET(make_small(erts_export_table_limit())); + } + else if (ERTS_IS_ATOM_STR("export_count",BIF_ARG_1)) { + BIF_RET(make_small(export_list_size(erts_active_code_ix()))); + } else if (ERTS_IS_ATOM_STR("tolerant_timeofday",BIF_ARG_1)) { if (erts_has_time_correction() && erts_time_offset_state() == ERTS_TIME_OFFSET_FINAL) { diff --git a/erts/emulator/beam/erl_init.c b/erts/emulator/beam/erl_init.c index 3d246468859c..fc2b1f51d6c5 100644 --- a/erts/emulator/beam/erl_init.c +++ b/erts/emulator/beam/erl_init.c @@ -95,7 +95,9 @@ static void erl_init(int ncpu, int time_correction, ErtsTimeWarpMode time_warp_mode, int node_tab_delete_delay, - ErtsDbSpinCount db_spin_count); + ErtsDbSpinCount db_spin_count, + int module_tab_sz, + int export_tab_sz); static erts_atomic_t exiting; @@ -255,7 +257,9 @@ erl_init(int ncpu, int time_correction, ErtsTimeWarpMode time_warp_mode, int node_tab_delete_delay, - ErtsDbSpinCount db_spin_count) + ErtsDbSpinCount db_spin_count, + int module_tab_sz, + int export_tab_sz) { init_global_literals(); erts_monitor_link_init(); @@ -283,9 +287,9 @@ erl_init(int ncpu, erts_code_ix_init(); erts_init_fun_table(); init_atom_table(); - init_export_table(); + init_export_table(export_tab_sz); erts_record_init_table(); - init_module_table(); + init_module_table(module_tab_sz); init_register_table(); init_message(); #ifdef BEAMASM @@ -1310,6 +1314,8 @@ erl_start(int argc, char **argv) ErtsTimeWarpMode time_warp_mode; int node_tab_delete_delay = ERTS_NODE_TAB_DELAY_GC_DEFAULT; ErtsDbSpinCount db_spin_count = ERTS_DB_SPNCNT_NORMAL; + int module_tab_sz = 0; + int export_tab_sz = 0; /* Must be set up as early as possible for crash dump encryption to work * properly. */ @@ -2446,6 +2452,28 @@ erl_start(int argc, char **argv) erts_halt_flush_timeout = (ErtsMonotonicTime) val; } } + else if (has_prefix("mml", sub_param)) { + long val; + arg = get_arg(sub_param+3, argv[i+1], &i); + errno = 0; + val = strtol(arg, NULL, 10); + if (errno != 0 || val < 1 || MAX_SMALL < val || INT_MAX < val) { + erts_fprintf(stderr, "Invalid module table limit %s\n", arg); + erts_usage(); + } + module_tab_sz = (int) val; + } + else if (has_prefix("mel", sub_param)) { + long val; + arg = get_arg(sub_param+3, argv[i+1], &i); + errno = 0; + val = strtol(arg, NULL, 10); + if (errno != 0 || val < 1 || MAX_SMALL < val || INT_MAX < val) { + erts_fprintf(stderr, "Invalid export table limit %s\n", arg); + erts_usage(); + } + export_tab_sz = (int) val; + } else { erts_fprintf(stderr, "bad -z option %s\n", argv[i]); erts_usage(); @@ -2529,7 +2557,9 @@ erl_start(int argc, char **argv) time_correction, time_warp_mode, node_tab_delete_delay, - db_spin_count); + db_spin_count, + module_tab_sz, + export_tab_sz); load_preloaded(); erts_end_staging_code_ix(); diff --git a/erts/emulator/beam/export.c b/erts/emulator/beam/export.c index f5fae5fdb0db..6a215c47c99a 100644 --- a/erts/emulator/beam/export.c +++ b/erts/emulator/beam/export.c @@ -35,6 +35,8 @@ #define EXPORT_INITIAL_SIZE 4000 #define EXPORT_LIMIT (512*1024) +static int export_limit = EXPORT_LIMIT; + #ifdef DEBUG # define IF_DEBUG(x) x #else @@ -115,7 +117,7 @@ static void export_stage(Export *export, #define ERTS_CODE_STAGED_OBJECT_ALLOC_TYPE ERTS_ALC_T_EXPORT #define ERTS_CODE_STAGED_TABLE_ALLOC_TYPE ERTS_ALC_T_EXPORT_TABLE #define ERTS_CODE_STAGED_TABLE_INITIAL_SIZE EXPORT_INITIAL_SIZE -#define ERTS_CODE_STAGED_TABLE_LIMIT EXPORT_LIMIT +#define ERTS_CODE_STAGED_TABLE_LIMIT export_limit #define ERTS_CODE_STAGED_WANT_GET #define ERTS_CODE_STAGED_WANT_PUT @@ -128,11 +130,19 @@ static void export_stage(Export *export, #include "erl_code_staged.h" void -init_export_table(void) +init_export_table(int limit) { + if (limit > 0) { + export_limit = limit; + } export_staged_init(); } +int erts_export_table_limit(void) +{ + return export_limit; +} + void export_info(fmtfn_t to, void *to_arg) { diff --git a/erts/emulator/beam/export.h b/erts/emulator/beam/export.h index 7a218f27b785..f95fcc0d94b6 100644 --- a/erts/emulator/beam/export.h +++ b/erts/emulator/beam/export.h @@ -122,8 +122,9 @@ typedef struct export_ #define DBG_CHECK_EXPORT(EP, CX) #endif -void init_export_table(void); +void init_export_table(int limit); void export_info(fmtfn_t, void *); +int erts_export_table_limit(void); ERTS_GLB_INLINE void erts_activate_export_trampoline(Export *ep, int code_ix); ERTS_GLB_INLINE int erts_is_export_trampoline_active(const Export * const ep, int code_ix); diff --git a/erts/emulator/beam/module.c b/erts/emulator/beam/module.c index fb4defbc55eb..2e9a041a899b 100644 --- a/erts/emulator/beam/module.c +++ b/erts/emulator/beam/module.c @@ -43,6 +43,8 @@ #define MODULE_SIZE 50 #define MODULE_LIMIT (64*1024) +static int module_limit = MODULE_LIMIT; + static IndexTable module_tables[ERTS_NUM_CODE_IX]; erts_rwmtx_t the_old_code_rwlocks[ERTS_NUM_CODE_IX]; @@ -103,11 +105,15 @@ static void module_free(Module* mod) erts_atomic_add_nob(&tot_module_bytes, -sizeof(Module)); } -void init_module_table(void) +void init_module_table(int limit) { HashFunctions f; int i; + if (limit > 0) { + module_limit = limit; + } + f.hash = (H_FUN) module_hash; f.cmp = (HCMP_FUN) module_cmp; f.alloc = (HALLOC_FUN) module_alloc; @@ -117,8 +123,8 @@ void init_module_table(void) f.meta_print = (HMPRINT_FUN) erts_print; for (i = 0; i < ERTS_NUM_CODE_IX; i++) { - erts_index_init(ERTS_ALC_T_MODULE_TABLE, &module_tables[i], "module_code", - MODULE_SIZE, MODULE_LIMIT, f); + erts_index_init(ERTS_ALC_T_MODULE_TABLE, &module_tables[i], "module_code", + MODULE_SIZE, module_limit, f); } for (i=0; i [{ct_hooks,[ts_install_cth]}, {timetrap, {minutes, 2}}]. -all() -> +all() -> [process_count, system_version, misc_smoke_tests, ets_count, heap_size, wordsize, memory, ets_limit, atom_limit, atom_count, + module_limit, export_limit, module_limit_barrier, export_limit_barrier, procs_bug, system_logger]. @@ -555,6 +564,139 @@ atom_count(Config) when is_list(Config) -> ok. +%% Verify system_info(module_limit) reflects the +zmml setting, +%% and that the default (no option) is 65536. +module_limit(Config) when is_list(Config) -> + {ok, Peer, Node} = ?CT_PEER(["+zmml", "70000"]), + 70000 = rpc:call(Node, erlang, system_info, [module_limit]), + peer:stop(Peer), + {ok, DPeer, DNode} = ?CT_PEER([]), + 65536 = rpc:call(DNode, erlang, system_info, [module_limit]), + peer:stop(DPeer), + ok. + +%% Verify system_info(export_limit) reflects the +zmel setting, +%% and that the default (no option) is 524288. +export_limit(Config) when is_list(Config) -> + {ok, Peer, Node} = ?CT_PEER(["+zmel", "600000"]), + 600000 = rpc:call(Node, erlang, system_info, [export_limit]), + peer:stop(Peer), + {ok, DPeer, DNode} = ?CT_PEER([]), + 524288 = rpc:call(DNode, erlang, system_info, [export_limit]), + peer:stop(DPeer), + ok. + +%% Verify that the module_code table holds at most 'limit' entries: +%% filling it exactly to the limit succeeds, the next insert crashes +%% the node. +module_limit_barrier(Config) when is_list(Config) -> + %% Probe a default peer to learn the boot-time module count. + {ok, PPeer, PNode} = ?CT_PEER([]), + Used = rpc:call(PNode, erlang, system_info, [module_count]), + peer:stop(PPeer), + %% Use a limit that is a multiple of the index page size and strictly + %% above the boot-time count, so the table is crashed at exactly Limit + %% entries (the limit is only enforced at a page boundary). + Limit = page_aligned_limit_above(Used), + + %% Start the real peer with a tight module limit. + {ok, Peer, Node} = ?CT_PEER(["+zmml", integer_to_list(Limit)]), + Limit = rpc:call(Node, erlang, system_info, [module_limit]), + + %% Fill the table exactly to the limit. H is exact for this node + %% regardless of probe accuracy. + C0 = rpc:call(Node, erlang, system_info, [module_count]), + H = Limit - C0, + true = H >= 0, + [begin + Name = list_to_atom("mod_barrier_" ++ integer_to_list(I)), + Bin = gen_mod(Name, 0), + {module, Name} = + rpc:call(Node, code, load_binary, + [Name, "mod_barrier.beam", Bin]) + end || I <- lists:seq(1, H)], + %% Boundary reached: table is exactly full, no crash. + Limit = rpc:call(Node, erlang, system_info, [module_count]), + + %% One more module overflows the table and crashes the node. + erlang:monitor_node(Node, true), + OverName = list_to_atom("mod_barrier_" ++ integer_to_list(H + 1)), + OverBin = gen_mod(OverName, 0), + catch rpc:call(Node, code, load_binary, + [OverName, "mod_barrier.beam", OverBin]), + receive + {nodedown, Node} -> ok + after 30000 -> + ct:fail("node did not crash when module_code table overflowed") + end, + catch peer:stop(Peer), + ok. + +%% Verify that the export_list table holds at most 'limit' entries: +%% filling it exactly to the limit succeeds, the next insert crashes +%% the node. +export_limit_barrier(Config) when is_list(Config) -> + %% Probe a default peer to learn the boot-time export count. + {ok, PPeer, PNode} = ?CT_PEER([]), + Used = rpc:call(PNode, erlang, system_info, [export_count]), + peer:stop(PPeer), + %% Use a limit that is a multiple of the index page size and strictly + %% above the boot-time count, so the table is crashed at exactly Limit + %% entries (the limit is only enforced at a page boundary). + Limit = page_aligned_limit_above(Used), + + %% Start the real peer with a tight export limit (module limit + %% stays at the default). + {ok, Peer, Node} = ?CT_PEER(["+zmel", integer_to_list(Limit)]), + Limit = rpc:call(Node, erlang, system_info, [export_limit]), + + %% Fill the table exactly to the limit with a single module. A + %% module exporting K user functions adds K + 2 export entries + %% (compiler auto-adds module_info/0 and module_info/1), so export + %% H - 2 user functions to reach exactly Limit. + C0 = rpc:call(Node, erlang, system_info, [export_count]), + H = Limit - C0, + true = H >= 2, + FillName = exp_barrier_fill, + FillBin = gen_mod(FillName, H - 2), + {module, FillName} = + rpc:call(Node, code, load_binary, + [FillName, "exp_barrier_fill.beam", FillBin]), + %% Self-validates the +2 accounting: if it is off this fails clearly. + Limit = rpc:call(Node, erlang, system_info, [export_count]), + + %% One more module with at least one export overflows the table and + %% crashes the node. + erlang:monitor_node(Node, true), + OverName = exp_barrier_over, + OverBin = gen_mod(OverName, 1), + catch rpc:call(Node, code, load_binary, + [OverName, "exp_barrier_over.beam", OverBin]), + receive + {nodedown, Node} -> ok + after 30000 -> + ct:fail("node did not crash when export_list table overflowed") + end, + catch peer:stop(Peer), + ok. + +%% Smallest multiple of the index page size strictly greater than N. +page_aligned_limit_above(N) -> + ((N div ?INDEX_PAGE_SIZE) + 1) * ?INDEX_PAGE_SIZE. + +%% Generate a trivial module exporting NExports zero-arity functions, +%% each returning 'ok'. Returns the compiled beam binary. +gen_mod(Name, NExports) -> + Exports = [{list_to_atom("f" ++ integer_to_list(I)), 0} + || I <- lists:seq(1, NExports)], + Forms = [{attribute,0,module,Name}, + {attribute,0,export,Exports}] + ++ [{function,0,F,0,[{clause,0,[],[],[{atom,0,ok}]}]} + || {F,_} <- Exports], + {ok, Name, Bin} = compile:forms(Forms, [return_errors]), + Bin. + + system_logger(Config) when is_list(Config) -> TC = self(), diff --git a/erts/etc/common/erlexec.c b/erts/etc/common/erlexec.c index c80463372eb5..49153dbbf64e 100644 --- a/erts/etc/common/erlexec.c +++ b/erts/etc/common/erlexec.c @@ -175,6 +175,8 @@ static char *plusz_val_switches[] = { "ebwt", "osrl", "hft", + "mml", + "mel", NULL }; diff --git a/erts/preloaded/src/erlang.erl b/erts/preloaded/src/erlang.erl index 53424e1f9afb..6eaddf996ce2 100644 --- a/erts/preloaded/src/erlang.erl +++ b/erts/preloaded/src/erlang.erl @@ -10456,6 +10456,8 @@ the `CpuTopology` type to change. (end_time) -> non_neg_integer(); (ets_count) -> pos_integer(); (ets_limit) -> pos_integer(); + (export_count) -> pos_integer(); + (export_limit) -> pos_integer(); (fullsweep_after) -> {fullsweep_after, non_neg_integer()}; (garbage_collection) -> garbage_collection_defaults(); (heap_sizes) -> [non_neg_integer()]; @@ -10473,6 +10475,8 @@ the `CpuTopology` type to change. (min_bin_vheap_size) -> {min_bin_vheap_size, MinBinVHeapSize :: pos_integer()}; (modified_timing_level) -> integer() | undefined; + (module_count) -> pos_integer(); + (module_limit) -> pos_integer(); (multi_scheduling) -> disabled | blocked | blocked_normal | enabled; (multi_scheduling_blockers) -> [Pid :: pid()]; (nif_version) -> string();