From eb8e404dde527507801ffa00b6a30358bceace44 Mon Sep 17 00:00:00 2001 From: Mateusz Front Date: Tue, 2 Jun 2026 11:58:32 +0000 Subject: [PATCH 01/87] Add support for process aliases Implement Erlang process aliases together with the reference-representation rework they require, rebased onto release-0.7 and squashed into one commit. Public API (libs/estdlib/src/erlang.erl): - alias/0, unalias/1 - monitor/3 with {alias, explicit_unalias | demonitor | reply_demonitor} - spawn_opt {monitor, [monitor_option()]} Runtime: - New process-reference term carrying ref_ticks + owner process id (RefData), alongside short/local references; BEAM-compliant reference ordering and binary_to_term/term_to_binary round-tripping (term.{c,h}, external_term.c). - Monitors store RefData; alias monitors (CONTEXT_MONITOR_ALIAS) with the three alias modes and their lifecycle (context.{c,h}). - send/2, the send opcode and the JIT send path resolve an alias to its target process and drop reply_demonitor aliases after first use (nifs.c, opcodesswitch.h, jit.c). - Reference size macros: REF_SIZE -> TERM_BOXED_REFERENCE_SHORT_SIZE, plus TERM_BOXED_REFERENCE_PROCESS_SIZE and TERM_BOXED_REFERENCE_RESOURCE_SIZE. Tests: alias coverage added to test_monitor, test_refs_ordering and test_binary_to_term. Signed-off-by: Davide Bettio --- libs/estdlib/src/erlang.erl | 54 ++++- src/libAtomVM/context.c | 104 +++++++-- src/libAtomVM/context.h | 45 +++- src/libAtomVM/defaultatoms.def | 6 + src/libAtomVM/ets.c | 2 +- src/libAtomVM/external_term.c | 19 +- src/libAtomVM/globalcontext.h | 2 - src/libAtomVM/jit.c | 36 +++- src/libAtomVM/nifs.c | 199 +++++++++++++++--- src/libAtomVM/nifs.gperf | 3 + src/libAtomVM/opcodesswitch.h | 24 ++- src/libAtomVM/otp_socket.c | 14 +- src/libAtomVM/resources.h | 2 +- src/libAtomVM/term.c | 19 ++ src/libAtomVM/term.h | 126 ++++++++++- .../emscripten/src/lib/websocket_nifs.c | 2 +- .../components/avm_builtins/adc_driver.c | 4 +- .../components/avm_builtins/dac_driver.c | 2 +- .../components/avm_builtins/i2c_resource.c | 2 +- .../components/avm_builtins/network_driver.c | 8 +- .../components/avm_builtins/socket_driver.c | 2 +- .../components/avm_builtins/uart_driver.c | 4 +- src/platforms/rp2/src/lib/networkdriver.c | 6 +- tests/erlang_tests/test_binary_to_term.erl | 17 ++ tests/erlang_tests/test_monitor.erl | 151 +++++++++++++ tests/erlang_tests/test_refs_ordering.erl | 35 ++- 26 files changed, 781 insertions(+), 107 deletions(-) diff --git a/libs/estdlib/src/erlang.erl b/libs/estdlib/src/erlang.erl index 760063cec6..4fdb14225f 100644 --- a/libs/estdlib/src/erlang.erl +++ b/libs/estdlib/src/erlang.erl @@ -104,6 +104,7 @@ make_ref/0, send/2, monitor/2, + monitor/3, demonitor/1, demonitor/2, exit/1, @@ -174,7 +175,9 @@ tl/1, trunc/1, tuple_size/1, - tuple_to_list/1 + tuple_to_list/1, + alias/0, + unalias/1 ]). -export_type([ @@ -212,7 +215,8 @@ | {max_heap_size, pos_integer()} | {atomvm_heap_growth, atomvm_heap_growth_strategy()} | link - | monitor. + | monitor + | {monitor, [monitor_option()]}. -type send_destination() :: pid() @@ -238,6 +242,8 @@ -type raise_stacktrace() :: [{module(), atom(), arity() | [term()]} | {function(), arity() | [term()]}] | stacktrace(). +-type monitor_option() :: {alias, explicit_unalias | demonitor | reply_demonitor}. + %%----------------------------------------------------------------------------- %% @param Time time in milliseconds after which to send the timeout message. %% @param Dest Pid or server name to which to send the timeout message. @@ -1306,6 +1312,28 @@ send(_Target, _Message) -> monitor(_Type, _PidOrPort) -> erlang:nif_error(undefined). +%%----------------------------------------------------------------------------- +%% @param Type type of monitor to create +%% @param PidOrPort pid or port of the object to monitor +%% @param Options monitor options +%% @returns a monitor reference +%% @doc Creates a monitor and allows passing additional options. +%% Currently, only the `{alias, AliasMode}' option is supported. Passing it +%% makes the monitor also an alias on the calling process (see `alias/0'). +%% `AliasMode' defines the behaviour of the alias: +%% - explicit_unalias - the alias can be only removed with `unalias/1', +%% - demonitor - the alias is also removed when `demonitor/1' is called +%% on the monitor, +%% - reply_demonitor - the alias is also removed after a first message +%% is sent via it. +%% @end +%%----------------------------------------------------------------------------- +-spec monitor + (Type :: process, Pid :: pid() | atom(), [monitor_option()]) -> reference(); + (Type :: port, Port :: port() | atom(), [monitor_option()]) -> reference(). +monitor(_Type, _PidOrPort, _Options) -> + erlang:nif_error(undefined). + %%----------------------------------------------------------------------------- %% @param Monitor reference of monitor to remove %% @returns `true' @@ -2147,3 +2175,25 @@ tuple_size(_Tuple) -> -spec tuple_to_list(Tuple :: tuple()) -> [term()]. tuple_to_list(_Tuple) -> erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @returns A reference aliasing the calling process. +%% @doc Creates an alias for the calling process. The alias can be used +%% to send messages to the process like the PID. The alias can also be +%% created along with a monitor - see `monitor/3'. The alias can be +%% removed by calling `unalias/1'. +%% @end +%%----------------------------------------------------------------------------- +-spec alias() -> Alias when Alias :: reference(). +alias() -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Alias the alias to be removed. +%% @returns `true' if alias was removed, `false' if it was not found +%% @doc Removes process alias. See `alias/0' for more information. +%% @end +%%----------------------------------------------------------------------------- +-spec unalias(Alias) -> boolean() when Alias :: reference(). +unalias(_Alias) -> + erlang:nif_error(undefined). diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index b8b29273d5..14da1c327b 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -267,6 +267,7 @@ void context_destroy(Context *ctx) case CONTEXT_MONITOR_MONITORED_LOCAL: case CONTEXT_MONITOR_MONITORING_LOCAL: case CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME: + case CONTEXT_MONITOR_ALIAS: UNREACHABLE(); } } @@ -430,7 +431,7 @@ void context_process_monitor_down_signal(Context *ctx, struct TermSignal *signal struct Monitor *monitor = GET_LIST_ENTRY(item, struct Monitor, monitor_list_head); if (monitor->monitor_type == CONTEXT_MONITOR_MONITORING_LOCAL) { struct MonitorLocalMonitor *monitoring_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); - if (monitoring_monitor->monitor_obj == monitor_obj && monitoring_monitor->ref_ticks == ref_ticks) { + if (monitoring_monitor->monitor_obj == monitor_obj && monitoring_monitor->ref_data.ref_ticks == ref_ticks) { // Remove link list_remove(&monitor->monitor_list_head); free(monitoring_monitor); @@ -441,7 +442,7 @@ void context_process_monitor_down_signal(Context *ctx, struct TermSignal *signal } else if (monitor->monitor_type == CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME) { int32_t monitor_process_id = term_to_local_process_id(monitor_obj); struct MonitorLocalRegisteredNameMonitor *monitoring_monitor = CONTAINER_OF(monitor, struct MonitorLocalRegisteredNameMonitor, monitor); - if (monitoring_monitor->monitor_process_id == monitor_process_id && monitoring_monitor->ref_ticks == ref_ticks) { + if (monitoring_monitor->monitor_process_id == monitor_process_id && monitoring_monitor->ref_data.ref_ticks == ref_ticks) { // Remove link list_remove(&monitor->monitor_list_head); @@ -726,7 +727,7 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) Context *target = globalcontext_get_process_nolock(glb, local_process_id); if (LIKELY(target != NULL)) { // target can be null if we didn't process a MonitorDownSignal - mailbox_send_ref_signal(target, DemonitorSignal, monitoring_monitor->ref_ticks); + mailbox_send_ref_signal(target, DemonitorSignal, monitoring_monitor->ref_data.ref_ticks); } free(monitoring_monitor); break; @@ -738,7 +739,7 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) Context *target = globalcontext_get_process_nolock(glb, local_process_id); if (LIKELY(target != NULL)) { // target can be null if we didn't process a MonitorDownSignal - mailbox_send_ref_signal(target, DemonitorSignal, monitoring_monitor->ref_ticks); + mailbox_send_ref_signal(target, DemonitorSignal, monitoring_monitor->ref_data.ref_ticks); } free(monitoring_monitor); break; @@ -786,7 +787,7 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) Context *target = globalcontext_get_process_nolock(glb, local_process_id); // Target cannot be NULL as we processed Demonitor signals assert(target != NULL); - int required_terms = REF_SIZE + TUPLE_SIZE(5); + int required_terms = TERM_BOXED_REFERENCE_PROCESS_SIZE + TUPLE_SIZE(5); if (UNLIKELY(memory_ensure_free(ctx, required_terms) != MEMORY_GC_OK)) { // TODO: handle out of memory here fprintf(stderr, "Cannot handle out of memory.\n"); @@ -794,7 +795,7 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) AVM_ABORT(); } // Prepare the message on ctx's heap which will be freed afterwards. - term ref = term_from_ref_ticks(monitored_monitor->ref_ticks, &ctx->heap); + term ref = term_from_ref_data(&monitored_monitor->ref_data, &ctx->heap); term port_or_process = term_pid_or_port_from_context(ctx); term port_or_process_atom @@ -811,6 +812,11 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) free(monitored_monitor); break; } + case CONTEXT_MONITOR_ALIAS: { + struct MonitorAlias *alias = CONTAINER_OF(monitor, struct MonitorAlias, monitor); + free(alias); + break; + } } } return result; @@ -850,7 +856,7 @@ struct Monitor *monitor_link_new(term link_pid) } } -struct Monitor *monitor_new(term monitor_pid, uint64_t ref_ticks, bool is_monitoring) +struct Monitor *monitor_new(term monitor_pid, RefData *ref_data, bool is_monitoring) { struct MonitorLocalMonitor *monitor = malloc(sizeof(struct MonitorLocalMonitor)); if (IS_NULL_PTR(monitor)) { @@ -862,12 +868,12 @@ struct Monitor *monitor_new(term monitor_pid, uint64_t ref_ticks, bool is_monito monitor->monitor.monitor_type = CONTEXT_MONITOR_MONITORED_LOCAL; } monitor->monitor_obj = monitor_pid; - monitor->ref_ticks = ref_ticks; + monitor->ref_data = *ref_data; return &monitor->monitor; } -struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, uint64_t ref_ticks) +struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, RefData *ref_data) { struct MonitorLocalRegisteredNameMonitor *monitor = malloc(sizeof(struct MonitorLocalRegisteredNameMonitor)); if (IS_NULL_PTR(monitor)) { @@ -876,7 +882,20 @@ struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, t monitor->monitor.monitor_type = CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME; monitor->monitor_process_id = monitor_process_id; monitor->monitor_name = monitor_name; - monitor->ref_ticks = ref_ticks; + monitor->ref_data = *ref_data; + + return &monitor->monitor; +} + +struct Monitor *monitor_alias_new(RefData *ref_data, enum ContextMonitorAliasType alias_type) +{ + struct MonitorAlias *monitor = malloc(sizeof(struct MonitorAlias)); + if (IS_NULL_PTR(monitor)) { + return NULL; + } + monitor->monitor.monitor_type = CONTEXT_MONITOR_ALIAS; + monitor->ref_data = *ref_data; + monitor->alias_type = alias_type; return &monitor->monitor; } @@ -915,7 +934,7 @@ bool context_add_monitor(Context *ctx, struct Monitor *new_monitor) case CONTEXT_MONITOR_MONITORED_LOCAL: { struct MonitorLocalMonitor *new_local_monitor = CONTAINER_OF(new_monitor, struct MonitorLocalMonitor, monitor); struct MonitorLocalMonitor *existing_local_monitor = CONTAINER_OF(existing, struct MonitorLocalMonitor, monitor); - if (UNLIKELY(existing_local_monitor->monitor_obj == new_local_monitor->monitor_obj && existing_local_monitor->ref_ticks == new_local_monitor->ref_ticks)) { + if (UNLIKELY(existing_local_monitor->monitor_obj == new_local_monitor->monitor_obj && existing_local_monitor->ref_data.ref_ticks == new_local_monitor->ref_data.ref_ticks)) { free(new_local_monitor); return false; } @@ -926,12 +945,22 @@ bool context_add_monitor(Context *ctx, struct Monitor *new_monitor) struct MonitorLocalRegisteredNameMonitor *existing_local_registeredname_monitor = CONTAINER_OF(existing, struct MonitorLocalRegisteredNameMonitor, monitor); if (UNLIKELY(existing_local_registeredname_monitor->monitor_process_id == new_local_registeredname_monitor->monitor_process_id && existing_local_registeredname_monitor->monitor_name == new_local_registeredname_monitor->monitor_name - && existing_local_registeredname_monitor->ref_ticks == new_local_registeredname_monitor->ref_ticks)) { + && existing_local_registeredname_monitor->ref_data.ref_ticks == new_local_registeredname_monitor->ref_data.ref_ticks)) { free(new_local_registeredname_monitor); return false; } break; } + case CONTEXT_MONITOR_ALIAS: { + struct MonitorAlias *new_alias_monitor = CONTAINER_OF(new_monitor, struct MonitorAlias, monitor); + struct MonitorAlias *existing_alias_monitor = CONTAINER_OF(existing, struct MonitorAlias, monitor); + + if (UNLIKELY(existing_alias_monitor->alias_type == new_alias_monitor->alias_type && existing_alias_monitor->ref_data.ref_ticks == new_alias_monitor->ref_data.ref_ticks)) { + free(new_monitor); + return false; + } + break; + } case CONTEXT_MONITOR_RESOURCE: { struct ResourceContextMonitor *new_resource_monitor = CONTAINER_OF(new_monitor, struct ResourceContextMonitor, monitor); struct ResourceContextMonitor *existing_resource_monitor = CONTAINER_OF(existing, struct ResourceContextMonitor, monitor); @@ -1065,6 +1094,11 @@ void context_unlink_ack(Context *ctx, term link_pid, uint64_t unlink_id) void context_demonitor(Context *ctx, uint64_t ref_ticks) { + struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); + if (alias != NULL && alias->alias_type != ContextMonitorAliasExplicitUnalias) { + context_unalias(alias); + } + struct ListHead *item; LIST_FOR_EACH (item, &ctx->monitors_head) { struct Monitor *monitor = GET_LIST_ENTRY(item, struct Monitor, monitor_list_head); @@ -1072,7 +1106,7 @@ void context_demonitor(Context *ctx, uint64_t ref_ticks) case CONTEXT_MONITOR_MONITORING_LOCAL: case CONTEXT_MONITOR_MONITORED_LOCAL: { struct MonitorLocalMonitor *local_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); - if (local_monitor->ref_ticks == ref_ticks) { + if (local_monitor->ref_data.ref_ticks == ref_ticks) { list_remove(&monitor->monitor_list_head); free(local_monitor); return; @@ -1081,7 +1115,7 @@ void context_demonitor(Context *ctx, uint64_t ref_ticks) } case CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME: { struct MonitorLocalRegisteredNameMonitor *local_registeredname_monitor = CONTAINER_OF(monitor, struct MonitorLocalRegisteredNameMonitor, monitor); - if (local_registeredname_monitor->ref_ticks == ref_ticks) { + if (local_registeredname_monitor->ref_data.ref_ticks == ref_ticks) { list_remove(&monitor->monitor_list_head); free(local_registeredname_monitor); return; @@ -1098,11 +1132,36 @@ void context_demonitor(Context *ctx, uint64_t ref_ticks) } case CONTEXT_MONITOR_LINK_LOCAL: case CONTEXT_MONITOR_LINK_REMOTE: + case CONTEXT_MONITOR_ALIAS: break; } } } +struct MonitorAlias *context_find_alias(Context *ctx, uint64_t ref_ticks) +{ + struct ListHead *item; + LIST_FOR_EACH (item, &ctx->monitors_head) { + struct Monitor *monitor = GET_LIST_ENTRY(item, struct Monitor, monitor_list_head); + if (monitor->monitor_type == CONTEXT_MONITOR_ALIAS) { + struct MonitorAlias *alias_monitor = CONTAINER_OF(monitor, struct MonitorAlias, monitor); + if (alias_monitor->ref_data.ref_ticks == ref_ticks) { + return alias_monitor; + } + } + } + + return NULL; +} + +void context_unalias(struct MonitorAlias *alias) +{ + TERM_DEBUG_ASSERT(alias != NULL); + struct Monitor *monitor = &alias->monitor; + list_remove(&monitor->monitor_list_head); + free(alias); +} + term context_get_monitor_pid(Context *ctx, uint64_t ref_ticks, bool *is_monitoring) { struct ListHead *item; @@ -1112,7 +1171,7 @@ term context_get_monitor_pid(Context *ctx, uint64_t ref_ticks, bool *is_monitori case CONTEXT_MONITOR_MONITORING_LOCAL: case CONTEXT_MONITOR_MONITORED_LOCAL: { struct MonitorLocalMonitor *local_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); - if (local_monitor->ref_ticks == ref_ticks) { + if (local_monitor->ref_data.ref_ticks == ref_ticks) { *is_monitoring = monitor->monitor_type == CONTEXT_MONITOR_MONITORING_LOCAL; return local_monitor->monitor_obj; } @@ -1120,7 +1179,7 @@ term context_get_monitor_pid(Context *ctx, uint64_t ref_ticks, bool *is_monitori } case CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME: { struct MonitorLocalRegisteredNameMonitor *local_registeredname_monitor = CONTAINER_OF(monitor, struct MonitorLocalRegisteredNameMonitor, monitor); - if (local_registeredname_monitor->ref_ticks == ref_ticks) { + if (local_registeredname_monitor->ref_data.ref_ticks == ref_ticks) { *is_monitoring = true; return term_from_local_process_id(local_registeredname_monitor->monitor_process_id); } @@ -1129,6 +1188,7 @@ term context_get_monitor_pid(Context *ctx, uint64_t ref_ticks, bool *is_monitori case CONTEXT_MONITOR_LINK_LOCAL: case CONTEXT_MONITOR_LINK_REMOTE: case CONTEXT_MONITOR_RESOURCE: + case CONTEXT_MONITOR_ALIAS: break; } } @@ -1255,7 +1315,13 @@ COLD_FUNC void context_dump(Context *ctx) struct MonitorLocalMonitor *monitoring_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); fprintf(stderr, "monitor to "); term_display(stderr, monitoring_monitor->monitor_obj, ctx); - fprintf(stderr, " ref=%lu", (long unsigned) monitoring_monitor->ref_ticks); + fprintf(stderr, " ref=%lu", (long unsigned) monitoring_monitor->ref_data.ref_ticks); + fprintf(stderr, "\n"); + break; + } + case CONTEXT_MONITOR_ALIAS: { + struct MonitorAlias *monitor_alias = CONTAINER_OF(monitor, struct MonitorAlias, monitor); + fprintf(stderr, "has alias ref=%lu", (long unsigned) monitor_alias->ref_data.ref_ticks); fprintf(stderr, "\n"); break; } @@ -1263,7 +1329,7 @@ COLD_FUNC void context_dump(Context *ctx) struct MonitorLocalMonitor *monitored_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); fprintf(stderr, "monitored by "); term_display(stderr, monitored_monitor->monitor_obj, ctx); - fprintf(stderr, " ref=%lu", (long unsigned) monitored_monitor->ref_ticks); + fprintf(stderr, " ref=%lu", (long unsigned) monitored_monitor->ref_data.ref_ticks); fprintf(stderr, "\n"); break; } @@ -1273,7 +1339,7 @@ COLD_FUNC void context_dump(Context *ctx) term_display(stderr, local_registeredname_monitor->monitor_name, ctx); fprintf(stderr, " ("); term_display(stderr, term_from_local_process_id(local_registeredname_monitor->monitor_process_id), ctx); - fprintf(stderr, ") ref=%lu", (long unsigned) local_registeredname_monitor->ref_ticks); + fprintf(stderr, ") ref=%lu", (long unsigned) local_registeredname_monitor->ref_data.ref_ticks); fprintf(stderr, "\n"); break; } diff --git a/src/libAtomVM/context.h b/src/libAtomVM/context.h index e43f360620..cb9f7e0b1a 100644 --- a/src/libAtomVM/context.h +++ b/src/libAtomVM/context.h @@ -177,6 +177,14 @@ enum ContextMonitorType CONTEXT_MONITOR_RESOURCE, CONTEXT_MONITOR_LINK_REMOTE, CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME, + CONTEXT_MONITOR_ALIAS, +}; + +enum ContextMonitorAliasType +{ + ContextMonitorAliasExplicitUnalias, + ContextMonitorAliasDemonitor, + ContextMonitorAliasReplyDemonitor, }; #define UNLINK_ID_LINK_ACTIVE 0x0 @@ -200,18 +208,25 @@ struct LinkLocalMonitor struct MonitorLocalMonitor { struct Monitor monitor; - uint64_t ref_ticks; + RefData ref_data; term monitor_obj; }; struct MonitorLocalRegisteredNameMonitor { struct Monitor monitor; - uint64_t ref_ticks; + RefData ref_data; int32_t monitor_process_id; term monitor_name; }; +struct MonitorAlias +{ + struct Monitor monitor; + RefData ref_data; + enum ContextMonitorAliasType alias_type; +}; + // The other half is called ResourceMonitor and is a linked list of resources struct ResourceContextMonitor { @@ -511,21 +526,23 @@ struct Monitor *monitor_link_new(term link_pid); * @brief Create a monitor on a process. * * @param monitor_pid monitored process - * @param ref_ticks reference of the monitor + * @param ref_data reference of the monitor * @param is_monitoring if ctx is the monitoring process * @return the allocated monitor or NULL if allocation failed */ -struct Monitor *monitor_new(term monitor_pid, uint64_t ref_ticks, bool is_monitoring); +struct Monitor *monitor_new(term monitor_pid, RefData *ref_data, bool is_monitoring); + +struct Monitor *monitor_alias_new(RefData *ref_data, enum ContextMonitorAliasType alias_type); /** * @brief Create a monitor on a process by registered name. * * @param monitor_process_id monitored process id * @param monitor_name name of the monitor (atom) - * @param ref_ticks reference of the monitor + * @param ref_data reference of the monitor * @return the allocated monitor or NULL if allocation failed */ -struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, uint64_t ref_ticks); +struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, RefData *ref_data); /** * @brief Create a resource monitor. @@ -582,6 +599,22 @@ void context_unlink_ack(Context *ctx, term link_pid, uint64_t unlink_id); */ void context_demonitor(Context *ctx, uint64_t ref_ticks); +/** + * @brief Find a process alias + * + * @param ctx the context being executed + * @param ref_ticks reference of the alias to find + * @return found alias or NULL + */ +struct MonitorAlias *context_find_alias(Context *ctx, uint64_t ref_ticks); + +/** + * @brief Remove an alias of a process + * + * @param alias The alias to remove, can be obtained using context_find_alias + */ +void context_unalias(struct MonitorAlias *alias); + /** * @brief Get target of a monitor. * diff --git a/src/libAtomVM/defaultatoms.def b/src/libAtomVM/defaultatoms.def index 6dc95175d9..229271306f 100644 --- a/src/libAtomVM/defaultatoms.def +++ b/src/libAtomVM/defaultatoms.def @@ -222,3 +222,9 @@ X(JIT_RISCV32_ATOM, "\xB", "jit_riscv32") X(JIT_RISCV64_ATOM, "\xB", "jit_riscv64") X(JIT_WASM32_ATOM, "\xA", "jit_wasm32") X(JIT_XTENSA_ATOM, "\xA", "jit_xtensa") + +X(ALIAS_ATOM, "\x5", "alias") +X(DEMONITOR_ATOM, "\x9", "demonitor") +X(EXPLICIT_UNALIAS_ATOM, "\x10", "explicit_unalias") +X(REPLY_DEMONITOR_ATOM, "\xF", "reply_demonitor") +X(TAG_ATOM, "\x3", "tag") diff --git a/src/libAtomVM/ets.c b/src/libAtomVM/ets.c index b33715b686..f02576a6a1 100644 --- a/src/libAtomVM/ets.c +++ b/src/libAtomVM/ets.c @@ -180,7 +180,7 @@ ets_result_t ets_create_table_maybe_gc( if (named) { *ret = name; } else { - if (UNLIKELY(memory_ensure_free_opt(ctx, REF_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_SHORT_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { ets_multimap_delete(multimap, ctx->global); #ifndef AVM_NO_SMP smp_rwlock_destroy(table->lock); diff --git a/src/libAtomVM/external_term.c b/src/libAtomVM/external_term.c index e0da90a042..448a60d06b 100644 --- a/src/libAtomVM/external_term.c +++ b/src/libAtomVM/external_term.c @@ -521,6 +521,8 @@ static int serialize_term(uint8_t *buf, term t, GlobalContext *glb) uint32_t len; if (term_is_resource_reference(t)) { len = 4; + } else if (term_is_process_reference(t)) { + len = 3; } else { len = 2; } @@ -542,6 +544,15 @@ static int serialize_term(uint8_t *buf, term t, GlobalContext *glb) WRITE_64_UNALIGNED(buf + k + 12, ((uintptr_t) serialize_ref)); } return k + 20; + } else if (term_is_process_reference(t)) { + if (!IS_NULL_PTR(buf)) { + uint64_t ticks = term_to_ref_ticks(t); + uint32_t process_id = term_process_ref_to_process_id(t); + WRITE_32_UNALIGNED(buf + k, creation); + WRITE_64_UNALIGNED(buf + k + 4, ticks); + WRITE_32_UNALIGNED(buf + k + 12, process_id); + } + return k + 16; } else { if (!IS_NULL_PTR(buf)) { uint64_t ticks = term_to_ref_ticks(t); @@ -999,6 +1010,10 @@ static term parse_external_terms(const uint8_t *external_term_buf, size_t *eterm if (len == 2 && node == this_node && creation == this_creation) { uint64_t ticks = ((uint64_t) data[0]) << 32 | data[1]; return term_from_ref_ticks(ticks, heap); + } else if (len == 3 && node == this_node && creation == this_creation) { + uint64_t ticks = ((uint64_t) data[0]) << 32 | data[1]; + uint32_t process_id = data[2]; + return term_make_process_reference(process_id, ticks, heap); } else if (len == 4 && node == this_node && creation == this_creation) { // This is a resource uint64_t resource_type_ptr = ((uint64_t) data[0]) << 32 | data[1]; @@ -1442,7 +1457,9 @@ static int calculate_heap_usage(const uint8_t *external_term_buf, size_t remaini // Check if it's non-distributed node, in which case it's always a local ref if (external_term_buf[4] == strlen("nonode@nohost") && memcmp(external_term_buf + 5, "nonode@nohost", strlen("nonode@nohost")) == 0) { if (len == 2) { - heap_size = REF_SIZE; + heap_size = TERM_BOXED_REFERENCE_SHORT_SIZE; + } else if (len == 3) { + heap_size = TERM_BOXED_REFERENCE_PROCESS_SIZE; } else if (len == 4) { heap_size = TERM_BOXED_REFERENCE_RESOURCE_SIZE; } diff --git a/src/libAtomVM/globalcontext.h b/src/libAtomVM/globalcontext.h index eb7779e9d1..2876aaab80 100644 --- a/src/libAtomVM/globalcontext.h +++ b/src/libAtomVM/globalcontext.h @@ -46,8 +46,6 @@ extern "C" { #endif -#define INVALID_PROCESS_ID 0 - struct Context; #ifndef TYPEDEF_CONTEXT diff --git a/src/libAtomVM/jit.c b/src/libAtomVM/jit.c index 8d8bd5b4f8..f25e6aeef7 100644 --- a/src/libAtomVM/jit.c +++ b/src/libAtomVM/jit.c @@ -840,15 +840,16 @@ static bool jit_send(Context *ctx, JITState *jit_state) return false; } ctx->x[0] = return_value; - } else { - if (term_is_atom(recipient_term)) { - recipient_term = globalcontext_get_registered_process(ctx->global, term_to_atom_index(recipient_term)); - if (UNLIKELY(recipient_term == UNDEFINED_ATOM)) { - set_error(ctx, jit_state, 0, BADARG_ATOM); - return false; - } + } else if (term_is_local_pid_or_port(recipient_term)) { + int local_process_id = term_to_local_process_id(recipient_term); + globalcontext_send_message(ctx->global, local_process_id, ctx->x[1]); + ctx->x[0] = ctx->x[1]; + } else if (term_is_atom(recipient_term)) { + recipient_term = globalcontext_get_registered_process(ctx->global, term_to_atom_index(recipient_term)); + if (UNLIKELY(recipient_term == UNDEFINED_ATOM)) { + set_error(ctx, jit_state, 0, BADARG_ATOM); + return false; } - int local_process_id; if (term_is_local_pid_or_port(recipient_term)) { local_process_id = term_to_local_process_id(recipient_term); @@ -858,7 +859,26 @@ static bool jit_send(Context *ctx, JITState *jit_state) } globalcontext_send_message(ctx->global, local_process_id, ctx->x[1]); ctx->x[0] = ctx->x[1]; + } else if (term_is_process_reference(recipient_term)) { + int32_t process_id = term_process_ref_to_process_id(recipient_term); + int64_t ref_ticks = term_to_ref_ticks(recipient_term); + Context *p = globalcontext_get_process_lock(ctx->global, process_id); + if (p) { + struct MonitorAlias *alias = context_find_alias(p, ref_ticks); + if (!IS_NULL_PTR(alias)) { + if (alias->alias_type == ContextMonitorAliasReplyDemonitor) { + context_unalias(alias); + } + mailbox_send(p, ctx->x[1]); + } + globalcontext_get_process_unlock(ctx->global, p); + } + ctx->x[0] = ctx->x[1]; + } else if (!term_is_reference(recipient_term)) { + set_error(ctx, jit_state, 0, BADARG_ATOM); + return false; } + return true; } diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 2e3f70f7eb..2820bc7a89 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -296,6 +296,8 @@ static term nif_unicode_characters_to_binary(Context *ctx, int argc, term argv[] static term nif_erlang_lists_subtract(Context *ctx, int argc, term argv[]); static term nif_erlang_crc32(Context *ctx, int argc, term argv[]); static term nif_erlang_crc32_combine_3(Context *ctx, int argc, term argv[]); +static term nif_erlang_alias(Context *ctx, int argc, term argv[]); +static term nif_erlang_unalias(Context *ctx, int argc, term argv[]); static term nif_zlib_compress_1(Context *ctx, int argc, term argv[]); #define DECLARE_MATH_NIF_FUN(moniker) \ @@ -987,6 +989,14 @@ static const struct Nif crc32_combine_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_erlang_crc32_combine_3 }; +static const struct Nif erlang_alias_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_erlang_alias +}; +static const struct Nif erlang_unalias_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_erlang_unalias +}; static const struct Nif zlib_compress_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_zlib_compress_1 @@ -1325,7 +1335,7 @@ static NativeHandlerResult process_console_message(Context *ctx, term msg) { // msg is not in the port's heap NativeHandlerResult result = NativeContinue; - if (UNLIKELY(memory_ensure_free_opt(ctx, 12, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(3) + TERM_BOXED_REFERENCE_PROCESS_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { fprintf(stderr, "Unable to allocate sufficient memory for console driver.\n"); AVM_ABORT(); } @@ -1417,6 +1427,42 @@ static NativeHandlerResult process_console_mailbox(Context *ctx) return result; } +static term parse_monitor_opts(Context *ctx, term monitor_opts, bool *is_alias, enum ContextMonitorAliasType *alias_type) +{ + *is_alias = false; + while (term_is_nonempty_list(monitor_opts)) { + term option = term_get_list_head(monitor_opts); + if (term_is_tuple(option) && term_get_tuple_arity(option) == 2 && term_get_tuple_element(option, 0) == ALIAS_ATOM) { + *is_alias = true; + switch (term_get_tuple_element(option, 1)) { + case EXPLICIT_UNALIAS_ATOM: + *alias_type = ContextMonitorAliasExplicitUnalias; + break; + case DEMONITOR_ATOM: + *alias_type = ContextMonitorAliasDemonitor; + break; + case REPLY_DEMONITOR_ATOM: + *alias_type = ContextMonitorAliasReplyDemonitor; + break; + default: + RAISE_ERROR(BADARG_ATOM); + } + } else if (term_is_tuple(option) && term_get_tuple_arity(option) == 2 && term_get_tuple_element(option, 0) == TAG_ATOM) { + RAISE_ERROR(UNSUPPORTED_ATOM); + } else { + RAISE_ERROR(BADARG_ATOM); + } + + monitor_opts = term_get_list_tail(monitor_opts); + } + + if (UNLIKELY(!term_is_nil(monitor_opts))) { + RAISE_ERROR(BADARG_ATOM); + } + + return OK_ATOM; +} + // Common handling of spawn/1, spawn/3, spawn_opt/2, spawn_opt/4 // opts_term is [] for spawn/1,3 static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_freeze, term opts_term) @@ -1424,7 +1470,7 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free term min_heap_size_term = interop_proplist_get_value(opts_term, MIN_HEAP_SIZE_ATOM); term max_heap_size_term = interop_proplist_get_value(opts_term, MAX_HEAP_SIZE_ATOM); term link_term = interop_proplist_get_value(opts_term, LINK_ATOM); - term monitor_term = interop_proplist_get_value(opts_term, MONITOR_ATOM); + term monitor_term = interop_proplist_get_value_default(opts_term, MONITOR_ATOM, term_invalid_term()); term heap_growth_strategy = interop_proplist_get_value_default(opts_term, ATOMVM_HEAP_GROWTH_ATOM, BOUNDED_FREE_ATOM); term request_term = interop_proplist_get_value_default(opts_term, REQUEST_ATOM, UNDEFINED_ATOM); term group_leader; @@ -1494,7 +1540,8 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free context_destroy(new_ctx); RAISE_ERROR(BADARG_ATOM); } - uint64_t ref_ticks = 0; + RefData ref_data; + bool is_spawn_monitor = false; term new_pid = term_from_local_process_id(new_ctx->process_id); if (link_term == TRUE_ATOM) { @@ -1514,25 +1561,53 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free context_add_monitor(ctx, self_link); } if (monitor_term == TRUE_ATOM) { - // We can call context_add_monitor directly on new process because it's not started yet - ref_ticks = globalcontext_get_ref_ticks(ctx->global); - struct Monitor *new_monitor = monitor_new(term_from_local_process_id(ctx->process_id), ref_ticks, false); + monitor_term = term_nil(); + } + if (term_is_list(monitor_term)) { + is_spawn_monitor = true; + bool is_alias; + enum ContextMonitorAliasType alias_type; + + if (UNLIKELY(term_is_invalid_term(parse_monitor_opts(ctx, monitor_term, &is_alias, &alias_type)))) { + context_destroy(new_ctx); + return term_invalid_term(); + } + struct Monitor *alias_monitor = NULL; + if (is_alias) { + ref_data = (RefData){ .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = ctx->process_id }; + alias_monitor = monitor_alias_new(&ref_data, alias_type); + if (IS_NULL_PTR(alias_monitor)) { + context_destroy(new_ctx); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + } else { + ref_data = (RefData){ .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = INVALID_PROCESS_ID }; + } + + struct Monitor *new_monitor = monitor_new(term_from_local_process_id(ctx->process_id), &ref_data, false); if (IS_NULL_PTR(new_monitor)) { context_destroy(new_ctx); + free(alias_monitor); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - struct Monitor *self_monitor = monitor_new(new_pid, ref_ticks, true); + struct Monitor *self_monitor = monitor_new(new_pid, &ref_data, true); if (IS_NULL_PTR(self_monitor)) { + free(alias_monitor); free(new_monitor); context_destroy(new_ctx); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } + + // We can call context_add_monitor directly on new process because it's not started yet context_add_monitor(new_ctx, new_monitor); context_add_monitor(ctx, self_monitor); + if (is_alias) { + context_add_monitor(ctx, alias_monitor); + } } - if (ref_ticks) { - int res_size = REF_SIZE + TUPLE_SIZE(2); + if (is_spawn_monitor) { + int res_size = TERM_BOXED_REFERENCE_PROCESS_SIZE + TUPLE_SIZE(2); if (UNLIKELY(memory_ensure_free_opt(ctx, res_size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { context_destroy(new_ctx); RAISE_ERROR(OUT_OF_MEMORY_ATOM); @@ -1540,13 +1615,13 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free scheduler_init_ready(new_ctx); - term ref = term_from_ref_ticks(ref_ticks, &ctx->heap); + term ref = term_from_ref_data(&ref_data, &ctx->heap); - term pid_ref_tuple = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(pid_ref_tuple, 0, new_pid); - term_put_tuple_element(pid_ref_tuple, 1, ref); + term process_ref_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(process_ref_tuple, 0, new_pid); + term_put_tuple_element(process_ref_tuple, 1, ref); - return pid_ref_tuple; + return process_ref_tuple; } else if (UNLIKELY(valid_request)) { // Handling of spawn_request // spawn_request requires that the reply is enqueued before @@ -1739,6 +1814,20 @@ static term nif_erlang_send_2(Context *ctx, int argc, term argv[]) globalcontext_send_message(glb, local_process_id, argv[1]); + } else if (term_is_process_reference(target)) { + int32_t process_id = term_process_ref_to_process_id(target); + int64_t ref_ticks = term_to_ref_ticks(target); + Context *p = globalcontext_get_process_lock(glb, process_id); + if (p) { + struct MonitorAlias *alias = context_find_alias(p, ref_ticks); + if (alias != NULL) { + if (alias->alias_type == ContextMonitorAliasReplyDemonitor) { + context_unalias(alias); + } + mailbox_send(p, argv[1]); + } + globalcontext_get_process_unlock(glb, p); + } } else if (term_is_atom(target)) { // We need to hold a lock on the processes_table until the message is sent to avoid a race condition, // otherwise the receiving process could be killed at any point between checking it is registered, @@ -1763,7 +1852,7 @@ static term nif_erlang_send_2(Context *ctx, int argc, term argv[]) globalcontext_send_message_nolock(glb, local_process_id, argv[1]); synclist_unlock(&glb->processes_table); - } else { + } else if (!term_is_reference(target)) { RAISE_ERROR(BADARG_ATOM); } @@ -1842,7 +1931,7 @@ term nif_erlang_make_ref_0(Context *ctx, int argc, term argv[]) UNUSED(argv); // a ref is 64 bits, hence 8 bytes - if (UNLIKELY(memory_ensure_free_opt(ctx, REF_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_SHORT_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } @@ -4920,17 +5009,22 @@ static term nif_erlang_memory(Context *ctx, int argc, term argv[]) static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) { - UNUSED(argc); term object_type = argv[0]; term target_proc = argv[1]; + term options = argc == 3 ? argv[2] : term_nil(); term target_pid; size_t target_proc_size = 0; + bool is_alias; + enum ContextMonitorAliasType alias_type; if (object_type != PROCESS_ATOM && object_type != PORT_ATOM) { RAISE_ERROR(BADARG_ATOM); } + if (UNLIKELY(term_is_invalid_term(parse_monitor_opts(ctx, options, &is_alias, &alias_type)))) { + return term_invalid_term(); + } if (term_is_atom(target_proc)) { target_pid = globalcontext_get_registered_process(ctx->global, term_to_atom_index(target_proc)); target_proc_size = TUPLE_SIZE(2); @@ -4951,7 +5045,7 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) local_process_id = term_to_local_process_id(target_pid); // Monitoring self is possible but no monitor is actually created if (UNLIKELY(local_process_id == ctx->process_id)) { - if (UNLIKELY(memory_ensure_free_opt(ctx, REF_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_SHORT_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } uint64_t ref_ticks = globalcontext_get_ref_ticks(ctx->global); @@ -4963,7 +5057,7 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) } if (IS_NULL_PTR(target)) { - int res_size = REF_SIZE + TUPLE_SIZE(5) + target_proc_size; + int res_size = TERM_BOXED_REFERENCE_PROCESS_SIZE + TUPLE_SIZE(5) + target_proc_size; if (UNLIKELY(memory_ensure_free_opt(ctx, res_size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } @@ -4989,20 +5083,34 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) if ((object_type == PROCESS_ATOM && target->native_handler != NULL) || (object_type == PORT_ATOM && target->native_handler == NULL)) { RAISE_ERROR(BADARG_ATOM); } - uint64_t ref_ticks = globalcontext_get_ref_ticks(ctx->global); - term monitoring_pid = term_from_local_process_id(ctx->process_id); + + RefData ref_data; + struct Monitor *alias_monitor = NULL; + if (is_alias) { + ref_data = (RefData){ .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = ctx->process_id }; + alias_monitor = monitor_alias_new(&ref_data, alias_type); + if (IS_NULL_PTR(alias_monitor)) { + globalcontext_get_process_unlock(ctx->global, target); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + } else { + ref_data = (RefData){ .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = INVALID_PROCESS_ID }; + } struct Monitor *self_monitor; if (term_is_atom(target_proc)) { - self_monitor = monitor_registeredname_monitor_new(local_process_id, target_proc, ref_ticks); + self_monitor = monitor_registeredname_monitor_new(local_process_id, target_proc, &ref_data); } else { - self_monitor = monitor_new(target_pid, ref_ticks, true); + self_monitor = monitor_new(target_pid, &ref_data, true); } if (IS_NULL_PTR(self_monitor)) { globalcontext_get_process_unlock(ctx->global, target); + free(alias_monitor); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - struct Monitor *other_monitor = monitor_new(monitoring_pid, ref_ticks, false); + term monitoring_pid = term_from_local_process_id(ctx->process_id); + struct Monitor *other_monitor = monitor_new(monitoring_pid, &ref_data, false); if (IS_NULL_PTR(other_monitor)) { + free(alias_monitor); free(self_monitor); globalcontext_get_process_unlock(ctx->global, target); RAISE_ERROR(OUT_OF_MEMORY_ATOM); @@ -5011,12 +5119,15 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) globalcontext_get_process_unlock(ctx->global, target); context_add_monitor(ctx, self_monitor); + if (is_alias) { + context_add_monitor(ctx, alias_monitor); + } - if (UNLIKELY(memory_ensure_free_opt(ctx, REF_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_PROCESS_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - return term_from_ref_ticks(ref_ticks, &ctx->heap); + return term_from_ref_data(&ref_data, &ctx->heap); } static term nif_erlang_demonitor(Context *ctx, int argc, term argv[]) @@ -7448,6 +7559,42 @@ static term nif_erlang_crc32_combine_3(Context *ctx, int argc, term argv[]) return make_maybe_boxed_int64(ctx, crc); } +static term nif_erlang_alias(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + UNUSED(argv); + + if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_PROCESS_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + RefData ref_data = { .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = ctx->process_id }; + term process_ref = term_from_ref_data(&ref_data, &ctx->heap); + struct Monitor *monitor = monitor_alias_new(&ref_data, ContextMonitorAliasExplicitUnalias); + if (IS_NULL_PTR(monitor)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + context_add_monitor(ctx, monitor); + return process_ref; +} + +static term nif_erlang_unalias(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term process_ref = argv[0]; + VALIDATE_VALUE(process_ref, term_is_local_reference); + uint64_t ref_ticks = term_to_ref_ticks(process_ref); + + struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); + if (IS_NULL_PTR(alias)) { + return FALSE_ATOM; + } else { + context_unalias(alias); + return TRUE_ATOM; + } +} + #ifdef WITH_ZLIB static term nif_zlib_compress_1(Context *ctx, int argc, term argv[]) { diff --git a/src/libAtomVM/nifs.gperf b/src/libAtomVM/nifs.gperf index c7ce64cf7e..ac9ffd59bb 100644 --- a/src/libAtomVM/nifs.gperf +++ b/src/libAtomVM/nifs.gperf @@ -101,6 +101,7 @@ erlang:make_ref/0, &make_ref_nif erlang:make_tuple/2, &make_tuple_nif erlang:memory/1, &memory_nif erlang:monitor/2, &monitor_nif +erlang:monitor/3, &monitor_nif erlang:demonitor/1, &demonitor_nif erlang:demonitor/2, &demonitor_nif erlang:is_process_alive/1, &is_process_alive_nif @@ -158,6 +159,8 @@ erlang:loaded/0, &erlang_loaded_nif erlang:module_loaded/1,&module_loaded_nif erlang:nif_error/1,&nif_error_nif erlang:list_to_bitstring/1, &list_to_bitstring_nif +erlang:alias/0, &erlang_alias_nif +erlang:unalias/1, &erlang_unalias_nif erts_debug:flat_size/1, &flat_size_nif erts_internal:cmp_term/2, &erts_internal_cmp_term_nif file:get_cwd/0, IF_HAVE_GETCWD_PATHMAX(&file_get_cwd_nif) diff --git a/src/libAtomVM/opcodesswitch.h b/src/libAtomVM/opcodesswitch.h index 889ff319a8..49d3b11342 100644 --- a/src/libAtomVM/opcodesswitch.h +++ b/src/libAtomVM/opcodesswitch.h @@ -2374,12 +2374,28 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) int local_process_id; if (term_is_local_pid_or_port(recipient_term)) { local_process_id = term_to_local_process_id(recipient_term); - } else { + TRACE("send/0 target_pid=%i\n", local_process_id); + TRACE_SEND(ctx, x_regs[0], x_regs[1]); + globalcontext_send_message(ctx->global, local_process_id, x_regs[1]); + } else if (term_is_process_reference(recipient_term)) { + int32_t local_process_id = term_process_ref_to_process_id(recipient_term); + TRACE("send/0 target_pid=%i\n", local_process_id); + TRACE_SEND(ctx, x_regs[0], x_regs[1]); + int64_t ref_ticks = term_to_ref_ticks(recipient_term); + Context *p = globalcontext_get_process_lock(ctx->global, local_process_id); + if (p) { + struct MonitorAlias *alias = context_find_alias(p, ref_ticks); + if (alias != NULL) { + if (alias->alias_type == ContextMonitorAliasReplyDemonitor) { + context_unalias(alias); + } + mailbox_send(p, x_regs[1]); + } + globalcontext_get_process_unlock(ctx->global, p); + } + } else if (!term_is_reference(recipient_term)) { RAISE_ERROR(BADARG_ATOM); } - TRACE("send/0 target_pid=%i\n", local_process_id); - TRACE_SEND(ctx, x_regs[0], x_regs[1]); - globalcontext_send_message(ctx->global, local_process_id, x_regs[1]); x_regs[0] = x_regs[1]; } break; diff --git a/src/libAtomVM/otp_socket.c b/src/libAtomVM/otp_socket.c index 4195a96241..37381fe344 100644 --- a/src/libAtomVM/otp_socket.c +++ b/src/libAtomVM/otp_socket.c @@ -246,7 +246,7 @@ static const AtomStringIntPair otp_socket_setopt_level_table[] = { static ErlNifResourceType *socket_resource_type; -#define SOCKET_MAKE_SELECT_NOTIFICATION_SIZE (TUPLE_SIZE(4) + REF_SIZE + TUPLE_SIZE(2) + REF_SIZE + TERM_BOXED_REFERENCE_RESOURCE_SIZE) +#define SOCKET_MAKE_SELECT_NOTIFICATION_SIZE (TUPLE_SIZE(4) + TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE + TERM_BOXED_REFERENCE_RESOURCE_SIZE) static term socket_make_select_notification(struct SocketResource *rsrc_obj, Heap *heap); // @@ -644,7 +644,7 @@ static term nif_socket_open(Context *ctx, int argc, term argv[]) term obj = term_from_resource(rsrc_obj, &ctx->heap); enif_release_resource(rsrc_obj); // decrement refcount after enif_alloc_resource - size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(2) + REF_SIZE; + size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE; if (UNLIKELY(memory_ensure_free_with_roots(ctx, requested_size, 1, &obj, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); RAISE_ERROR(OUT_OF_MEMORY_ATOM); @@ -695,7 +695,7 @@ bool term_is_otp_socket(term socket_term) static int send_closed_notification(Context *ctx, term socket_term, int32_t selecting_process_id, struct SocketResource *rsrc_obj) { // send a {'$socket', Socket, abort, {Ref | undefined, closed}} message to the pid - if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(4) + TUPLE_SIZE(2) + REF_SIZE, 1, &socket_term, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(4) + TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE, 1, &socket_term, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); return -1; } @@ -1814,7 +1814,7 @@ static term nif_socket_listen(Context *ctx, int argc, term argv[]) #if OTP_SOCKET_LWIP static term make_accepted_socket_term(Context *ctx, struct SocketResource *conn_rsrc_obj) { - if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_REFERENCE_RESOURCE_SIZE + TUPLE_SIZE(2) + REF_SIZE) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_REFERENCE_RESOURCE_SIZE + TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } term obj = term_from_resource(conn_rsrc_obj, &ctx->heap); @@ -1909,7 +1909,7 @@ static term nif_socket_accept(Context *ctx, int argc, term argv[]) term new_resource = term_from_resource(conn_rsrc_obj, &ctx->heap); enif_release_resource(conn_rsrc_obj); // decrement refcount after enif_alloc_resource - size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(2) + REF_SIZE; + size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE; if (UNLIKELY(memory_ensure_free_with_roots(ctx, requested_size, 1, &new_resource, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); RAISE_ERROR(OUT_OF_MEMORY_ATOM); @@ -1940,7 +1940,7 @@ static term nif_socket_accept(Context *ctx, int argc, term argv[]) SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - size_t requested_size = TERM_BOXED_REFERENCE_RESOURCE_SIZE + TUPLE_SIZE(2) + TUPLE_SIZE(2) + REF_SIZE; + size_t requested_size = TERM_BOXED_REFERENCE_RESOURCE_SIZE + TUPLE_SIZE(2) + TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE; if (UNLIKELY(memory_ensure_free_with_roots(ctx, requested_size, 1, argv, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); LWIP_END(); @@ -2319,7 +2319,7 @@ static term nif_socket_recv_lwip(Context *ctx, term resource_term, struct Socket } size_t ensure_packet_avail = term_binary_heap_size(buffer_size); - size_t requested_size = REF_SIZE + 2 * TUPLE_SIZE(2) + ensure_packet_avail + (is_recvfrom ? (TUPLE_SIZE(2) + INET_ADDR4_TUPLE_SIZE + TERM_MAP_SIZE(2)) : 0); + size_t requested_size = TERM_BOXED_REFERENCE_SHORT_SIZE + 2 * TUPLE_SIZE(2) + ensure_packet_avail + (is_recvfrom ? (TUPLE_SIZE(2) + INET_ADDR4_TUPLE_SIZE + TERM_MAP_SIZE(2)) : 0); // Because resource is locked, we must ensure it's not garbage collected if (UNLIKELY(memory_ensure_free_with_roots(ctx, requested_size, 1, &resource_term, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); diff --git a/src/libAtomVM/resources.h b/src/libAtomVM/resources.h index 5c95a5c4ed..58c97cf9b6 100644 --- a/src/libAtomVM/resources.h +++ b/src/libAtomVM/resources.h @@ -164,7 +164,7 @@ void select_event_count_and_destroy_closed(struct ListHead *select_events, size_ */ void destroy_resource_monitors(struct RefcBinary *resource, GlobalContext *global); -#define SELECT_EVENT_NOTIFICATION_SIZE (TUPLE_SIZE(4) + REF_SIZE + TERM_BOXED_REFERENCE_RESOURCE_SIZE) +#define SELECT_EVENT_NOTIFICATION_SIZE (TUPLE_SIZE(4) + TERM_BOXED_REFERENCE_SHORT_SIZE + TERM_BOXED_REFERENCE_RESOURCE_SIZE) /** * @brief Build a select event notification. diff --git a/src/libAtomVM/term.c b/src/libAtomVM/term.c index f8b1a0cce3..7fdfde55d3 100644 --- a/src/libAtomVM/term.c +++ b/src/libAtomVM/term.c @@ -405,6 +405,12 @@ int term_funprint(PrinterFun *fun, term t, const GlobalContext *global) uint64_t resource_ptr = (uintptr_t) refc_binary->data; return fun->print(fun, "#Ref<0.%" PRIu32 ".%" PRIu32 ".%" PRIu32 ".%" PRIu32 ">", (uint32_t) (resource_type_ptr >> 32), (uint32_t) resource_type_ptr, (uint32_t) (resource_ptr >> 32), (uint32_t) resource_ptr); + } else if (term_is_process_reference(t)) { + int32_t process_id = term_process_ref_to_process_id(t); + uint64_t ref_ticks = term_to_ref_ticks(t); + + // Update also REF_AS_CSTRING_LEN when changing this format string + return fun->print(fun, "#Ref<%" PRId32 ".%" PRIu32 ".%" PRIu32 ">", process_id, (uint32_t) (ref_ticks >> 32), (uint32_t) ref_ticks); } else if (term_is_local_reference(t)) { // Update also REF_AS_CSTRING_LEN when changing this format string uint64_t ref_ticks = term_to_ref_ticks(t); @@ -676,11 +682,15 @@ TermCompareResult term_compare(term t, term other, TermCompareOpts opts, GlobalC uint32_t len, other_len; if (term_is_resource_reference(t)) { len = 4; + } else if (term_is_process_reference(t)) { + len = 3; } else { len = 2; } if (term_is_resource_reference(other)) { other_len = 4; + } else if (term_is_process_reference(other)) { + other_len = 3; } else { other_len = 2; } @@ -694,6 +704,15 @@ TermCompareResult term_compare(term t, term other, TermCompareOpts opts, GlobalC int64_t other_ticks = term_to_ref_ticks(other); other_data[0] = other_ticks >> 32; other_data[1] = (uint32_t) other_ticks; + } else if (len == 3) { + data[0] = term_process_ref_to_process_id(t); + int64_t t_ticks = term_to_ref_ticks(t); + data[1] = t_ticks >> 32; + data[2] = (uint32_t) t_ticks; + other_data[0] = term_process_ref_to_process_id(other); + int64_t other_ticks = term_to_ref_ticks(other); + other_data[1] = other_ticks >> 32; + other_data[2] = (uint32_t) other_ticks; } else { // len == 4 struct RefcBinary *refc = term_resource_refc_binary_ptr(t); diff --git a/src/libAtomVM/term.h b/src/libAtomVM/term.h index df1d411354..b68a0bb57b 100644 --- a/src/libAtomVM/term.h +++ b/src/libAtomVM/term.h @@ -129,7 +129,14 @@ extern "C" { #define TERM_BOXED_REFC_BINARY_SIZE 6 #define TERM_BOXED_BIN_MATCH_STATE_SIZE 4 #define TERM_BOXED_SUB_BINARY_SIZE 4 +#if TERM_BYTES == 8 #define TERM_BOXED_REFERENCE_RESOURCE_SIZE 4 +#else +// Enough size would be 4, but reference types +// are distinguished by size and 4 conflicts with +// TERM_BOXED_REFERENCE_PROCESS_SIZE on 32bit arch. +#define TERM_BOXED_REFERENCE_RESOURCE_SIZE 5 +#endif #define TERM_BOXED_REFERENCE_RESOURCE_HEADER (((TERM_BOXED_REFERENCE_RESOURCE_SIZE - 1) << 6) | TERM_BOXED_REF) #define TERM_BOXED_RESOURCE_SIZE TERM_BOXED_REFERENCE_RESOURCE_SIZE @@ -151,7 +158,13 @@ extern "C" { #define BOXED_INT64_SIZE (BOXED_TERMS_REQUIRED_FOR_INT64 + 1) #define BOXED_FUN_SIZE 3 #define FLOAT_SIZE (sizeof(float_term_t) / sizeof(term) + 1) -#define REF_SIZE ((int) ((sizeof(uint64_t) / sizeof(term)) + 1)) +// Reference types are distinguished by their size. +// If you change a reference size, make sure it doesn't +// conflict with other reference sizes on all architectures. +#define TERM_BOXED_REFERENCE_SHORT_SIZE ((int) ((sizeof(uint64_t) / sizeof(term)) + 1)) +#define REF_SIZE _Pragma("REF_SIZE is deprecated, use TERM_BOXED_REFERENCE_SHORT_SIZE instead") TERM_BOXED_REFERENCE_SHORT_SIZE +#define TERM_BOXED_REFERENCE_PROCESS_SIZE (TERM_BOXED_REFERENCE_SHORT_SIZE + 1) +#define TERM_BOXED_REFERENCE_PROCESS_HEADER (((TERM_BOXED_REFERENCE_PROCESS_SIZE - 1) << 6) | TERM_BOXED_REF) #if TERM_BYTES == 8 #define EXTERNAL_PID_SIZE 3 #elif TERM_BYTES == 4 @@ -167,10 +180,23 @@ extern "C" { #else #error #endif +#define EXTERNAL_REF_MAX_WORDS 5 +#define TERM_BOXED_REFERENCE_MAX_SIZE EXTERNAL_REF_SIZE(EXTERNAL_REF_MAX_WORDS) +_Static_assert(TERM_BOXED_REFERENCE_SHORT_SIZE < TERM_BOXED_REFERENCE_PROCESS_SIZE, "Short ref size must be smaller than process ref size"); +_Static_assert(TERM_BOXED_REFERENCE_PROCESS_SIZE < TERM_BOXED_REFERENCE_RESOURCE_SIZE, "Process ref size must be smaller than reference resource size"); +_Static_assert(TERM_BOXED_REFERENCE_PROCESS_SIZE <= TERM_BOXED_REFERENCE_MAX_SIZE, "Max ref size can't be smaller than all other ref sizes"); #define TUPLE_SIZE(elems) ((int) (elems + 1)) #define CONS_SIZE 2 #define REFC_BINARY_CONS_OFFSET 4 #define REFERENCE_RESOURCE_CONS_OFFSET 2 + +#if TERM_BYTES == 4 +#define REFERENCE_PROCESS_PID_OFFSET 3 + +#elif TERM_BYTES == 8 +#define REFERENCE_PROCESS_PID_OFFSET 2 +#endif + #define LIST_SIZE(num_elements, element_size) ((num_elements) * ((element_size) + CONS_SIZE)) #define TERM_STRING_SIZE(length) (2 * (length)) #define TERM_MAP_SIZE(num_elements) (3 + 2 * (num_elements)) @@ -179,6 +205,8 @@ extern "C" { #define LIST_HEAD_INDEX 1 #define LIST_TAIL_INDEX 0 +#define INVALID_PROCESS_ID 0 + #define TERM_BINARY_SIZE_IS_HEAP(size) ((size) < REFC_BINARY_MIN) #if TERM_BYTES == 4 @@ -210,6 +238,9 @@ extern "C" { // Local ref is at most 30 bytes: // 2^32-1 = 4294967295 (10 chars) // "#Ref<0." "." ">\0" (10 chars) +// Process ref is at most 39 bytes: +// 2^32-1 = 4294967295 (10 chars) +// "#Ref<" "." "." ">\0" (9 chars) // Resource ref is at most 52 bytes: // 2^32-1 = 4294967295 (10 chars) // "#Ref<0." "." "." "." ">\0" (12 chars) @@ -243,6 +274,21 @@ extern "C" { typedef struct GlobalContext GlobalContext; #endif +enum RefType +{ + RefTypeShort, + RefTypeProcess, + RefTypeResource, + RefTypeExternal +}; + +typedef struct RefData RefData; +struct RefData +{ + uint64_t ref_ticks; + int32_t process_id; +}; + typedef struct PrinterFun PrinterFun; typedef int (*printer_function_t)(PrinterFun *fun, const char *fmt, ...) PRINTF_FORMAT_ARGS(2, 3); @@ -915,6 +961,25 @@ static inline bool term_is_local_reference(term t) return false; } +/** + * @brief Checks if a term is a process reference + * + * @details See \c term_make_process_reference(). + * @param t the term that will be checked. + * @return \c true if check succeeds, \c false otherwise. + */ +static inline bool term_is_process_reference(term t) +{ + if (term_is_boxed(t)) { + const term *boxed_value = term_to_const_term_ptr(t); + if (boxed_value[0] == TERM_BOXED_REFERENCE_PROCESS_HEADER) { + return true; + } + } + + return false; +} + /** * @brief Checks if a term is an external reference * @@ -2166,8 +2231,8 @@ static inline int term_bs_insert_binary(term t, int offset, term src, int n) */ static inline term term_from_ref_ticks(uint64_t ref_ticks, Heap *heap) { - term *boxed_value = memory_heap_alloc(heap, REF_SIZE); - boxed_value[0] = ((REF_SIZE - 1) << 6) | TERM_BOXED_REF; + term *boxed_value = memory_heap_alloc(heap, TERM_BOXED_REFERENCE_SHORT_SIZE); + boxed_value[0] = ((TERM_BOXED_REFERENCE_SHORT_SIZE - 1) << 6) | TERM_BOXED_REF; #if TERM_BYTES == 8 boxed_value[1] = (term) ref_ticks; @@ -2200,6 +2265,50 @@ static inline uint64_t term_to_ref_ticks(term rt) #endif } +/** + * @brief Creates a process reference + * @details Process reference contains ref_ticks and process_id of a process. + * They are used by process aliases and monitors. + * + * @param process_id process_id of a process that the reference will identify. + * @param ref_ticks an unique uint64 value that will be used to create ref term. + * @param heap the heap to allocate memory in + * @return a ref term created using given ref ticks. + */ +static inline term term_make_process_reference(int32_t process_id, uint64_t ref_ticks, Heap *heap) +{ + term *boxed_value = memory_heap_alloc(heap, TERM_BOXED_REFERENCE_PROCESS_SIZE); + boxed_value[0] = TERM_BOXED_REFERENCE_PROCESS_HEADER; + +#if TERM_BYTES == 4 + boxed_value[1] = (ref_ticks >> 32); + boxed_value[2] = (ref_ticks & 0xFFFFFFFF); + boxed_value[3] = process_id; + +#elif TERM_BYTES == 8 + boxed_value[1] = (term) ref_ticks; + boxed_value[2] = process_id; + +#else +#error "terms must be either 32 or 64 bit wide" +#endif + + return ((term) boxed_value) | TERM_PRIMARY_BOXED; +} + +static inline uint32_t term_process_ref_to_process_id(term rt) +{ + TERM_DEBUG_ASSERT(term_is_process_reference(rt)); + const term *boxed_value = term_to_const_term_ptr(rt); +#if TERM_BYTES == 4 + return (uint32_t) boxed_value[3]; +#elif TERM_BYTES == 8 + return (uint32_t) boxed_value[2]; +#else +#error "terms must be either 32 or 64 bit wide" +#endif +} + /** * @brief Make an external pid term from node, process_id, serial and creation * @@ -2336,7 +2445,7 @@ static inline uint64_t term_get_external_port_number(term t) * @param heap the heap to allocate memory in * @return an external heap term created using given parameters. */ -static inline term term_make_external_reference(term node, uint16_t len, uint32_t *data, uint32_t creation, Heap *heap) +static inline term term_make_external_reference(term node, uint16_t len, const uint32_t *data, uint32_t creation, Heap *heap) { TERM_DEBUG_ASSERT(term_is_atom(node)); @@ -2981,6 +3090,15 @@ static inline term term_from_resource(void *resource, Heap *heap) return ret; } +static inline term term_from_ref_data(RefData *ref_data, Heap *heap) +{ + if (ref_data->process_id == INVALID_PROCESS_ID) { + return term_from_ref_ticks(ref_data->ref_ticks, heap); + } else { + return term_make_process_reference(ref_data->process_id, ref_data->ref_ticks, heap); + } +} + /** * @brief Get a resource term from a resource type and a serialization reference * number. diff --git a/src/platforms/emscripten/src/lib/websocket_nifs.c b/src/platforms/emscripten/src/lib/websocket_nifs.c index a6d0bc8c89..ab8e084652 100644 --- a/src/platforms/emscripten/src/lib/websocket_nifs.c +++ b/src/platforms/emscripten/src/lib/websocket_nifs.c @@ -95,7 +95,7 @@ static void websocket_down(ErlNifEnv *caller_env, void *obj, ErlNifPid *pid, Erl } } -#define TERM_WEBSOCKET_RESOURCE_SIZE (TERM_BOXED_RESOURCE_SIZE + REF_SIZE + TUPLE_SIZE(3)) +#define TERM_WEBSOCKET_RESOURCE_SIZE (TERM_BOXED_RESOURCE_SIZE + TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(3)) static term term_make_websocket_resource(struct WebsocketResource *rsrc, Heap *heap) { diff --git a/src/platforms/esp32/components/avm_builtins/adc_driver.c b/src/platforms/esp32/components/avm_builtins/adc_driver.c index 2ad34bccd0..210c8e8b98 100644 --- a/src/platforms/esp32/components/avm_builtins/adc_driver.c +++ b/src/platforms/esp32/components/avm_builtins/adc_driver.c @@ -344,7 +344,7 @@ static term nif_adc_init(Context *ctx, int argc, term argv[]) #endif // {ok, {'$adc', Unit :: resource(), ref()}} - size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(3) + REF_SIZE + TERM_BOXED_REFERENCE_RESOURCE_SIZE; + size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(3) + TERM_BOXED_REFERENCE_SHORT_SIZE + TERM_BOXED_REFERENCE_RESOURCE_SIZE; ESP_LOGD(TAG, "Requesting memory size %u for return message", requested_size); if (UNLIKELY(memory_ensure_free(ctx, requested_size) != MEMORY_GC_OK)) { enif_release_resource(unit_rsrc); @@ -492,7 +492,7 @@ static term nif_adc_acquire(Context *ctx, int argc, term argv[]) chan_rsrc->calibration = calibration; // {ok, {'$adc', resource(), ref()}} - size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(3) + REF_SIZE + TERM_BOXED_REFERENCE_RESOURCE_SIZE; + size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(3) + TERM_BOXED_REFERENCE_SHORT_SIZE + TERM_BOXED_REFERENCE_RESOURCE_SIZE; ESP_LOGD(TAG, "Requesting memory size %u for return message", requested_size); if (UNLIKELY(memory_ensure_free(ctx, requested_size) != MEMORY_GC_OK)) { enif_release_resource(chan_rsrc); diff --git a/src/platforms/esp32/components/avm_builtins/dac_driver.c b/src/platforms/esp32/components/avm_builtins/dac_driver.c index 878cd15454..c06a5777af 100644 --- a/src/platforms/esp32/components/avm_builtins/dac_driver.c +++ b/src/platforms/esp32/components/avm_builtins/dac_driver.c @@ -127,7 +127,7 @@ static term nif_oneshot_new_channel_p(Context *ctx, int argc, term argv[]) enif_release_resource(chan_rsrc); if (!err) { - if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(3) + REF_SIZE + TUPLE_SIZE(2), 1, &chan_obj, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(3) + TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2), 1, &chan_obj, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { ESP_LOGE(TAG, "failed to allocate memory for result: %s:%i.", __FILE__, __LINE__); dac_oneshot_del_channel(chan_rsrc->handle); chan_rsrc->handle = NULL; diff --git a/src/platforms/esp32/components/avm_builtins/i2c_resource.c b/src/platforms/esp32/components/avm_builtins/i2c_resource.c index c8f906c85e..08bb9c21dc 100644 --- a/src/platforms/esp32/components/avm_builtins/i2c_resource.c +++ b/src/platforms/esp32/components/avm_builtins/i2c_resource.c @@ -269,7 +269,7 @@ static term nif_i2c_open(Context *ctx, int argc, term argv[]) // // {'$i2c', Resource :: resource(), Ref :: reference()} :: i2c() - size_t requested_size = TUPLE_SIZE(3) + REF_SIZE; + size_t requested_size = TUPLE_SIZE(3) + TERM_BOXED_REFERENCE_SHORT_SIZE; if (UNLIKELY(memory_ensure_free_with_roots(ctx, requested_size, 1, &obj, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { close_i2c_resource(rsrc_obj); ESP_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); diff --git a/src/platforms/esp32/components/avm_builtins/network_driver.c b/src/platforms/esp32/components/avm_builtins/network_driver.c index ad0f3e924b..84eeb21167 100644 --- a/src/platforms/esp32/components/avm_builtins/network_driver.c +++ b/src/platforms/esp32/components/avm_builtins/network_driver.c @@ -75,7 +75,7 @@ #define BSSID_SIZE 6 #define TAG "network_driver" -#define PORT_REPLY_SIZE (TUPLE_SIZE(2) + REF_SIZE) +#define PORT_REPLY_SIZE (TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE) static const char *const ap_atom = ATOM_STR("\x2", "ap"); static const char *const ap_channel_atom = ATOM_STR("\xA", "ap_channel"); @@ -1886,7 +1886,7 @@ static NativeHandlerResult consume_mailbox(Context *ctx) return NativeContinue; } - // TODO: port this code to standard port (and gen_message) + //TODO: port this code to standard port (and gen_message) term pid = term_get_tuple_element(msg, 0); term ref = term_get_tuple_element(msg, 1); term cmd = term_get_tuple_element(msg, 2); @@ -1934,7 +1934,7 @@ static NativeHandlerResult consume_mailbox(Context *ctx) default: { ESP_LOGE(TAG, "Unrecognized command: %x", cmd); // {Ref, {error, badarg}} - size_t heap_size = TUPLE_SIZE(2) + REF_SIZE + TUPLE_SIZE(2); + size_t heap_size = TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2); if (UNLIKELY(memory_ensure_free(ctx, heap_size) != MEMORY_GC_OK)) { ESP_LOGE(TAG, "Unable to allocate heap space for error; no message sent"); return NativeContinue; @@ -1944,7 +1944,7 @@ static NativeHandlerResult consume_mailbox(Context *ctx) } } else { // {Ref, {error, badarg}} - size_t heap_size = TUPLE_SIZE(2) + REF_SIZE + TUPLE_SIZE(2); + size_t heap_size = TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2); if (UNLIKELY(memory_ensure_free(ctx, heap_size) != MEMORY_GC_OK)) { ESP_LOGE(TAG, "Unable to allocate heap space for error; no message sent"); return NativeContinue; diff --git a/src/platforms/esp32/components/avm_builtins/socket_driver.c b/src/platforms/esp32/components/avm_builtins/socket_driver.c index 99f7933677..9330c69ce1 100644 --- a/src/platforms/esp32/components/avm_builtins/socket_driver.c +++ b/src/platforms/esp32/components/avm_builtins/socket_driver.c @@ -443,7 +443,7 @@ static struct UDPSocketData *udp_socket_data_new(Context *ctx, struct netconn *c } // When this method is called, ensure free was called with REPLY_SIZE -#define REPLY_SIZE (TUPLE_SIZE(2) + REF_SIZE) +#define REPLY_SIZE (TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE) static void do_send_reply(Context *ctx, term reply, uint64_t ref_ticks, int32_t pid) { GlobalContext *glb = ctx->global; diff --git a/src/platforms/esp32/components/avm_builtins/uart_driver.c b/src/platforms/esp32/components/avm_builtins/uart_driver.c index 761370840c..ace2aba894 100644 --- a/src/platforms/esp32/components/avm_builtins/uart_driver.c +++ b/src/platforms/esp32/components/avm_builtins/uart_driver.c @@ -237,7 +237,7 @@ EventListener *uart_interrupt_callback(GlobalContext *glb, EventListener *listen int bin_size = term_binary_heap_size(count); Heap heap; - if (UNLIKELY(memory_init_heap(&heap, bin_size + REF_SIZE + TUPLE_SIZE(2) * 2) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_init_heap(&heap, bin_size + TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2) * 2) != MEMORY_GC_OK)) { free(usj_buf); fprintf(stderr, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); AVM_ABORT(); @@ -804,7 +804,7 @@ static NativeHandlerResult uart_driver_consume_mailbox(Context *ctx) int local_pid = term_to_local_process_id(gen_message.pid); if (is_closed) { - if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2) * 2 + REF_SIZE) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2) * 2 + TERM_BOXED_REFERENCE_SHORT_SIZE) != MEMORY_GC_OK)) { ESP_LOGE(TAG, "[uart_driver_consume_mailbox] Failed to allocate space for error tuple"); globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); } diff --git a/src/platforms/rp2/src/lib/networkdriver.c b/src/platforms/rp2/src/lib/networkdriver.c index 84f280fed4..f7ceafbabb 100644 --- a/src/platforms/rp2/src/lib/networkdriver.c +++ b/src/platforms/rp2/src/lib/networkdriver.c @@ -41,7 +41,7 @@ #pragma GCC diagnostic pop -#define PORT_REPLY_SIZE (TUPLE_SIZE(2) + REF_SIZE) +#define PORT_REPLY_SIZE (TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE) #define DEFAULT_HOSTNAME_FMT "atomvm-%02x%02x%02x%02x%02x%02x" #define DEFAULT_HOSTNAME_SIZE (strlen("atomvm-") + 12 + 1) @@ -758,7 +758,7 @@ static NativeHandlerResult consume_mailbox(Context *ctx) default: { // {Ref, {error, badarg}} - size_t heap_size = TUPLE_SIZE(2) + REF_SIZE + TUPLE_SIZE(2); + size_t heap_size = TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2); if (UNLIKELY(memory_ensure_free(ctx, heap_size) != MEMORY_GC_OK)) { return NativeContinue; } @@ -767,7 +767,7 @@ static NativeHandlerResult consume_mailbox(Context *ctx) } } else { // {Ref, {error, badarg}} - size_t heap_size = TUPLE_SIZE(2) + REF_SIZE + TUPLE_SIZE(2); + size_t heap_size = TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2); if (UNLIKELY(memory_ensure_free(ctx, heap_size) != MEMORY_GC_OK)) { return NativeContinue; } diff --git a/tests/erlang_tests/test_binary_to_term.erl b/tests/erlang_tests/test_binary_to_term.erl index 17d507c4a1..5bb2e58104 100644 --- a/tests/erlang_tests/test_binary_to_term.erl +++ b/tests/erlang_tests/test_binary_to_term.erl @@ -157,6 +157,7 @@ start() -> ok = test_safe_option(), ok = test_invalid_export_fun_encoding(), ok = test_atom_utf8_ext_node(), + ok = test_encode_process_ref(), 0. test_reverse(T, Interop) -> @@ -994,6 +995,16 @@ test_encode_resource(OTPVersion) -> AlteredResource4 = binary_to_term(AlteredResourceBin4), false = AlteredResource4 =:= Resource, ok. +test_encode_process_ref() -> + AliasesAvailable = is_atomvm_or_otp_version_at_least("24"), + if + AliasesAvailable -> + ProcessRef = erlang:alias(), + ProcessRef = binary_to_term(term_to_binary(ProcessRef)), + ok; + true -> + ok + end. % Verify term_to_binary(binary_to_term(Bin)) is idempotent. binary_to_term_idempotent(Binary, _OTPVersion) -> @@ -1001,6 +1012,12 @@ binary_to_term_idempotent(Binary, _OTPVersion) -> Binary = term_to_binary(Term), Term. +is_atomvm_or_otp_version_at_least(OTPVersion) -> + case erlang:system_info(machine) of + "ATOM" -> true; + "BEAM" -> erlang:system_info(otp_release) >= OTPVersion + end. + test_atom_encoding() -> true = compare_pair_encoding(latin1_as_utf8_1), true = compare_pair_encoding(latin1_as_utf8_2), diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 55b070e74a..7d742fee44 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -34,6 +34,33 @@ start() -> ok = test_monitor_demonitor_from_other(), ok = test_monitor_registered(), ok = test_monitor_registered_noproc(), + + AliasesAvailable = + case erlang:system_info(machine) of + "ATOM" -> true; + "BEAM" -> erlang:system_info(otp_release) >= "24" + end, + if + AliasesAvailable -> + ok = test_alias(), + ok = test_multiple_aliases(), + ok = test_multiple_unaliases(), + ok = test_unalias_from_wrong_process(), + ok = test_monitor_alias_dead_process(), + ok = test_monitor_multiple_aliases_monitors(fun spawn_monitor/2), + ok = test_monitor_multiple_aliases_monitors(fun spawn_and_monitor/2), + ok = test_monitor_alias_demonitor(fun spawn_monitor/2), + ok = test_monitor_alias_demonitor(fun spawn_and_monitor/2), + ok = test_monitor_alias_explicit_unalias(fun spawn_monitor/2), + ok = test_monitor_alias_explicit_unalias(fun spawn_and_monitor/2), + ok = test_monitor_alias_reply_demonitor(fun spawn_monitor/2), + ok = test_monitor_alias_reply_demonitor(fun spawn_and_monitor/2), + ok = test_monitor_down_alias(fun spawn_monitor/2), + ok = test_monitor_down_alias(fun spawn_and_monitor/2), + ok; + true -> + ok + end, 0. test_monitor_normal() -> @@ -229,7 +256,131 @@ test_monitor_demonitor_from_other() -> end, ok. +test_alias() -> + P = spawn_opt(fun echo_loop/0, []), + Alias = erlang:alias(), + do_test_alias(P, Alias), + ok. + +test_multiple_aliases() -> + P = spawn_opt(fun echo_loop/0, []), + A1 = erlang:alias(), + A2 = erlang:alias(), + A3 = erlang:alias(), + do_test_alias(P, A1), + do_test_alias(P, A3), + do_test_alias(P, A2), + ok. + +test_multiple_unaliases() -> + A = erlang:alias(), + true = erlang:unalias(A), + false = erlang:unalias(A), + false = erlang:unalias(A), + ok. + +test_unalias_from_wrong_process() -> + A = erlang:alias(), + TestProcess = self(), + spawn_opt(fun() -> TestProcess ! erlang:unalias(A) end, [link]), + false = recv_one(), + P = spawn_opt(fun echo_loop/0, []), + do_test_alias(P, A), + ok. + +do_test_alias(P, Alias) -> + do_test_alias(P, Alias, fun erlang:unalias/1). + +do_test_alias(P, Alias, UnaliasFun) -> + Ref = make_ref(), + P ! {{m1, Ref}, Alias}, + {m1, Ref} = recv_one(), + UnaliasFun(Alias), + P ! {{m2, Ref}, Alias}, + P ! {{m3, Ref}, self()}, + {m3, Ref} = recv_one(), + ok. + +test_monitor_alias_demonitor(SpawnFun) -> + {P, Mon} = SpawnFun(fun echo_loop/0, [{alias, demonitor}]), + do_test_alias(P, Mon, fun demonitor/1), + ok. + +test_monitor_alias_explicit_unalias(SpawnFun) -> + {P, Mon} = SpawnFun(fun echo_loop/0, [{alias, explicit_unalias}]), + P ! {m1, Mon}, + m1 = recv_one(), + demonitor(Mon), + do_test_alias(P, Mon), + ok. + +test_monitor_alias_reply_demonitor(SpawnFun) -> + {P, Mon} = SpawnFun(fun echo_loop/0, [{alias, reply_demonitor}]), + do_test_alias(P, Mon, fun(_Mon) -> ok end), + ok. + +test_monitor_down_alias(SpawnFun) -> + {P, Mon} = SpawnFun(fun echo_loop/0, [{alias, demonitor}]), + erlang:unalias(Mon), + P ! {m1, Mon}, + P ! {m2, self()}, + m2 = recv_one(), + P ! quit, + {'DOWN', Mon, process, P, normal} = recv_one(), + ok. + +test_monitor_multiple_aliases_monitors(SpawnFun) -> + {P, Mon1} = SpawnFun(fun echo_loop/0, [{alias, demonitor}]), + Mon2 = erlang:monitor(process, P, [{alias, reply_demonitor}]), + Mon3 = erlang:monitor(process, P, [{alias, explicit_unalias}]), + Mon4 = erlang:monitor(process, P), + A1 = erlang:alias(), + A2 = erlang:alias(), + do_test_alias(P, A2), + do_test_alias(P, Mon3), + do_test_alias(P, A1), + do_test_alias(P, Mon1, fun demonitor/1), + P ! quit, + {'DOWN', Mon2, process, P, normal} = recv_one(), + {'DOWN', Mon3, process, P, normal} = recv_one(), + {'DOWN', Mon4, process, P, normal} = recv_one(), + ok. + +test_monitor_alias_dead_process() -> + {P, Mon0} = spawn_opt(fun() -> ok end, [monitor]), + {'DOWN', Mon0, process, P, normal} = recv_one(), + Mon1 = erlang:monitor(process, P, [{alias, demonitor}]), + {'DOWN', Mon1, process, P, noproc} = recv_one(), + Mon2 = erlang:monitor(process, P, [{alias, reply_demonitor}]), + {'DOWN', Mon2, process, P, noproc} = recv_one(), + Mon3 = erlang:monitor(process, P, [{alias, explicit_unalias}]), + {'DOWN', Mon3, process, P, noproc} = recv_one(), + ok. + +spawn_monitor(LoopFun, Opts) -> + spawn_opt(LoopFun, [{monitor, Opts}]). + +spawn_and_monitor(LoopFun, Opts) -> + P = spawn_opt(LoopFun, []), + Mon = erlang:monitor(process, P, Opts), + {P, Mon}. + normal_loop() -> receive {Caller, quit} -> Caller ! {self(), finished} end. + +echo_loop() -> + receive + quit -> + ok; + {Msg, ReplyTo} -> + ReplyTo ! Msg, + echo_loop() + end. + +recv_one() -> + receive + Msg -> Msg + after 500 -> timeout + end. diff --git a/tests/erlang_tests/test_refs_ordering.erl b/tests/erlang_tests/test_refs_ordering.erl index 5071ae5633..6b54f8bf24 100644 --- a/tests/erlang_tests/test_refs_ordering.erl +++ b/tests/erlang_tests/test_refs_ordering.erl @@ -20,17 +20,17 @@ -module(test_refs_ordering). --export([start/0, sort/1, insert/2, check/2, get_ref/2]). +-export([start/0, sort/1, insert/2, check/2, get_ref/3, make_alias_ref/0]). start() -> - A = get_ref(3, []), - B = get_ref(7, []), - C = get_ref(1, []), - D = get_ref(3, []), - E = get_ref(4, []), + A = get_ref(3, [], fun make_ref/0), + B = get_ref(7, [], fun make_alias_ref/0), + C = get_ref(1, [], fun make_ref/0), + D = get_ref(3, [], fun make_alias_ref/0), + E = get_ref(4, [], fun make_ref/0), Sorted = sort([E, C, D, A, B]), - check(Sorted, [A, B, C, D, E]) + - bool_to_n(Sorted < [make_ref()]) * 2 + + check(Sorted, [A, C, E, B, D]) + + bool_to_n(Sorted < [make_alias_ref()]) * 2 + bool_to_n(Sorted > {make_ref()}) * 4. sort(L) -> @@ -57,12 +57,25 @@ check(T, Expected) when T == Expected -> check(T, Expected) when T /= Expected -> 0. -get_ref(0, Acc) -> +get_ref(0, Acc, _Generator) -> Acc; -get_ref(N, _Acc) -> - get_ref(N - 1, make_ref()). +get_ref(N, _Acc, Generator) -> + get_ref(N - 1, Generator(), Generator). bool_to_n(true) -> 1; bool_to_n(false) -> 0. + +make_alias_ref() -> + AliasesAvailable = + case erlang:system_info(machine) of + "ATOM" -> true; + "BEAM" -> erlang:system_info(otp_release) >= "24" + end, + if + AliasesAvailable -> + erlang:alias(); + true -> + {mock_alias_ref, make_ref()} + end. From a4575d47abd0678c11cab08c5eacffb61da74429 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 2 Jun 2026 16:01:37 +0000 Subject: [PATCH 02/87] Fix data race in process alias message send Sending to a process reference resolved and deactivated the alias from the sending process: it walked and mutated the target's monitor list (context_find_alias / context_unalias) while holding only the processes_table read lock. That lock only guards against the target being freed, not against the target mutating its own monitor list (via alias/0, unalias/1, demonitor or process teardown), so a send racing with a monitor change could corrupt the list or free a node used by another scheduler -- a data race / use-after-free on SMP. Route alias sends through the mailbox instead, the same way monitor and demonitor operations are already delivered across processes. The send paths now post an AliasMessageSignal carrying {Ref, Message} via the shared helper globalcontext_send_message_to_alias; the owner validates the alias against its own monitor list when it drains signals (context_process_alias_message_signal) and either delivers the message or drops it for an inactive alias. All monitor-list access now happens in the owning process, and the three send paths (erlang:send/2, the send opcode and the JIT send) share one helper. Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 26 ++++++++++++++++++++++++++ src/libAtomVM/context.h | 12 ++++++++++++ src/libAtomVM/globalcontext.c | 17 +++++++++++++++++ src/libAtomVM/globalcontext.h | 14 ++++++++++++++ src/libAtomVM/jit.c | 20 ++++++++------------ src/libAtomVM/mailbox.c | 1 + src/libAtomVM/mailbox.h | 1 + src/libAtomVM/nifs.c | 13 +------------ src/libAtomVM/opcodesswitch.h | 24 ++++++++++-------------- src/libAtomVM/scheduler.c | 1 + 10 files changed, 91 insertions(+), 38 deletions(-) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index 14da1c327b..eaf995e74f 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -218,6 +218,7 @@ void context_destroy(Context *ctx) case FlushInfoMonitorSignal: case LinkExitSignal: // target will not be found when processing this link case MonitorDownSignal: // likewise + case AliasMessageSignal: // process is terminating; drop the alias message case CodeServerResumeSignal: break; case NormalMessage: { @@ -464,6 +465,31 @@ void context_process_monitor_down_signal(Context *ctx, struct TermSignal *signal // (flush option removes messages that were already sent) } +void context_process_alias_message_signal(Context *ctx, struct TermSignal *signal) +{ + // signal->signal_term is a 2-tuple {Ref, Message}. The alias lookup runs here, in the owner's own + // context, against the owner's own monitor list -- the same single-owner discipline that alias/0, + // unalias/1 and demonitor rely on. That is what makes it race-free, unlike checking from the sender. + term ref = term_get_tuple_element(signal->signal_term, 0); + uint64_t ref_ticks = term_to_ref_ticks(ref); + term message = term_get_tuple_element(signal->signal_term, 1); + + struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); + if (alias == NULL) { + // Alias is not (or no longer) active: drop the message, matching OTP. + return; + } + bool reply_demonitor = alias->alias_type == ContextMonitorAliasReplyDemonitor; + + // Re-deliver the payload as a normal message into our own mailbox. + mailbox_send(ctx, message); + + if (reply_demonitor) { + // reply_demonitor: deactivate the alias after the first reply through it. + context_unalias(alias); + } +} + void context_process_code_server_resume_signal(Context *ctx) { #ifndef AVM_NO_JIT diff --git a/src/libAtomVM/context.h b/src/libAtomVM/context.h index cb9f7e0b1a..31e3209d86 100644 --- a/src/libAtomVM/context.h +++ b/src/libAtomVM/context.h @@ -494,6 +494,18 @@ bool context_process_link_exit_signal(Context *ctx, struct TermSignal *signal); */ void context_process_monitor_down_signal(Context *ctx, struct TermSignal *signal); +/** + * @brief Process an alias message signal. + * + * @details The signal term is a 2-tuple {Ref, Message}. If Ref is an active alias of this process, + * Message is delivered as a normal message; otherwise it is dropped. For a reply_demonitor alias the + * alias is also deactivated and the monitor removed. Runs in the owner's own context (no race). + * + * @param ctx the context being executed + * @param signal the signal with the {Ref, Message} tuple + */ +void context_process_alias_message_signal(Context *ctx, struct TermSignal *signal); + /** * @brief Resume execution after module has been loaded * diff --git a/src/libAtomVM/globalcontext.c b/src/libAtomVM/globalcontext.c index d00213284c..43bdb59590 100644 --- a/src/libAtomVM/globalcontext.c +++ b/src/libAtomVM/globalcontext.c @@ -414,6 +414,23 @@ void globalcontext_send_message(GlobalContext *glb, int32_t process_id, term t) } } +void globalcontext_send_message_to_alias(GlobalContext *glb, int32_t process_id, term ref, term message) +{ + Context *p = globalcontext_get_process_lock(glb, process_id); + if (p) { + // Post the alias message as a signal carrying {Ref, Message}. The owner validates the alias + // against its own monitor list (in its own context, when draining signals) and either delivers + // the message or drops it. The sender must never touch the target's monitor list directly. + BEGIN_WITH_STACK_HEAP(TUPLE_SIZE(2), temp_heap) + term tuple = term_alloc_tuple(2, &temp_heap); + term_put_tuple_element(tuple, 0, ref); + term_put_tuple_element(tuple, 1, message); + mailbox_send_term_signal(p, AliasMessageSignal, tuple); + END_WITH_STACK_HEAP(temp_heap, glb) + globalcontext_get_process_unlock(glb, p); + } +} + void globalcontext_send_message_nolock(GlobalContext *glb, int32_t process_id, term t) { Context *p = globalcontext_get_process_nolock(glb, process_id); diff --git a/src/libAtomVM/globalcontext.h b/src/libAtomVM/globalcontext.h index 2876aaab80..b36465e28c 100644 --- a/src/libAtomVM/globalcontext.h +++ b/src/libAtomVM/globalcontext.h @@ -262,6 +262,20 @@ bool globalcontext_process_exists(GlobalContext *glb, int32_t process_id); */ void globalcontext_send_message(GlobalContext *glb, int32_t process_id, term t); +/** + * @brief Send a message to a process alias (process reference). + * + * @details Posts an AliasMessageSignal carrying {Ref, Message} to the owner process. The owner + * validates the alias against its own monitors when it drains signals and either delivers Message + * as a normal message or drops it. This avoids touching the owner's monitor list from the sender. + * + * @param glb the global context (that owns the process table). + * @param process_id the local process id of the alias owner. + * @param ref the process reference (alias) used as the send target. + * @param message the message to send. + */ +void globalcontext_send_message_to_alias(GlobalContext *glb, int32_t process_id, term ref, term message); + /** * @brief Send a message to a process from another process. * There should be a lock on the process table. This variant can be used by diff --git a/src/libAtomVM/jit.c b/src/libAtomVM/jit.c index f25e6aeef7..eb6e474950 100644 --- a/src/libAtomVM/jit.c +++ b/src/libAtomVM/jit.c @@ -861,18 +861,7 @@ static bool jit_send(Context *ctx, JITState *jit_state) ctx->x[0] = ctx->x[1]; } else if (term_is_process_reference(recipient_term)) { int32_t process_id = term_process_ref_to_process_id(recipient_term); - int64_t ref_ticks = term_to_ref_ticks(recipient_term); - Context *p = globalcontext_get_process_lock(ctx->global, process_id); - if (p) { - struct MonitorAlias *alias = context_find_alias(p, ref_ticks); - if (!IS_NULL_PTR(alias)) { - if (alias->alias_type == ContextMonitorAliasReplyDemonitor) { - context_unalias(alias); - } - mailbox_send(p, ctx->x[1]); - } - globalcontext_get_process_unlock(ctx->global, p); - } + globalcontext_send_message_to_alias(ctx->global, process_id, recipient_term, ctx->x[1]); ctx->x[0] = ctx->x[1]; } else if (!term_is_reference(recipient_term)) { set_error(ctx, jit_state, 0, BADARG_ATOM); @@ -1015,6 +1004,13 @@ static Context *jit_process_signal_messages(Context *ctx, JITState *jit_state) reprocess_outer = true; break; } + case AliasMessageSignal: { + struct TermSignal *alias_message_signal + = CONTAINER_OF(signal_message, struct TermSignal, base); + context_process_alias_message_signal(ctx, alias_message_signal); + reprocess_outer = true; + break; + } case CodeServerResumeSignal: { #ifdef JIT_JUMPTABLE_IS_DATA // WASM: saved_function_ptr contains raw label (set by jit_trap_and_load) diff --git a/src/libAtomVM/mailbox.c b/src/libAtomVM/mailbox.c index fe256df7ed..0e6a235fea 100644 --- a/src/libAtomVM/mailbox.c +++ b/src/libAtomVM/mailbox.c @@ -101,6 +101,7 @@ void mailbox_message_dispose(MailboxMessage *m, Heap *heap) case SetGroupLeaderSignal: case LinkExitSignal: case MonitorDownSignal: + case AliasMessageSignal: case UnlinkRemoteIDSignal: case UnlinkRemoteIDAckSignal: { struct TermSignal *term_signal = CONTAINER_OF(m, struct TermSignal, base); diff --git a/src/libAtomVM/mailbox.h b/src/libAtomVM/mailbox.h index 50e7dd1c2b..862dc2324c 100644 --- a/src/libAtomVM/mailbox.h +++ b/src/libAtomVM/mailbox.h @@ -99,6 +99,7 @@ enum MessageType DemonitorSignal, MonitorDownSignal, CodeServerResumeSignal, + AliasMessageSignal, }; struct MailboxMessage diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 2820bc7a89..c67142d4d0 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1816,18 +1816,7 @@ static term nif_erlang_send_2(Context *ctx, int argc, term argv[]) } else if (term_is_process_reference(target)) { int32_t process_id = term_process_ref_to_process_id(target); - int64_t ref_ticks = term_to_ref_ticks(target); - Context *p = globalcontext_get_process_lock(glb, process_id); - if (p) { - struct MonitorAlias *alias = context_find_alias(p, ref_ticks); - if (alias != NULL) { - if (alias->alias_type == ContextMonitorAliasReplyDemonitor) { - context_unalias(alias); - } - mailbox_send(p, argv[1]); - } - globalcontext_get_process_unlock(glb, p); - } + globalcontext_send_message_to_alias(glb, process_id, target, argv[1]); } else if (term_is_atom(target)) { // We need to hold a lock on the processes_table until the message is sent to avoid a race condition, // otherwise the receiving process could be killed at any point between checking it is registered, diff --git a/src/libAtomVM/opcodesswitch.h b/src/libAtomVM/opcodesswitch.h index 49d3b11342..99769863a8 100644 --- a/src/libAtomVM/opcodesswitch.h +++ b/src/libAtomVM/opcodesswitch.h @@ -900,6 +900,13 @@ static void destroy_extended_registers(Context *ctx, unsigned int live) reprocess_outer = true; \ break; \ } \ + case AliasMessageSignal: { \ + struct TermSignal *alias_message_signal \ + = CONTAINER_OF(signal_message, struct TermSignal, base); \ + context_process_alias_message_signal(ctx, alias_message_signal); \ + reprocess_outer = true; \ + break; \ + } \ case CodeServerResumeSignal: { \ context_process_code_server_resume_signal(ctx); \ RESUME(); \ @@ -2378,21 +2385,10 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) TRACE_SEND(ctx, x_regs[0], x_regs[1]); globalcontext_send_message(ctx->global, local_process_id, x_regs[1]); } else if (term_is_process_reference(recipient_term)) { - int32_t local_process_id = term_process_ref_to_process_id(recipient_term); - TRACE("send/0 target_pid=%i\n", local_process_id); + int32_t target_process_id = term_process_ref_to_process_id(recipient_term); + TRACE("send/0 target_pid=%i\n", target_process_id); TRACE_SEND(ctx, x_regs[0], x_regs[1]); - int64_t ref_ticks = term_to_ref_ticks(recipient_term); - Context *p = globalcontext_get_process_lock(ctx->global, local_process_id); - if (p) { - struct MonitorAlias *alias = context_find_alias(p, ref_ticks); - if (alias != NULL) { - if (alias->alias_type == ContextMonitorAliasReplyDemonitor) { - context_unalias(alias); - } - mailbox_send(p, x_regs[1]); - } - globalcontext_get_process_unlock(ctx->global, p); - } + globalcontext_send_message_to_alias(ctx->global, target_process_id, recipient_term, x_regs[1]); } else if (!term_is_reference(recipient_term)) { RAISE_ERROR(BADARG_ATOM); } diff --git a/src/libAtomVM/scheduler.c b/src/libAtomVM/scheduler.c index a856d60012..e4d03d9a0a 100644 --- a/src/libAtomVM/scheduler.c +++ b/src/libAtomVM/scheduler.c @@ -137,6 +137,7 @@ static void scheduler_process_native_signal_messages(Context *ctx) case UnlinkRemoteIDSignal: // ports can't be part of distributed links case UnlinkRemoteIDAckSignal: // id. case CodeServerResumeSignal: // ports do not wait for code server + case AliasMessageSignal: // ports cannot own aliases break; case NormalMessage: { UNREACHABLE(); From b116c261cd400925d250b0a720439c29d20ee589 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 2 Jun 2026 16:13:14 +0000 Subject: [PATCH 03/87] Make reply_demonitor alias remove the monitor on reply A monitor created with {alias, reply_demonitor} previously only deactivated the alias on the first reply through it; the underlying monitor stayed active, so the owner could still receive a stale 'DOWN'. Match OTP: on the first message received through the alias, also remove the monitor and signal the monitored process to drop its side, as erlang:demonitor/1 would. Add a regression test asserting that, after a reply through a reply_demonitor alias, no 'DOWN' is delivered when the monitored process later exits. Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 20 ++++++++++++++++++-- tests/erlang_tests/test_monitor.erl | 13 +++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index eaf995e74f..ab47cf8abe 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -481,12 +481,28 @@ void context_process_alias_message_signal(Context *ctx, struct TermSignal *signa } bool reply_demonitor = alias->alias_type == ContextMonitorAliasReplyDemonitor; + // For reply_demonitor we must also demonitor; capture the monitored pid before context_demonitor + // removes the local monitoring entry below. + bool is_monitoring = false; + term monitor_pid = reply_demonitor + ? context_get_monitor_pid(ctx, ref_ticks, &is_monitoring) + : term_invalid_term(); + // Re-deliver the payload as a normal message into our own mailbox. mailbox_send(ctx, message); if (reply_demonitor) { - // reply_demonitor: deactivate the alias after the first reply through it. - context_unalias(alias); + // Deactivate the alias and remove the local monitor, then tell the monitored process to drop + // its side of the monitor (as erlang:demonitor/1 does). + context_demonitor(ctx, ref_ticks); + if (!term_is_invalid_term(monitor_pid) && is_monitoring) { + int32_t monitored_process_id = term_to_local_process_id(monitor_pid); + Context *target = globalcontext_get_process_lock(ctx->global, monitored_process_id); + if (target) { + mailbox_send_ref_signal(target, DemonitorSignal, ref_ticks); + globalcontext_get_process_unlock(ctx->global, target); + } + } } } diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 7d742fee44..f67ae4a312 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -55,6 +55,8 @@ start() -> ok = test_monitor_alias_explicit_unalias(fun spawn_and_monitor/2), ok = test_monitor_alias_reply_demonitor(fun spawn_monitor/2), ok = test_monitor_alias_reply_demonitor(fun spawn_and_monitor/2), + ok = test_reply_demonitor_removes_monitor(fun spawn_monitor/2), + ok = test_reply_demonitor_removes_monitor(fun spawn_and_monitor/2), ok = test_monitor_down_alias(fun spawn_monitor/2), ok = test_monitor_down_alias(fun spawn_and_monitor/2), ok; @@ -319,6 +321,17 @@ test_monitor_alias_reply_demonitor(SpawnFun) -> do_test_alias(P, Mon, fun(_Mon) -> ok end), ok. +%% reply_demonitor must, on the first reply through the alias, also remove the underlying monitor +%% (like demonitor/1), so no 'DOWN' is delivered when the monitored process later exits. +test_reply_demonitor_removes_monitor(SpawnFun) -> + {P, Mon} = SpawnFun(fun echo_loop/0, [{alias, reply_demonitor}]), + Ref = make_ref(), + P ! {{reply, Ref}, Mon}, + {reply, Ref} = recv_one(), + P ! quit, + timeout = recv_one(), + ok. + test_monitor_down_alias(SpawnFun) -> {P, Mon} = SpawnFun(fun echo_loop/0, [{alias, demonitor}]), erlang:unalias(Mon), From 7b2ec4be8b745a045b92d91fbdea2af60349c9fe Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 2 Jun 2026 17:59:58 +0000 Subject: [PATCH 04/87] Drop OTP < 26 gating from alias tests Aliases require OTP 24, so the alias tests guarded their alias-specific cases behind a runtime OTP-version check (plus a make_ref-based fallback for older BEAM). AtomVM no longer supports OTP < 26, where aliases are always available, so those checks are dead code: run the alias cases unconditionally and drop the is_atomvm_or_otp_version_at_least/1 helper and the mock-alias fallback. Signed-off-by: Davide Bettio --- tests/erlang_tests/test_binary_to_term.erl | 18 ++------- tests/erlang_tests/test_monitor.erl | 45 ++++++++-------------- tests/erlang_tests/test_refs_ordering.erl | 12 +----- 3 files changed, 21 insertions(+), 54 deletions(-) diff --git a/tests/erlang_tests/test_binary_to_term.erl b/tests/erlang_tests/test_binary_to_term.erl index 5bb2e58104..d499e31e43 100644 --- a/tests/erlang_tests/test_binary_to_term.erl +++ b/tests/erlang_tests/test_binary_to_term.erl @@ -996,15 +996,9 @@ test_encode_resource(OTPVersion) -> false = AlteredResource4 =:= Resource, ok. test_encode_process_ref() -> - AliasesAvailable = is_atomvm_or_otp_version_at_least("24"), - if - AliasesAvailable -> - ProcessRef = erlang:alias(), - ProcessRef = binary_to_term(term_to_binary(ProcessRef)), - ok; - true -> - ok - end. + ProcessRef = erlang:alias(), + ProcessRef = binary_to_term(term_to_binary(ProcessRef)), + ok. % Verify term_to_binary(binary_to_term(Bin)) is idempotent. binary_to_term_idempotent(Binary, _OTPVersion) -> @@ -1012,12 +1006,6 @@ binary_to_term_idempotent(Binary, _OTPVersion) -> Binary = term_to_binary(Term), Term. -is_atomvm_or_otp_version_at_least(OTPVersion) -> - case erlang:system_info(machine) of - "ATOM" -> true; - "BEAM" -> erlang:system_info(otp_release) >= OTPVersion - end. - test_atom_encoding() -> true = compare_pair_encoding(latin1_as_utf8_1), true = compare_pair_encoding(latin1_as_utf8_2), diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index f67ae4a312..af23f780c6 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -35,34 +35,23 @@ start() -> ok = test_monitor_registered(), ok = test_monitor_registered_noproc(), - AliasesAvailable = - case erlang:system_info(machine) of - "ATOM" -> true; - "BEAM" -> erlang:system_info(otp_release) >= "24" - end, - if - AliasesAvailable -> - ok = test_alias(), - ok = test_multiple_aliases(), - ok = test_multiple_unaliases(), - ok = test_unalias_from_wrong_process(), - ok = test_monitor_alias_dead_process(), - ok = test_monitor_multiple_aliases_monitors(fun spawn_monitor/2), - ok = test_monitor_multiple_aliases_monitors(fun spawn_and_monitor/2), - ok = test_monitor_alias_demonitor(fun spawn_monitor/2), - ok = test_monitor_alias_demonitor(fun spawn_and_monitor/2), - ok = test_monitor_alias_explicit_unalias(fun spawn_monitor/2), - ok = test_monitor_alias_explicit_unalias(fun spawn_and_monitor/2), - ok = test_monitor_alias_reply_demonitor(fun spawn_monitor/2), - ok = test_monitor_alias_reply_demonitor(fun spawn_and_monitor/2), - ok = test_reply_demonitor_removes_monitor(fun spawn_monitor/2), - ok = test_reply_demonitor_removes_monitor(fun spawn_and_monitor/2), - ok = test_monitor_down_alias(fun spawn_monitor/2), - ok = test_monitor_down_alias(fun spawn_and_monitor/2), - ok; - true -> - ok - end, + ok = test_alias(), + ok = test_multiple_aliases(), + ok = test_multiple_unaliases(), + ok = test_unalias_from_wrong_process(), + ok = test_monitor_alias_dead_process(), + ok = test_monitor_multiple_aliases_monitors(fun spawn_monitor/2), + ok = test_monitor_multiple_aliases_monitors(fun spawn_and_monitor/2), + ok = test_monitor_alias_demonitor(fun spawn_monitor/2), + ok = test_monitor_alias_demonitor(fun spawn_and_monitor/2), + ok = test_monitor_alias_explicit_unalias(fun spawn_monitor/2), + ok = test_monitor_alias_explicit_unalias(fun spawn_and_monitor/2), + ok = test_monitor_alias_reply_demonitor(fun spawn_monitor/2), + ok = test_monitor_alias_reply_demonitor(fun spawn_and_monitor/2), + ok = test_reply_demonitor_removes_monitor(fun spawn_monitor/2), + ok = test_reply_demonitor_removes_monitor(fun spawn_and_monitor/2), + ok = test_monitor_down_alias(fun spawn_monitor/2), + ok = test_monitor_down_alias(fun spawn_and_monitor/2), 0. test_monitor_normal() -> diff --git a/tests/erlang_tests/test_refs_ordering.erl b/tests/erlang_tests/test_refs_ordering.erl index 6b54f8bf24..f1766506b4 100644 --- a/tests/erlang_tests/test_refs_ordering.erl +++ b/tests/erlang_tests/test_refs_ordering.erl @@ -68,14 +68,4 @@ bool_to_n(false) -> 0. make_alias_ref() -> - AliasesAvailable = - case erlang:system_info(machine) of - "ATOM" -> true; - "BEAM" -> erlang:system_info(otp_release) >= "24" - end, - if - AliasesAvailable -> - erlang:alias(); - true -> - {mock_alias_ref, make_ref()} - end. + erlang:alias(). From 0d12ca22507820bea0062fb452ed67eb6b77e16d Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 3 Jun 2026 09:24:01 +0000 Subject: [PATCH 05/87] Deactivate alias on automatic monitor removal at 'DOWN' For {alias, demonitor} and {alias, reply_demonitor}, the alias is deactivated when the monitor is removed, including the automatic removal when a 'DOWN' is delivered (as erlang:demonitor/1 already does for an explicit demonitor). context_process_monitor_down_signal removed the monitor but left the alias active, so a third party could still reach the owner through the alias after the monitored process had exited. Deactivate the alias (unless it is explicit_unalias) in both the local and registered-name 'DOWN' paths, and add a regression test. Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 15 +++++++++++++++ tests/erlang_tests/test_monitor.erl | 14 ++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index ab47cf8abe..c2596d288b 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -436,6 +436,14 @@ void context_process_monitor_down_signal(Context *ctx, struct TermSignal *signal // Remove link list_remove(&monitor->monitor_list_head); free(monitoring_monitor); + + // {alias, demonitor} / {alias, reply_demonitor}: the alias is deactivated when the + // monitor is removed, including the automatic removal at 'DOWN' delivery. + struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); + if (alias != NULL && alias->alias_type != ContextMonitorAliasExplicitUnalias) { + context_unalias(alias); + } + // Enqueue the term as a message. mailbox_send(ctx, signal->signal_term); break; @@ -456,6 +464,13 @@ void context_process_monitor_down_signal(Context *ctx, struct TermSignal *signal mailbox_send(ctx, signal->signal_term); END_WITH_STACK_HEAP(temp_heap, ctx->global); + // {alias, demonitor} / {alias, reply_demonitor}: deactivate the alias on the + // monitor's automatic removal at 'DOWN' delivery. + struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); + if (alias != NULL && alias->alias_type != ContextMonitorAliasExplicitUnalias) { + context_unalias(alias); + } + free(monitoring_monitor); break; } diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index af23f780c6..2e86f84830 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -52,6 +52,8 @@ start() -> ok = test_reply_demonitor_removes_monitor(fun spawn_and_monitor/2), ok = test_monitor_down_alias(fun spawn_monitor/2), ok = test_monitor_down_alias(fun spawn_and_monitor/2), + ok = test_monitor_alias_demonitor_deactivates_on_down(fun spawn_monitor/2), + ok = test_monitor_alias_demonitor_deactivates_on_down(fun spawn_and_monitor/2), 0. test_monitor_normal() -> @@ -331,6 +333,18 @@ test_monitor_down_alias(SpawnFun) -> {'DOWN', Mon, process, P, normal} = recv_one(), ok. +%% {alias, demonitor}: when the monitor is auto-removed on 'DOWN' delivery, the alias must be +%% deactivated too, so a third party can no longer reach us through it. +test_monitor_alias_demonitor_deactivates_on_down(SpawnFun) -> + {P, Mon} = SpawnFun(fun echo_loop/0, [{alias, demonitor}]), + P ! quit, + {'DOWN', Mon, process, P, normal} = recv_one(), + Echo = spawn_opt(fun echo_loop/0, []), + Echo ! {should_drop, Mon}, + timeout = recv_one(), + Echo ! quit, + ok. + test_monitor_multiple_aliases_monitors(SpawnFun) -> {P, Mon1} = SpawnFun(fun echo_loop/0, [{alias, demonitor}]), Mon2 = erlang:monitor(process, P, [{alias, reply_demonitor}]), From a5642b299ad0c6b82c9803d638387e5ec95e3074 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 3 Jun 2026 09:26:13 +0000 Subject: [PATCH 06/87] Release process lock on monitor/3 kind-mismatch error nif_erlang_monitor locks the target process, then raises badarg when the object type does not match the target (monitor(process, Port, ...) or vice versa). That error path returned without releasing the processes-table lock, unlike the three sibling out-of-memory error paths in the same function, leaking the lock and deadlocking the next writer on SMP builds. Unlock the target before raising. (A behavioural test would deadlock rather than fail; verified by inspection against the sibling error paths.) Signed-off-by: Davide Bettio --- src/libAtomVM/nifs.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index c67142d4d0..d66e3e07d2 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -5070,6 +5070,7 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) } if ((object_type == PROCESS_ATOM && target->native_handler != NULL) || (object_type == PORT_ATOM && target->native_handler == NULL)) { + globalcontext_get_process_unlock(ctx->global, target); RAISE_ERROR(BADARG_ATOM); } From 7b95b6e73a1b174ad4a7943ff4d7d4f53e5b1ed4 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 3 Jun 2026 09:29:18 +0000 Subject: [PATCH 07/87] Reserve monitor/3 and spawn_opt result space before publishing monitors Both nif_erlang_monitor and do_spawn published the monitor/alias state (queuing a MonitorSignal to the target and adding monitors to the contexts) and only then reserved heap for the returned reference. An out-of-memory at that last step raised, leaving the monitor half-installed: the target could later send a 'DOWN' for a reference the caller never received. Reserve the result space before publishing, freeing the monitor structs on failure. GC at the new point is safe -- the monitor structs hold only an immediate pid and a plain RefData, none reachable from the heap. Unwinding the separate spawn_opt link side effect on a later OOM is left out of scope. Signed-off-by: Davide Bettio --- src/libAtomVM/nifs.c | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index d66e3e07d2..782d764ccf 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1598,6 +1598,18 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free RAISE_ERROR(OUT_OF_MEMORY_ATOM); } + // Reserve the result tuple/ref space before publishing the monitors, so an OOM cannot leave + // ctx with monitors (and new_ctx queued to run) while the caller gets an exception and never + // receives {Pid, Ref}. GC here is safe: new_pid and ref_data are immediates. + int res_size = TERM_BOXED_REFERENCE_PROCESS_SIZE + TUPLE_SIZE(2); + if (UNLIKELY(memory_ensure_free_opt(ctx, res_size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + free(alias_monitor); + free(self_monitor); + free(new_monitor); + context_destroy(new_ctx); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + // We can call context_add_monitor directly on new process because it's not started yet context_add_monitor(new_ctx, new_monitor); context_add_monitor(ctx, self_monitor); @@ -1607,12 +1619,6 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free } if (is_spawn_monitor) { - int res_size = TERM_BOXED_REFERENCE_PROCESS_SIZE + TUPLE_SIZE(2); - if (UNLIKELY(memory_ensure_free_opt(ctx, res_size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { - context_destroy(new_ctx); - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } - scheduler_init_ready(new_ctx); term ref = term_from_ref_data(&ref_data, &ctx->heap); @@ -5105,6 +5111,19 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) globalcontext_get_process_unlock(ctx->global, target); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } + + // Reserve the result reference space *before* publishing any monitor/alias state, so an + // out-of-memory here cannot leave the target with a queued MonitorSignal (or ctx with monitors) + // while the caller gets an exception and never receives the reference. GC here is safe: the + // monitor structs hold only an immediate pid and a plain RefData, none reachable from the heap. + if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_PROCESS_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + free(alias_monitor); + free(self_monitor); + free(other_monitor); + globalcontext_get_process_unlock(ctx->global, target); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + mailbox_send_monitor_signal(target, MonitorSignal, other_monitor); globalcontext_get_process_unlock(ctx->global, target); @@ -5113,10 +5132,6 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) context_add_monitor(ctx, alias_monitor); } - if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_PROCESS_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } - return term_from_ref_data(&ref_data, &ctx->heap); } From 1741064549d7b14be66de42a712199deb9742b85 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 3 Jun 2026 09:55:48 +0000 Subject: [PATCH 08/87] Preserve send order for alias messages An alias send is delivered via an AliasMessageSignal so the owner validates the alias in its own context (race-free). But mailbox_process_outer_list moved normal messages straight to the inner mailbox while signals were processed afterwards and re-posted, so `Alias ! m1, Pid ! m2` to the same receiver could deliver m2 before m1 -- violating the single-sender message ordering guarantee. Convert alias messages to normal messages in place during the outer-list reverse (new mailbox_process_outer_list_with_aliases), prepending them into the normal stream so they keep send order relative to plain messages from the same sender. context_process_alias_message_signal now returns the payload instead of re-posting it. Teardown and the port scheduler keep the plain mailbox_process_outer_list (which returns alias signals to drop): they must not run the reply_demonitor path, which takes the processes-table lock that context_destroy already holds. Add a regression test (alias send then pid send keep order). Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 28 ++++++++++++---------------- src/libAtomVM/context.h | 11 +++++++---- src/libAtomVM/jit.c | 12 +++--------- src/libAtomVM/mailbox.c | 29 ++++++++++++++++++++++++++++- src/libAtomVM/mailbox.h | 14 +++++++++++++- src/libAtomVM/opcodesswitch.h | 12 +++--------- tests/erlang_tests/test_monitor.erl | 21 +++++++++++++++++++++ 7 files changed, 87 insertions(+), 40 deletions(-) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index c2596d288b..5183d5b6b9 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -480,11 +480,13 @@ void context_process_monitor_down_signal(Context *ctx, struct TermSignal *signal // (flush option removes messages that were already sent) } -void context_process_alias_message_signal(Context *ctx, struct TermSignal *signal) +term context_process_alias_message_signal(Context *ctx, struct TermSignal *signal) { // signal->signal_term is a 2-tuple {Ref, Message}. The alias lookup runs here, in the owner's own // context, against the owner's own monitor list -- the same single-owner discipline that alias/0, // unalias/1 and demonitor rely on. That is what makes it race-free, unlike checking from the sender. + // Returns the message to deliver as a normal message, or an invalid term if the alias is inactive. + // The caller delivers it in send order, so an alias send is not reordered against a plain send. term ref = term_get_tuple_element(signal->signal_term, 0); uint64_t ref_ticks = term_to_ref_ticks(ref); term message = term_get_tuple_element(signal->signal_term, 1); @@ -492,23 +494,15 @@ void context_process_alias_message_signal(Context *ctx, struct TermSignal *signa struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); if (alias == NULL) { // Alias is not (or no longer) active: drop the message, matching OTP. - return; + return term_invalid_term(); } - bool reply_demonitor = alias->alias_type == ContextMonitorAliasReplyDemonitor; - // For reply_demonitor we must also demonitor; capture the monitored pid before context_demonitor - // removes the local monitoring entry below. - bool is_monitoring = false; - term monitor_pid = reply_demonitor - ? context_get_monitor_pid(ctx, ref_ticks, &is_monitoring) - : term_invalid_term(); - - // Re-deliver the payload as a normal message into our own mailbox. - mailbox_send(ctx, message); - - if (reply_demonitor) { - // Deactivate the alias and remove the local monitor, then tell the monitored process to drop - // its side of the monitor (as erlang:demonitor/1 does). + if (alias->alias_type == ContextMonitorAliasReplyDemonitor) { + // reply_demonitor: deactivate the alias and remove the local monitor, then tell the monitored + // process to drop its side of the monitor (as erlang:demonitor/1 does). Capture the monitored + // pid before context_demonitor removes the local monitoring entry. + bool is_monitoring = false; + term monitor_pid = context_get_monitor_pid(ctx, ref_ticks, &is_monitoring); context_demonitor(ctx, ref_ticks); if (!term_is_invalid_term(monitor_pid) && is_monitoring) { int32_t monitored_process_id = term_to_local_process_id(monitor_pid); @@ -519,6 +513,8 @@ void context_process_alias_message_signal(Context *ctx, struct TermSignal *signa } } } + + return message; } void context_process_code_server_resume_signal(Context *ctx) diff --git a/src/libAtomVM/context.h b/src/libAtomVM/context.h index 31e3209d86..db36c17d98 100644 --- a/src/libAtomVM/context.h +++ b/src/libAtomVM/context.h @@ -497,14 +497,17 @@ void context_process_monitor_down_signal(Context *ctx, struct TermSignal *signal /** * @brief Process an alias message signal. * - * @details The signal term is a 2-tuple {Ref, Message}. If Ref is an active alias of this process, - * Message is delivered as a normal message; otherwise it is dropped. For a reply_demonitor alias the - * alias is also deactivated and the monitor removed. Runs in the owner's own context (no race). + * @details The signal term is a 2-tuple {Ref, Message}. If Ref is an active alias of this process the + * Message is returned for delivery as a normal message; otherwise an invalid term is returned and the + * message is dropped. For a reply_demonitor alias the alias is also deactivated and the monitor + * removed. Runs in the owner's own context (no race). The caller delivers the returned message in send + * order so an alias send is not reordered against a plain send. * * @param ctx the context being executed * @param signal the signal with the {Ref, Message} tuple + * @return the message to deliver, or an invalid term to drop it */ -void context_process_alias_message_signal(Context *ctx, struct TermSignal *signal); +term context_process_alias_message_signal(Context *ctx, struct TermSignal *signal); /** * @brief Resume execution after module has been loaded diff --git a/src/libAtomVM/jit.c b/src/libAtomVM/jit.c index eb6e474950..882fc716cb 100644 --- a/src/libAtomVM/jit.c +++ b/src/libAtomVM/jit.c @@ -892,7 +892,7 @@ static term *jit_extended_register_ptr(Context *ctx, unsigned int index) static Context *jit_process_signal_messages(Context *ctx, JITState *jit_state) { TRACE("jit_process_signal_messages\n"); - MailboxMessage *signal_message = mailbox_process_outer_list(&ctx->mailbox); + MailboxMessage *signal_message = mailbox_process_outer_list_with_aliases(ctx); bool handle_error = false; bool reprocess_outer = false; while (signal_message) { @@ -1004,13 +1004,6 @@ static Context *jit_process_signal_messages(Context *ctx, JITState *jit_state) reprocess_outer = true; break; } - case AliasMessageSignal: { - struct TermSignal *alias_message_signal - = CONTAINER_OF(signal_message, struct TermSignal, base); - context_process_alias_message_signal(ctx, alias_message_signal); - reprocess_outer = true; - break; - } case CodeServerResumeSignal: { #ifdef JIT_JUMPTABLE_IS_DATA // WASM: saved_function_ptr contains raw label (set by jit_trap_and_load) @@ -1026,6 +1019,7 @@ static Context *jit_process_signal_messages(Context *ctx, JITState *jit_state) #endif break; } + case AliasMessageSignal: case NormalMessage: { UNREACHABLE(); } @@ -1035,7 +1029,7 @@ static Context *jit_process_signal_messages(Context *ctx, JITState *jit_state) signal_message = next; if (UNLIKELY(reprocess_outer && signal_message == NULL)) { reprocess_outer = false; - signal_message = mailbox_process_outer_list(&ctx->mailbox); + signal_message = mailbox_process_outer_list_with_aliases(ctx); } } if (context_get_flags(ctx, Killed)) { diff --git a/src/libAtomVM/mailbox.c b/src/libAtomVM/mailbox.c index 0e6a235fea..7f1bab67c6 100644 --- a/src/libAtomVM/mailbox.c +++ b/src/libAtomVM/mailbox.c @@ -22,6 +22,7 @@ #include +#include "context.h" #include "memory.h" #include "scheduler.h" #include "synclist.h" @@ -365,7 +366,7 @@ void mailbox_reset(Mailbox *mbox) mbox->receive_pointer_prev = NULL; } -MailboxMessage *mailbox_process_outer_list(Mailbox *mbox) +static MailboxMessage *mailbox_process_outer_list_internal(Context *ctx, Mailbox *mbox) { // Empty outer list using CAS MailboxMessage *current = mbox->outer_first; @@ -388,6 +389,22 @@ MailboxMessage *mailbox_process_outer_list(Mailbox *mbox) } current->next = previous_normal; previous_normal = current; + } else if (ctx != NULL && current->type == AliasMessageSignal) { + // Convert an alias message to a normal message in place, validating the alias in the + // owner's own context. Prepending it to the normal stream during this reverse keeps it + // ordered with plain sends from the same sender (alias and pid sends must not be reordered). + term message = context_process_alias_message_signal(ctx, CONTAINER_OF(current, struct TermSignal, base)); + if (!term_is_invalid_term(message)) { + MailboxMessage *converted = mailbox_message_create_from_term(NormalMessage, message); + if (converted != NULL) { + if (last_normal == NULL) { + last_normal = converted; + } + converted->next = previous_normal; + previous_normal = converted; + } + } + mailbox_message_dispose(current, &ctx->heap); } else { current->next = previous_signal; previous_signal = current; @@ -423,6 +440,16 @@ MailboxMessage *mailbox_process_outer_list(Mailbox *mbox) return previous_signal; } +MailboxMessage *mailbox_process_outer_list(Mailbox *mbox) +{ + return mailbox_process_outer_list_internal(NULL, mbox); +} + +MailboxMessage *mailbox_process_outer_list_with_aliases(Context *ctx) +{ + return mailbox_process_outer_list_internal(ctx, &ctx->mailbox); +} + void mailbox_next(Mailbox *mbox) { // This is called from OP_LOOP_REC_END opcode, so we cannot make any diff --git a/src/libAtomVM/mailbox.h b/src/libAtomVM/mailbox.h index 862dc2324c..77cbad7d54 100644 --- a/src/libAtomVM/mailbox.h +++ b/src/libAtomVM/mailbox.h @@ -214,12 +214,24 @@ size_t mailbox_size(Mailbox *mbox); /** * @brief Process the outer list of messages. * - * @details To be called from the process only + * @details To be called from the process only. Alias messages (if any) are returned as signals. * @param mbox the mailbox to work with * @return the signal messages in received order. */ MailboxMessage *mailbox_process_outer_list(Mailbox *mbox); +/** + * @brief Process the outer list of messages, delivering alias messages in send order. + * + * @details Like mailbox_process_outer_list, but AliasMessageSignals are validated in ctx's own + * monitor list and, when the alias is active, converted to normal messages in place so they keep + * send order relative to plain messages from the same sender. To be called from the process only, + * while not holding the processes table lock (the reply_demonitor path takes it). + * @param ctx the context whose mailbox is processed + * @return the remaining (non-alias) signal messages in received order. + */ +MailboxMessage *mailbox_process_outer_list_with_aliases(Context *ctx); + /** * @brief Sends a message to a certain mailbox. * diff --git a/src/libAtomVM/opcodesswitch.h b/src/libAtomVM/opcodesswitch.h index 99769863a8..0293286313 100644 --- a/src/libAtomVM/opcodesswitch.h +++ b/src/libAtomVM/opcodesswitch.h @@ -788,7 +788,7 @@ static void destroy_extended_registers(Context *ctx, unsigned int live) #define PROCESS_SIGNAL_MESSAGES() \ { \ - MailboxMessage *signal_message = mailbox_process_outer_list(&ctx->mailbox); \ + MailboxMessage *signal_message = mailbox_process_outer_list_with_aliases(ctx); \ bool handle_error = false; \ bool reprocess_outer = false; \ while (signal_message) { \ @@ -900,18 +900,12 @@ static void destroy_extended_registers(Context *ctx, unsigned int live) reprocess_outer = true; \ break; \ } \ - case AliasMessageSignal: { \ - struct TermSignal *alias_message_signal \ - = CONTAINER_OF(signal_message, struct TermSignal, base); \ - context_process_alias_message_signal(ctx, alias_message_signal); \ - reprocess_outer = true; \ - break; \ - } \ case CodeServerResumeSignal: { \ context_process_code_server_resume_signal(ctx); \ RESUME(); \ break; \ } \ + case AliasMessageSignal: \ case NormalMessage: { \ UNREACHABLE(); \ } \ @@ -921,7 +915,7 @@ static void destroy_extended_registers(Context *ctx, unsigned int live) signal_message = next; \ if (UNLIKELY(reprocess_outer && signal_message == NULL)) { \ reprocess_outer = false; \ - signal_message = mailbox_process_outer_list(&ctx->mailbox); \ + signal_message = mailbox_process_outer_list_with_aliases(ctx); \ } \ } \ if (context_get_flags(ctx, Killed)) { \ diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 2e86f84830..f0c25cb41c 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -54,6 +54,7 @@ start() -> ok = test_monitor_down_alias(fun spawn_and_monitor/2), ok = test_monitor_alias_demonitor_deactivates_on_down(fun spawn_monitor/2), ok = test_monitor_alias_demonitor_deactivates_on_down(fun spawn_and_monitor/2), + ok = test_alias_pid_send_order(), 0. test_monitor_normal() -> @@ -345,6 +346,26 @@ test_monitor_alias_demonitor_deactivates_on_down(SpawnFun) -> Echo ! quit, ok. +%% Messages from one sender to one receiver must keep send order, even when one goes via an alias +%% (signal path) and the next via the pid (normal message). +test_alias_pid_send_order() -> + Parent = self(), + P = spawn_opt( + fun() -> + Alias = erlang:alias(), + Parent ! {ready, self(), Alias}, + receive A -> Parent ! {got, A} end, + receive B -> Parent ! {got, B} end + end, + [] + ), + {ready, P, Alias} = recv_one(), + Alias ! m1, + P ! m2, + {got, m1} = recv_one(), + {got, m2} = recv_one(), + ok. + test_monitor_multiple_aliases_monitors(SpawnFun) -> {P, Mon1} = SpawnFun(fun echo_loop/0, [{alias, demonitor}]), Mon2 = erlang:monitor(process, P, [{alias, reply_demonitor}]), From 0d3b04865ee4777d5c127395b3ed8731c0a2c6ad Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 3 Jun 2026 09:59:20 +0000 Subject: [PATCH 09/87] Compare local process references consistently against external refs term_compare sizes a process reference as 3 words (process_id + ref_ticks) in the local-vs-local branch, but the mixed local-vs-external branch still sized it as 2 words, dropping process_id. A process reference was therefore ordered differently depending on whether it was compared against a local or an external reference, which can make the term order inconsistent. (The reviewer's "two process refs with equal ref_ticks" case cannot actually occur: ref_ticks come from a global monotonic counter, so they are unique.) Size process references as 3 words in the mixed branch too and add the matching extraction, mirroring the local-vs-local logic. Exercising this path requires an external reference (distribution), so it is verified by inspection. Signed-off-by: Davide Bettio --- src/libAtomVM/term.c | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/libAtomVM/term.c b/src/libAtomVM/term.c index 7fdfde55d3..d24f766aa7 100644 --- a/src/libAtomVM/term.c +++ b/src/libAtomVM/term.c @@ -755,6 +755,8 @@ TermCompareResult term_compare(term t, term other, TermCompareOpts opts, GlobalC len = term_get_external_reference_len(t); } else if (term_is_resource_reference(t)) { len = 4; + } else if (term_is_process_reference(t)) { + len = 3; } else { len = 2; } @@ -762,6 +764,8 @@ TermCompareResult term_compare(term t, term other, TermCompareOpts opts, GlobalC other_len = term_get_external_reference_len(other); } else if (term_is_resource_reference(other)) { other_len = 4; + } else if (term_is_process_reference(other)) { + other_len = 3; } else { other_len = 2; } @@ -786,6 +790,27 @@ TermCompareResult term_compare(term t, term other, TermCompareOpts opts, GlobalC local_data[1] = (uint32_t) ref_ticks; other_data = local_data; } + } else if (len == 3) { + // len == 3 (one is a local process reference) + uint32_t local_data[3]; + if (term_is_external(t)) { + data = term_get_external_reference_words(t); + } else { + local_data[0] = term_process_ref_to_process_id(t); + int64_t ref_ticks = term_to_ref_ticks(t); + local_data[1] = ref_ticks >> 32; + local_data[2] = (uint32_t) ref_ticks; + data = local_data; + } + if (term_is_external(other)) { + other_data = term_get_external_reference_words(other); + } else { + local_data[0] = term_process_ref_to_process_id(other); + int64_t ref_ticks = term_to_ref_ticks(other); + local_data[1] = ref_ticks >> 32; + local_data[2] = (uint32_t) ref_ticks; + other_data = local_data; + } } else { // len == 4 (one is a resource) uint32_t local_data[4]; From 783976bccaa9fe2c0825c587cf0fe6400bb16fde Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 3 Jun 2026 10:00:25 +0000 Subject: [PATCH 10/87] Reject out-of-range process id when decoding a process reference binary_to_term of a NEWER_REFERENCE_EXT with len == 3 turned the third word straight into a process reference id without bounding it. Note this was not a memory-safety issue as the reviewer suggested: the id flows into globalcontext_get_process_lock, which performs a linear search of the process table (no out-of-bounds indexing), so an out-of-range value simply matches no process. Still, validate it like a local pid (TERM_MAX_LOCAL_PROCESS_ID) to reject clearly malformed input early. Signed-off-by: Davide Bettio --- src/libAtomVM/external_term.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libAtomVM/external_term.c b/src/libAtomVM/external_term.c index 448a60d06b..34cc7b7f59 100644 --- a/src/libAtomVM/external_term.c +++ b/src/libAtomVM/external_term.c @@ -1013,6 +1013,10 @@ static term parse_external_terms(const uint8_t *external_term_buf, size_t *eterm } else if (len == 3 && node == this_node && creation == this_creation) { uint64_t ticks = ((uint64_t) data[0]) << 32 | data[1]; uint32_t process_id = data[2]; + // Reject a malformed process id (the value comes from untrusted input). + if (process_id > TERM_MAX_LOCAL_PROCESS_ID) { + return term_invalid_term(); + } return term_make_process_reference(process_id, ticks, heap); } else if (len == 4 && node == this_node && creation == this_creation) { // This is a resource From db4bc4ffe64456c3aa16da7cad27afdac94f0141 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 3 Jun 2026 10:05:06 +0000 Subject: [PATCH 11/87] Return a real alias from monitor/3 on the self and noproc paths monitor(process, X, [{alias, ...}]) returned a plain short reference (no alias) when X was self() or an already-dead process, so the returned reference could not be used to send to the process or be passed to unalias/1. OTP returns a usable alias in both cases. Install the alias and return a process reference on both early-return paths. For self the alias stays active (self never sends a DOWN). On the noproc path the monitor is immediately removed by the noproc DOWN, so only an explicit_unalias alias stays active (demonitor / reply_demonitor are deactivated right away, as at a real DOWN). Add regression tests for both. Signed-off-by: Davide Bettio --- src/libAtomVM/nifs.c | 39 +++++++++++++++++++++++------ tests/erlang_tests/test_monitor.erl | 26 +++++++++++++++++++ 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 782d764ccf..7cc541918d 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -5038,14 +5038,26 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) target = NULL; } else { local_process_id = term_to_local_process_id(target_pid); - // Monitoring self is possible but no monitor is actually created + // Monitoring self is possible but no monitor is actually created (self never sends a DOWN). + // With {alias, _} an alias is still installed so the returned reference is a usable alias. if (UNLIKELY(local_process_id == ctx->process_id)) { - if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_SHORT_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + RefData ref_data = { .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = is_alias ? ctx->process_id : INVALID_PROCESS_ID }; + struct Monitor *alias_monitor = NULL; + if (is_alias) { + alias_monitor = monitor_alias_new(&ref_data, alias_type); + if (IS_NULL_PTR(alias_monitor)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + } + size_t self_ref_size = is_alias ? TERM_BOXED_REFERENCE_PROCESS_SIZE : TERM_BOXED_REFERENCE_SHORT_SIZE; + if (UNLIKELY(memory_ensure_free_opt(ctx, self_ref_size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + free(alias_monitor); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - uint64_t ref_ticks = globalcontext_get_ref_ticks(ctx->global); - term ref = term_from_ref_ticks(ref_ticks, &ctx->heap); - return ref; + if (is_alias) { + context_add_monitor(ctx, alias_monitor); + } + return term_from_ref_data(&ref_data, &ctx->heap); } target = globalcontext_get_process_lock(ctx->global, local_process_id); @@ -5053,11 +5065,24 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) if (IS_NULL_PTR(target)) { int res_size = TERM_BOXED_REFERENCE_PROCESS_SIZE + TUPLE_SIZE(5) + target_proc_size; + RefData ref_data = { .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = is_alias ? ctx->process_id : INVALID_PROCESS_ID }; + // The monitor is immediately removed by the noproc DOWN, so only an explicit_unalias alias + // stays active (demonitor / reply_demonitor would be deactivated right away, as at a DOWN). + struct Monitor *alias_monitor = NULL; + if (is_alias && alias_type == ContextMonitorAliasExplicitUnalias) { + alias_monitor = monitor_alias_new(&ref_data, alias_type); + if (IS_NULL_PTR(alias_monitor)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + } if (UNLIKELY(memory_ensure_free_opt(ctx, res_size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + free(alias_monitor); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - uint64_t ref_ticks = globalcontext_get_ref_ticks(ctx->global); - term ref = term_from_ref_ticks(ref_ticks, &ctx->heap); + if (alias_monitor != NULL) { + context_add_monitor(ctx, alias_monitor); + } + term ref = term_from_ref_data(&ref_data, &ctx->heap); term down_message_tuple = term_alloc_tuple(5, &ctx->heap); term_put_tuple_element(down_message_tuple, 0, DOWN_ATOM); term_put_tuple_element(down_message_tuple, 1, ref); diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index f0c25cb41c..549e9720b1 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -55,6 +55,8 @@ start() -> ok = test_monitor_alias_demonitor_deactivates_on_down(fun spawn_monitor/2), ok = test_monitor_alias_demonitor_deactivates_on_down(fun spawn_and_monitor/2), ok = test_alias_pid_send_order(), + ok = test_monitor_alias_noproc_returns_alias(), + ok = test_monitor_alias_self_returns_alias(), 0. test_monitor_normal() -> @@ -366,6 +368,30 @@ test_alias_pid_send_order() -> {got, m2} = recv_one(), ok. +%% monitor(process, DeadPid, [{alias, explicit_unalias}]) must still return a usable alias (and an +%% immediate noproc DOWN), like OTP, not a plain reference. +test_monitor_alias_noproc_returns_alias() -> + {P, _} = spawn_opt(fun() -> ok end, [monitor]), + receive + {'DOWN', _, _, P, _} -> ok + end, + Mon = erlang:monitor(process, P, [{alias, explicit_unalias}]), + {'DOWN', Mon, process, P, noproc} = recv_one(), + Echo = spawn_opt(fun echo_loop/0, []), + Echo ! {via_alias, Mon}, + via_alias = recv_one(), + true = erlang:unalias(Mon), + Echo ! quit, + ok. + +%% monitor(process, self(), [{alias, explicit_unalias}]) must return a usable alias, like OTP. +test_monitor_alias_self_returns_alias() -> + Mon = erlang:monitor(process, self(), [{alias, explicit_unalias}]), + Mon ! hello, + hello = recv_one(), + true = erlang:unalias(Mon), + ok. + test_monitor_multiple_aliases_monitors(SpawnFun) -> {P, Mon1} = SpawnFun(fun echo_loop/0, [{alias, demonitor}]), Mon2 = erlang:monitor(process, P, [{alias, reply_demonitor}]), From a7d314ef1ff6a8f6960567e1bde847d96e606949 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 3 Jun 2026 10:07:11 +0000 Subject: [PATCH 12/87] Document that sends to non-alias references are dropped A send to a reference that is not an active local process-reference alias (a plain/short ref, a resource ref, or an external ref) is silently dropped, which matches OTP for local references. Distributed aliases (external references) are not supported, so a message addressed to a remote alias is lost rather than routed over distribution. Add comments at the three send sites (erlang:send/2, the send opcode and the JIT send); no behaviour change. Signed-off-by: Davide Bettio --- src/libAtomVM/jit.c | 3 +++ src/libAtomVM/nifs.c | 4 ++++ src/libAtomVM/opcodesswitch.h | 3 +++ 3 files changed, 10 insertions(+) diff --git a/src/libAtomVM/jit.c b/src/libAtomVM/jit.c index 882fc716cb..54abbd716b 100644 --- a/src/libAtomVM/jit.c +++ b/src/libAtomVM/jit.c @@ -867,6 +867,9 @@ static bool jit_send(Context *ctx, JITState *jit_state) set_error(ctx, jit_state, 0, BADARG_ATOM); return false; } + // else: a reference that is not a local process reference (short/resource/external ref) is silently + // dropped, as OTP drops a send to a non-active-alias reference. Distributed aliases (external + // references) are unsupported, so they are lost. return true; } diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 7cc541918d..5516e066ae 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1850,6 +1850,10 @@ static term nif_erlang_send_2(Context *ctx, int argc, term argv[]) } else if (!term_is_reference(target)) { RAISE_ERROR(BADARG_ATOM); } + // else: target is a reference but not a local process reference (a plain/short ref, a resource ref, + // or an external reference). It is silently dropped, as OTP drops a send to a reference that is not + // an active local alias. Distributed aliases (external references) are not supported, so a message + // addressed to a remote alias is lost rather than routed over distribution. return argv[1]; } diff --git a/src/libAtomVM/opcodesswitch.h b/src/libAtomVM/opcodesswitch.h index 0293286313..3f52293588 100644 --- a/src/libAtomVM/opcodesswitch.h +++ b/src/libAtomVM/opcodesswitch.h @@ -2386,6 +2386,9 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) } else if (!term_is_reference(recipient_term)) { RAISE_ERROR(BADARG_ATOM); } + // else: a reference that is not a local process reference (short/resource/external + // ref) is silently dropped, as OTP drops a send to a non-active-alias reference. + // Distributed aliases (external references) are unsupported, so they are lost. x_regs[0] = x_regs[1]; } break; From d1f71ab133df4daf696f70994d5a28515abcbd67 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 3 Jun 2026 10:08:26 +0000 Subject: [PATCH 13/87] Initialize the trailing word of a 32-bit resource reference On 32-bit, TERM_BOXED_REFERENCE_RESOURCE_SIZE is 5 (one more than header + refc + mso cons) so reference shapes stay size-distinguishable, but term_from_resource only wrote 4 words, leaving the trailing padding word uninitialized. It is not read by comparison or serialization, but it is copied during GC, which is an uninitialized-memory read (sanitizer / valgrind). Initialize it to nil. No effect on 64-bit, where the resource size is exactly 4 words. Signed-off-by: Davide Bettio --- src/libAtomVM/term.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/libAtomVM/term.h b/src/libAtomVM/term.h index b68a0bb57b..6799c87658 100644 --- a/src/libAtomVM/term.h +++ b/src/libAtomVM/term.h @@ -3083,6 +3083,12 @@ static inline term term_from_resource(void *resource, Heap *heap) term *boxed_value = memory_heap_alloc(heap, TERM_BOXED_REFERENCE_RESOURCE_SIZE); boxed_value[0] = TERM_BOXED_REFERENCE_RESOURCE_HEADER; boxed_value[1] = (term) refc; +#if TERM_BOXED_REFERENCE_RESOURCE_SIZE > (REFERENCE_RESOURCE_CONS_OFFSET + CONS_SIZE) + // On 32-bit the resource reference is one word larger than header + refc + mso cons (so reference + // shapes stay size-distinguishable); initialize the trailing padding word so GC copies a defined + // value instead of uninitialized memory. + boxed_value[REFERENCE_RESOURCE_CONS_OFFSET + CONS_SIZE] = term_nil(); +#endif // Add the resource to the mso list refc_binary_add_refcount(refc, 1); term ret = ((term) boxed_value) | TERM_PRIMARY_BOXED; From 94e60fe3ff13f0fa72fef77a494038b7400ec9cb Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 3 Jun 2026 10:09:13 +0000 Subject: [PATCH 14/87] Add missing break to the CONTEXT_MONITOR_RESOURCE case in context_demonitor The CONTEXT_MONITOR_RESOURCE case fell through to the following monitor cases when the ref_ticks did not match. It is benign today (the next cases only break), but the alias commit added CONTEXT_MONITOR_ALIAS to the same switch, so tighten the fall-through. No behaviour change. Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index 5183d5b6b9..b686a1159d 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -1182,6 +1182,7 @@ void context_demonitor(Context *ctx, uint64_t ref_ticks) free(resource_monitor); return; } + break; } case CONTEXT_MONITOR_LINK_LOCAL: case CONTEXT_MONITOR_LINK_REMOTE: From e58781e99b8c175d034cf317b4230f7c30a9aa7b Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Thu, 4 Jun 2026 09:32:54 +0000 Subject: [PATCH 15/87] Remove never-scheduled processes from the scheduler queue on destroy globalcontext_init_process appends every new context to the scheduler waiting queue, but context_destroy never removed it. Destroying a process that was never scheduled -- as the spawn NIFs do on option errors, or port drivers on initialization failures -- left a dangling queue entry pointing into freed memory, corrupting the queue on the next scheduler operation. Pre-existing upstream; exposed by the spawn_opt atomicity test added in the next commit, the first to exercise such a path. Dequeue in context_destroy under the processes spinlock, and reset the queue item at the scheduler's own dequeue sites so the destroy-time dequeue is a no-op there. A concurrent re-queue cannot happen: scheduler_make_ready refuses Killed and Spawning processes under the same spinlock, and one of these flags is set on every destroy path. Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 8 ++++++++ src/libAtomVM/scheduler.c | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index b686a1159d..bbf1e30c52 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -139,6 +139,14 @@ void context_destroy(Context *ctx) // Hold and release the spin lock for timers and cancel any timer scheduler_cancel_timeout(ctx); + // Remove the process from the scheduler queue. A process terminated by the scheduler was + // already dequeued (and its queue item was reset, making this a no-op), but a process that + // is destroyed before it was ever scheduled -- e.g. on a spawn_opt option error -- is still + // on the waiting queue, and destroying it without dequeuing would leave a dangling entry. + SMP_SPINLOCK_LOCK(&ctx->global->processes_spinlock); + list_remove(&ctx->processes_list_head); + SMP_SPINLOCK_UNLOCK(&ctx->global->processes_spinlock); + // Another process can get an access to our mailbox until this point. struct ListHead *processes_table_list = synclist_wrlock(&ctx->global->processes_table); UNUSED(processes_table_list); diff --git a/src/libAtomVM/scheduler.c b/src/libAtomVM/scheduler.c index e4d03d9a0a..f7b3e800ec 100644 --- a/src/libAtomVM/scheduler.c +++ b/src/libAtomVM/scheduler.c @@ -290,6 +290,8 @@ Context *scheduler_run(GlobalContext *global) if (UNLIKELY(result->flags & Killed)) { SMP_SPINLOCK_LOCK(&global->processes_spinlock); list_remove(&result->processes_list_head); + // Reset the queue item so the dequeue in context_destroy is a no-op. + list_init(&result->processes_list_head); SMP_SPINLOCK_UNLOCK(&global->processes_spinlock); context_destroy(result); } else { @@ -430,6 +432,9 @@ void scheduler_terminate(Context *ctx) SMP_SPINLOCK_LOCK(&ctx->global->processes_spinlock); context_update_flags(ctx, ~NoFlags, Killed); list_remove(&ctx->processes_list_head); + // Reset the queue item so the dequeue in context_destroy is a no-op. For a leader process, + // context_destroy is called after the scheduler loop returns. + list_init(&ctx->processes_list_head); SMP_SPINLOCK_UNLOCK(&ctx->global->processes_spinlock); if (!ctx->leader) { context_destroy(ctx); From 3ad275ffbd5fa0c4cdce4b9b0f0a972c4eb3f8e6 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Thu, 4 Jun 2026 09:33:05 +0000 Subject: [PATCH 16/87] Install the spawn_opt link only after spawn can no longer fail do_spawn published the link before the monitor phase, which can still fail parsing the monitor options or on out-of-memory, including the result-space reservation. The error paths destroyed the new context but left the caller's link entry installed, so the destroyed context's teardown sent the caller a spurious {'EXIT', Pid, normal} -- delivered as a message when trapping exits -- and process_info(links) reported a link to a dead pid, for a spawn_opt call that raised. OTP's spawn_opt is atomic: either it returns and the link exists, or it raises without side effects. Run every fallible step -- option parsing, link and monitor allocations, and the result space reservation -- before publishing anything; publishing itself cannot fail. This closes the unwind residual left out of scope by the result-space commit. Add a regression test checking that spawn_opt(F, [link, {monitor, [Bad]}]) raises badarg without leaving a link behind or delivering an 'EXIT' message. Signed-off-by: Davide Bettio --- src/libAtomVM/nifs.c | 48 +++++++++++++++++++++-------- tests/erlang_tests/test_monitor.erl | 22 +++++++++++++ 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 5516e066ae..d8ee8f0a28 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1542,41 +1542,51 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free } RefData ref_data; bool is_spawn_monitor = false; + bool is_alias = false; + enum ContextMonitorAliasType alias_type; term new_pid = term_from_local_process_id(new_ctx->process_id); + // Run every fallible step (option parsing, allocations, result-space reservation) before + // publishing any side effect, so a late failure cannot leave the caller with a + // half-installed link or monitor: destroying the never-published new_ctx would otherwise + // send the caller a spurious {'EXIT', Pid, normal} for a spawn that raised. + struct Monitor *new_link = NULL; + struct Monitor *self_link = NULL; + struct Monitor *alias_monitor = NULL; + struct Monitor *new_monitor = NULL; + struct Monitor *self_monitor = NULL; + if (link_term == TRUE_ATOM) { - // We can call context_add_monitor directly on new process because it's not started yet - struct Monitor *new_link = monitor_link_new(term_from_local_process_id(ctx->process_id)); + new_link = monitor_link_new(term_from_local_process_id(ctx->process_id)); if (IS_NULL_PTR(new_link)) { context_destroy(new_ctx); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - struct Monitor *self_link = monitor_link_new(new_pid); + self_link = monitor_link_new(new_pid); if (IS_NULL_PTR(self_link)) { free(new_link); context_destroy(new_ctx); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - context_add_monitor(new_ctx, new_link); - context_add_monitor(ctx, self_link); } if (monitor_term == TRUE_ATOM) { monitor_term = term_nil(); } if (term_is_list(monitor_term)) { is_spawn_monitor = true; - bool is_alias; - enum ContextMonitorAliasType alias_type; if (UNLIKELY(term_is_invalid_term(parse_monitor_opts(ctx, monitor_term, &is_alias, &alias_type)))) { + free(new_link); + free(self_link); context_destroy(new_ctx); return term_invalid_term(); } - struct Monitor *alias_monitor = NULL; if (is_alias) { ref_data = (RefData){ .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = ctx->process_id }; alias_monitor = monitor_alias_new(&ref_data, alias_type); if (IS_NULL_PTR(alias_monitor)) { + free(new_link); + free(self_link); context_destroy(new_ctx); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } @@ -1584,14 +1594,18 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free ref_data = (RefData){ .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = INVALID_PROCESS_ID }; } - struct Monitor *new_monitor = monitor_new(term_from_local_process_id(ctx->process_id), &ref_data, false); + new_monitor = monitor_new(term_from_local_process_id(ctx->process_id), &ref_data, false); if (IS_NULL_PTR(new_monitor)) { - context_destroy(new_ctx); + free(new_link); + free(self_link); free(alias_monitor); + context_destroy(new_ctx); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - struct Monitor *self_monitor = monitor_new(new_pid, &ref_data, true); + self_monitor = monitor_new(new_pid, &ref_data, true); if (IS_NULL_PTR(self_monitor)) { + free(new_link); + free(self_link); free(alias_monitor); free(new_monitor); context_destroy(new_ctx); @@ -1603,14 +1617,24 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free // receives {Pid, Ref}. GC here is safe: new_pid and ref_data are immediates. int res_size = TERM_BOXED_REFERENCE_PROCESS_SIZE + TUPLE_SIZE(2); if (UNLIKELY(memory_ensure_free_opt(ctx, res_size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + free(new_link); + free(self_link); free(alias_monitor); free(self_monitor); free(new_monitor); context_destroy(new_ctx); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } + } - // We can call context_add_monitor directly on new process because it's not started yet + // Nothing can fail from here on: publish the link and the monitors, in this order so the + // entries keep their relative position in the monitor lists. + // We can call context_add_monitor directly on new process because it's not started yet + if (new_link != NULL) { + context_add_monitor(new_ctx, new_link); + context_add_monitor(ctx, self_link); + } + if (is_spawn_monitor) { context_add_monitor(new_ctx, new_monitor); context_add_monitor(ctx, self_monitor); if (is_alias) { diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 549e9720b1..8125036b70 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -57,6 +57,7 @@ start() -> ok = test_alias_pid_send_order(), ok = test_monitor_alias_noproc_returns_alias(), ok = test_monitor_alias_self_returns_alias(), + ok = test_spawn_opt_link_monitor_badarg_is_atomic(), 0. test_monitor_normal() -> @@ -392,6 +393,27 @@ test_monitor_alias_self_returns_alias() -> true = erlang:unalias(Mon), ok. +%% spawn_opt(F, [link, {monitor, [Bad]}]) must raise badarg atomically, like OTP: no process is +%% spawned and no link is left behind. The link is installed before the monitor options are +%% parsed, so it must not survive the error -- or the caller would keep a link to a destroyed +%% pid and receive a spurious {'EXIT', Pid, normal} for a spawn that raised. +test_spawn_opt_link_monitor_badarg_is_atomic() -> + false = erlang:process_flag(trap_exit, true), + ok = + try spawn_opt(fun() -> ok end, [link, {monitor, [bad_option]}]) of + Result -> {unexpected, Result} + catch + error:badarg -> ok + end, + {links, []} = erlang:process_info(self(), links), + ok = + receive + Other -> {unexpected_message, Other} + after 200 -> ok + end, + true = erlang:process_flag(trap_exit, false), + ok. + test_monitor_multiple_aliases_monitors(SpawnFun) -> {P, Mon1} = SpawnFun(fun echo_loop/0, [{alias, demonitor}]), Mon2 = erlang:monitor(process, P, [{alias, reply_demonitor}]), From b5883e415e6b46ea97c52b911cdb62bbd632131a Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Thu, 4 Jun 2026 10:03:59 +0000 Subject: [PATCH 17/87] Install nothing when monitoring self, like OTP The BEAM conformance CI run (test-erlang -b) showed that on OTP, monitor(process, self(), [{alias, _}]) is a no-op apart from returning a fresh reference: no monitor is installed and no alias either -- sends to the returned reference are dropped, and unalias/1 and demonitor(Ref, [info]) return false (verified on OTP 29). This reverts the self half of "Return a real alias from monitor/3 on the self and noproc paths", whose premise was wrong for self; the noproc half is conformant (its test passes on the BEAM). Return a plain short reference and install nothing, matching the existing monitor/2 self path, and update the test to assert the OTP behaviour. Signed-off-by: Davide Bettio --- src/libAtomVM/nifs.c | 21 +++++---------------- tests/erlang_tests/test_monitor.erl | 13 ++++++++----- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index d8ee8f0a28..120f5dbe28 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -5066,25 +5066,14 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) target = NULL; } else { local_process_id = term_to_local_process_id(target_pid); - // Monitoring self is possible but no monitor is actually created (self never sends a DOWN). - // With {alias, _} an alias is still installed so the returned reference is a usable alias. + // Monitoring self installs nothing, like OTP: no monitor (self never sends a DOWN) and, + // with {alias, _}, no alias either -- sends to the returned reference are dropped, + // unalias/1 and demonitor(Ref, [info]) return false. if (UNLIKELY(local_process_id == ctx->process_id)) { - RefData ref_data = { .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = is_alias ? ctx->process_id : INVALID_PROCESS_ID }; - struct Monitor *alias_monitor = NULL; - if (is_alias) { - alias_monitor = monitor_alias_new(&ref_data, alias_type); - if (IS_NULL_PTR(alias_monitor)) { - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } - } - size_t self_ref_size = is_alias ? TERM_BOXED_REFERENCE_PROCESS_SIZE : TERM_BOXED_REFERENCE_SHORT_SIZE; - if (UNLIKELY(memory_ensure_free_opt(ctx, self_ref_size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { - free(alias_monitor); + RefData ref_data = { .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = INVALID_PROCESS_ID }; + if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_SHORT_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - if (is_alias) { - context_add_monitor(ctx, alias_monitor); - } return term_from_ref_data(&ref_data, &ctx->heap); } diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 8125036b70..1e5519d642 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -56,7 +56,7 @@ start() -> ok = test_monitor_alias_demonitor_deactivates_on_down(fun spawn_and_monitor/2), ok = test_alias_pid_send_order(), ok = test_monitor_alias_noproc_returns_alias(), - ok = test_monitor_alias_self_returns_alias(), + ok = test_monitor_alias_self_installs_nothing(), ok = test_spawn_opt_link_monitor_badarg_is_atomic(), 0. @@ -385,12 +385,15 @@ test_monitor_alias_noproc_returns_alias() -> Echo ! quit, ok. -%% monitor(process, self(), [{alias, explicit_unalias}]) must return a usable alias, like OTP. -test_monitor_alias_self_returns_alias() -> +%% monitor(process, self(), [{alias, _}]) installs nothing, like OTP: no monitor and no active +%% alias -- sends to the returned reference are dropped, unalias/1 and demonitor(Ref, [info]) +%% return false. (Verified against OTP 29.) +test_monitor_alias_self_installs_nothing() -> Mon = erlang:monitor(process, self(), [{alias, explicit_unalias}]), Mon ! hello, - hello = recv_one(), - true = erlang:unalias(Mon), + timeout = recv_one(), + false = erlang:unalias(Mon), + false = erlang:demonitor(Mon, [info]), ok. %% spawn_opt(F, [link, {monitor, [Bad]}]) must raise badarg atomically, like OTP: no process is From 8f19764f7a4b4ec5fb3f72bc9a1faf72fda6231f Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Thu, 4 Jun 2026 10:04:20 +0000 Subject: [PATCH 18/87] Compare links against the initial set in the spawn_opt atomicity test On the BEAM the test process is the erl_eval process, which is linked to init, so the {links, []} assertion failed when running the tests with test-erlang -b. Snapshot the links before the spawn_opt call and assert they are unchanged after; this still catches a leaked link on AtomVM and is independent of the harness process's pre-existing links. Signed-off-by: Davide Bettio --- tests/erlang_tests/test_monitor.erl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 1e5519d642..557627a9e9 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -401,6 +401,9 @@ test_monitor_alias_self_installs_nothing() -> %% parsed, so it must not survive the error -- or the caller would keep a link to a destroyed %% pid and receive a spurious {'EXIT', Pid, normal} for a spawn that raised. test_spawn_opt_link_monitor_badarg_is_atomic() -> + %% On the BEAM the test process is linked to init, so compare against the initial links + %% instead of []. + {links, LinksBefore} = erlang:process_info(self(), links), false = erlang:process_flag(trap_exit, true), ok = try spawn_opt(fun() -> ok end, [link, {monitor, [bad_option]}]) of @@ -408,7 +411,7 @@ test_spawn_opt_link_monitor_badarg_is_atomic() -> catch error:badarg -> ok end, - {links, []} = erlang:process_info(self(), links), + {links, LinksBefore} = erlang:process_info(self(), links), ok = receive Other -> {unexpected_message, Other} From b1df67ae372eecc822b1be363f15deacf3313051 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Thu, 4 Jun 2026 10:46:43 +0000 Subject: [PATCH 19/87] Free never-published monitors through monitor_destroy The monitor_*_new constructors return a pointer to the struct Monitor embedded in their container struct, and the nifs.c error paths free()d that pointer directly. That is only correct because the monitor happens to be the first member of every container struct; recover the container with CONTAINER_OF instead, through a new monitor_destroy that switches on the monitor type, so a layout change cannot silently corrupt the heap. No functional change. Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 28 +++++++++++++++++++++++ src/libAtomVM/context.h | 12 ++++++++++ src/libAtomVM/nifs.c | 50 ++++++++++++++++++++--------------------- 3 files changed, 65 insertions(+), 25 deletions(-) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index bbf1e30c52..1ae08ebc61 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -975,6 +975,34 @@ struct Monitor *monitor_resource_monitor_new(void *resource, uint64_t ref_ticks) return &monitor->monitor; } +void monitor_destroy(struct Monitor *monitor) +{ + if (monitor == NULL) { + return; + } + switch (monitor->monitor_type) { + case CONTEXT_MONITOR_LINK_LOCAL: + free(CONTAINER_OF(monitor, struct LinkLocalMonitor, monitor)); + break; + case CONTEXT_MONITOR_LINK_REMOTE: + free(CONTAINER_OF(monitor, struct LinkRemoteMonitor, monitor)); + break; + case CONTEXT_MONITOR_MONITORING_LOCAL: + case CONTEXT_MONITOR_MONITORED_LOCAL: + free(CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor)); + break; + case CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME: + free(CONTAINER_OF(monitor, struct MonitorLocalRegisteredNameMonitor, monitor)); + break; + case CONTEXT_MONITOR_ALIAS: + free(CONTAINER_OF(monitor, struct MonitorAlias, monitor)); + break; + case CONTEXT_MONITOR_RESOURCE: + free(CONTAINER_OF(monitor, struct ResourceContextMonitor, monitor)); + break; + } +} + bool context_add_monitor(Context *ctx, struct Monitor *new_monitor) { struct ListHead *item; diff --git a/src/libAtomVM/context.h b/src/libAtomVM/context.h index db36c17d98..648ff15c0e 100644 --- a/src/libAtomVM/context.h +++ b/src/libAtomVM/context.h @@ -568,6 +568,18 @@ struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, t */ struct Monitor *monitor_resource_monitor_new(void *resource, uint64_t ref_ticks); +/** + * @brief Destroy a monitor that was not installed yet. + * @details Frees the container struct the monitor is embedded in, recovering + * it from its monitor type with CONTAINER_OF instead of relying on the monitor + * being the first member. It doesn't remove the monitor from any list, so it + * must only be used on monitors that were never passed to context_add_monitor, + * e.g. on error paths. + * + * @param monitor the monitor to free, or NULL in which case nothing is done + */ +void monitor_destroy(struct Monitor *monitor); + /** * @brief Half-unlink process to another process * @details If process is found, an unlink id is generated and the link is diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 120f5dbe28..9919ba8b66 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1564,7 +1564,7 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free } self_link = monitor_link_new(new_pid); if (IS_NULL_PTR(self_link)) { - free(new_link); + monitor_destroy(new_link); context_destroy(new_ctx); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } @@ -1576,8 +1576,8 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free is_spawn_monitor = true; if (UNLIKELY(term_is_invalid_term(parse_monitor_opts(ctx, monitor_term, &is_alias, &alias_type)))) { - free(new_link); - free(self_link); + monitor_destroy(new_link); + monitor_destroy(self_link); context_destroy(new_ctx); return term_invalid_term(); } @@ -1585,8 +1585,8 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free ref_data = (RefData){ .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = ctx->process_id }; alias_monitor = monitor_alias_new(&ref_data, alias_type); if (IS_NULL_PTR(alias_monitor)) { - free(new_link); - free(self_link); + monitor_destroy(new_link); + monitor_destroy(self_link); context_destroy(new_ctx); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } @@ -1596,18 +1596,18 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free new_monitor = monitor_new(term_from_local_process_id(ctx->process_id), &ref_data, false); if (IS_NULL_PTR(new_monitor)) { - free(new_link); - free(self_link); - free(alias_monitor); + monitor_destroy(new_link); + monitor_destroy(self_link); + monitor_destroy(alias_monitor); context_destroy(new_ctx); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } self_monitor = monitor_new(new_pid, &ref_data, true); if (IS_NULL_PTR(self_monitor)) { - free(new_link); - free(self_link); - free(alias_monitor); - free(new_monitor); + monitor_destroy(new_link); + monitor_destroy(self_link); + monitor_destroy(alias_monitor); + monitor_destroy(new_monitor); context_destroy(new_ctx); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } @@ -1617,11 +1617,11 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free // receives {Pid, Ref}. GC here is safe: new_pid and ref_data are immediates. int res_size = TERM_BOXED_REFERENCE_PROCESS_SIZE + TUPLE_SIZE(2); if (UNLIKELY(memory_ensure_free_opt(ctx, res_size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { - free(new_link); - free(self_link); - free(alias_monitor); - free(self_monitor); - free(new_monitor); + monitor_destroy(new_link); + monitor_destroy(self_link); + monitor_destroy(alias_monitor); + monitor_destroy(self_monitor); + monitor_destroy(new_monitor); context_destroy(new_ctx); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } @@ -5093,7 +5093,7 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) } } if (UNLIKELY(memory_ensure_free_opt(ctx, res_size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { - free(alias_monitor); + monitor_destroy(alias_monitor); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } if (alias_monitor != NULL) { @@ -5142,14 +5142,14 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) } if (IS_NULL_PTR(self_monitor)) { globalcontext_get_process_unlock(ctx->global, target); - free(alias_monitor); + monitor_destroy(alias_monitor); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } term monitoring_pid = term_from_local_process_id(ctx->process_id); struct Monitor *other_monitor = monitor_new(monitoring_pid, &ref_data, false); if (IS_NULL_PTR(other_monitor)) { - free(alias_monitor); - free(self_monitor); + monitor_destroy(alias_monitor); + monitor_destroy(self_monitor); globalcontext_get_process_unlock(ctx->global, target); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } @@ -5159,9 +5159,9 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) // while the caller gets an exception and never receives the reference. GC here is safe: the // monitor structs hold only an immediate pid and a plain RefData, none reachable from the heap. if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_PROCESS_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { - free(alias_monitor); - free(self_monitor); - free(other_monitor); + monitor_destroy(alias_monitor); + monitor_destroy(self_monitor); + monitor_destroy(other_monitor); globalcontext_get_process_unlock(ctx->global, target); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } @@ -5250,7 +5250,7 @@ static term nif_erlang_link(Context *ctx, int argc, term argv[]) if (UNLIKELY(!context_add_monitor(ctx, self_link))) { globalcontext_get_process_unlock(ctx->global, target); - free(other_link); + monitor_destroy(other_link); return TRUE_ATOM; } From 9fb87a982583943c1531520ad4dd0be482f996f4 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Thu, 4 Jun 2026 10:48:31 +0000 Subject: [PATCH 20/87] Const-qualify RefData parameters and document the reference helpers The RefData arguments of the monitor constructors and term_from_ref_data are only read, so take them as const pointers per the coding style. Document monitor_alias_new, term_from_ref_data and term_process_ref_to_process_id like their neighbours, and use the until-now unused REFERENCE_PROCESS_PID_OFFSET for the process id word of a process reference, which also drops a TERM_BYTES conditional. No functional change. Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 6 +++--- src/libAtomVM/context.h | 13 ++++++++++--- src/libAtomVM/term.h | 28 ++++++++++++++++++---------- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index 1ae08ebc61..0193790912 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -917,7 +917,7 @@ struct Monitor *monitor_link_new(term link_pid) } } -struct Monitor *monitor_new(term monitor_pid, RefData *ref_data, bool is_monitoring) +struct Monitor *monitor_new(term monitor_pid, const RefData *ref_data, bool is_monitoring) { struct MonitorLocalMonitor *monitor = malloc(sizeof(struct MonitorLocalMonitor)); if (IS_NULL_PTR(monitor)) { @@ -934,7 +934,7 @@ struct Monitor *monitor_new(term monitor_pid, RefData *ref_data, bool is_monitor return &monitor->monitor; } -struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, RefData *ref_data) +struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, const RefData *ref_data) { struct MonitorLocalRegisteredNameMonitor *monitor = malloc(sizeof(struct MonitorLocalRegisteredNameMonitor)); if (IS_NULL_PTR(monitor)) { @@ -948,7 +948,7 @@ struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, t return &monitor->monitor; } -struct Monitor *monitor_alias_new(RefData *ref_data, enum ContextMonitorAliasType alias_type) +struct Monitor *monitor_alias_new(const RefData *ref_data, enum ContextMonitorAliasType alias_type) { struct MonitorAlias *monitor = malloc(sizeof(struct MonitorAlias)); if (IS_NULL_PTR(monitor)) { diff --git a/src/libAtomVM/context.h b/src/libAtomVM/context.h index 648ff15c0e..c37fe302dd 100644 --- a/src/libAtomVM/context.h +++ b/src/libAtomVM/context.h @@ -545,9 +545,16 @@ struct Monitor *monitor_link_new(term link_pid); * @param is_monitoring if ctx is the monitoring process * @return the allocated monitor or NULL if allocation failed */ -struct Monitor *monitor_new(term monitor_pid, RefData *ref_data, bool is_monitoring); +struct Monitor *monitor_new(term monitor_pid, const RefData *ref_data, bool is_monitoring); -struct Monitor *monitor_alias_new(RefData *ref_data, enum ContextMonitorAliasType alias_type); +/** + * @brief Create a process alias. + * + * @param ref_data reference of the alias + * @param alias_type when the alias is deactivated, see the erlang:monitor/3 alias option + * @return the allocated monitor or NULL if allocation failed + */ +struct Monitor *monitor_alias_new(const RefData *ref_data, enum ContextMonitorAliasType alias_type); /** * @brief Create a monitor on a process by registered name. @@ -557,7 +564,7 @@ struct Monitor *monitor_alias_new(RefData *ref_data, enum ContextMonitorAliasTyp * @param ref_data reference of the monitor * @return the allocated monitor or NULL if allocation failed */ -struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, RefData *ref_data); +struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, const RefData *ref_data); /** * @brief Create a resource monitor. diff --git a/src/libAtomVM/term.h b/src/libAtomVM/term.h index 6799c87658..98ec0339a6 100644 --- a/src/libAtomVM/term.h +++ b/src/libAtomVM/term.h @@ -2283,30 +2283,29 @@ static inline term term_make_process_reference(int32_t process_id, uint64_t ref_ #if TERM_BYTES == 4 boxed_value[1] = (ref_ticks >> 32); boxed_value[2] = (ref_ticks & 0xFFFFFFFF); - boxed_value[3] = process_id; #elif TERM_BYTES == 8 boxed_value[1] = (term) ref_ticks; - boxed_value[2] = process_id; #else #error "terms must be either 32 or 64 bit wide" #endif + boxed_value[REFERENCE_PROCESS_PID_OFFSET] = process_id; return ((term) boxed_value) | TERM_PRIMARY_BOXED; } +/** + * @brief Get the process id out of a process reference + * + * @param rt the process reference term + * @return the process id of the process the reference identifies + */ static inline uint32_t term_process_ref_to_process_id(term rt) { TERM_DEBUG_ASSERT(term_is_process_reference(rt)); const term *boxed_value = term_to_const_term_ptr(rt); -#if TERM_BYTES == 4 - return (uint32_t) boxed_value[3]; -#elif TERM_BYTES == 8 - return (uint32_t) boxed_value[2]; -#else -#error "terms must be either 32 or 64 bit wide" -#endif + return (uint32_t) boxed_value[REFERENCE_PROCESS_PID_OFFSET]; } /** @@ -3096,7 +3095,16 @@ static inline term term_from_resource(void *resource, Heap *heap) return ret; } -static inline term term_from_ref_data(RefData *ref_data, Heap *heap) +/** + * @brief Create a reference term from a RefData + * @details Builds a short reference when process_id is INVALID_PROCESS_ID, + * and a process reference carrying the owner process id otherwise. + * + * @param ref_data ref ticks and owner process id of the reference + * @param heap the heap to allocate memory in + * @return a reference term created from the given ref data + */ +static inline term term_from_ref_data(const RefData *ref_data, Heap *heap) { if (ref_data->process_id == INVALID_PROCESS_ID) { return term_from_ref_ticks(ref_data->ref_ticks, heap); From 28d4ef05bd79a8d227e6327ba2339c8e4c976830 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Thu, 4 Jun 2026 10:49:53 +0000 Subject: [PATCH 21/87] Accept alias references in the erlang.erl send destination type erlang:send/2 accepts process alias references since the aliases were introduced, but send_destination() only allowed pids, ports and atoms. Add reference() like OTP does, document the alias destination (and the silent drop of non-alias references) on send/2, and mention the {monitor, MonitorOpts} option and the {Pid, MonitorRef} result in the spawn_opt docs. Signed-off-by: Davide Bettio --- libs/estdlib/src/erlang.erl | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/libs/estdlib/src/erlang.erl b/libs/estdlib/src/erlang.erl index 4fdb14225f..369e0cc47e 100644 --- a/libs/estdlib/src/erlang.erl +++ b/libs/estdlib/src/erlang.erl @@ -221,7 +221,8 @@ -type send_destination() :: pid() | port() - | atom(). + | atom() + | reference(). % Current type until we make these references -type resource() :: binary(). @@ -1228,8 +1229,10 @@ spawn_monitor(Module, Function, Args) -> %%----------------------------------------------------------------------------- %% @param Function function to create a process from -%% @param Options additional options. -%% @returns pid of the new process +%% @param Options additional options, see `spawn_option()'. With `monitor' +%% or `{monitor, MonitorOpts}' the new process is also monitored, see +%% `monitor/3' for the monitor options. +%% @returns pid of the new process, or `{Pid, MonitorRef}' when monitoring %% @doc Create a new process. %% @end %%----------------------------------------------------------------------------- @@ -1242,8 +1245,10 @@ spawn_opt(_Name, _Options) -> %% @param Module module of the function to create a process from %% @param Function name of the function to create a process from %% @param Args arguments to pass to the function to create a process from -%% @param Options additional options. -%% @returns pid of the new process +%% @param Options additional options, see `spawn_option()'. With `monitor' +%% or `{monitor, MonitorOpts}' the new process is also monitored, see +%% `monitor/3' for the monitor options. +%% @returns pid of the new process, or `{Pid, MonitorRef}' when monitoring %% @doc Create a new process by calling exported Function from Module with Args. %% @end %%----------------------------------------------------------------------------- @@ -1283,10 +1288,12 @@ make_ref() -> erlang:nif_error(undefined). %%----------------------------------------------------------------------------- -%% @param Pid process to send the message to +%% @param Target process, registered name or alias to send the message to %% @param Message message to send %% @returns the sent message -%% @doc Send a message to a given process +%% @doc Send a message to a given process. The target may also be an alias +%% reference created with `alias/0' or `monitor/3'; a message sent to +%% a reference that is not an active alias is silently dropped. %% @end %%----------------------------------------------------------------------------- -spec send(Target :: send_destination(), Message :: Message) -> Message. From 5d05ac69f4023eb2559493e06121f898f1bea8d0 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Thu, 4 Jun 2026 10:50:20 +0000 Subject: [PATCH 22/87] Add process aliases to the changelog Signed-off-by: Davide Bettio --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29b81ae4e4..be41f064ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `"USB_SERIAL_JTAG"` peripheral to the ESP32 `uart` module on chips with a built-in USB-Serial-JTAG controller (C3/C5/C6/C61/H2/H21/H4/P4/S3) - Added support for the `safe` option in `erlang:binary_to_term/2` +- Added support for process aliases (OTP ≥ 26 semantics): `erlang:alias/0`, `erlang:unalias/1`, + `erlang:monitor/3` with the `{alias, Mode}` option, `spawn_opt` `{monitor, MonitorOpts}` and + sending to an alias reference - Added xtensa JIT backend for esp32 platform - Added support for configuring pins and width for sdmmc on ESP32 - Added support for map comprehensions From 4e1ac2e0a3064f24f9bad28be0a5c631d0832af2 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Thu, 4 Jun 2026 12:44:11 +0000 Subject: [PATCH 23/87] Remove the unused RefType enum Nothing ever referenced it: reference flavors are distinguished by their boxed size (see the comment above TERM_BOXED_REFERENCE_SHORT_SIZE), so the enum was dead code. It also didn't follow the coding style, which wants enumerations typedef'd with a lower_snake_case _t name. Signed-off-by: Davide Bettio --- src/libAtomVM/term.h | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/libAtomVM/term.h b/src/libAtomVM/term.h index 98ec0339a6..d2c56e5e35 100644 --- a/src/libAtomVM/term.h +++ b/src/libAtomVM/term.h @@ -274,14 +274,6 @@ _Static_assert(TERM_BOXED_REFERENCE_PROCESS_SIZE <= TERM_BOXED_REFERENCE_MAX_SIZ typedef struct GlobalContext GlobalContext; #endif -enum RefType -{ - RefTypeShort, - RefTypeProcess, - RefTypeResource, - RefTypeExternal -}; - typedef struct RefData RefData; struct RefData { From 64914863d84d16171a360022f582495ba28376df Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Thu, 4 Jun 2026 12:44:43 +0000 Subject: [PATCH 24/87] Typedef the alias type enum as context_monitor_alias_type_t The coding style wants enumerations typedef'd with a lower_snake_case _t name (like run_result_t or ets_table_type_t), not a bare PascalCase enum tag. The values keep their ContextMonitorAlias prefix, which the style derives from the type name. No functional change. Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 2 +- src/libAtomVM/context.h | 8 ++++---- src/libAtomVM/nifs.c | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index 0193790912..d3319fc3c9 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -948,7 +948,7 @@ struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, t return &monitor->monitor; } -struct Monitor *monitor_alias_new(const RefData *ref_data, enum ContextMonitorAliasType alias_type) +struct Monitor *monitor_alias_new(const RefData *ref_data, context_monitor_alias_type_t alias_type) { struct MonitorAlias *monitor = malloc(sizeof(struct MonitorAlias)); if (IS_NULL_PTR(monitor)) { diff --git a/src/libAtomVM/context.h b/src/libAtomVM/context.h index c37fe302dd..cf36d599c0 100644 --- a/src/libAtomVM/context.h +++ b/src/libAtomVM/context.h @@ -180,12 +180,12 @@ enum ContextMonitorType CONTEXT_MONITOR_ALIAS, }; -enum ContextMonitorAliasType +typedef enum { ContextMonitorAliasExplicitUnalias, ContextMonitorAliasDemonitor, ContextMonitorAliasReplyDemonitor, -}; +} context_monitor_alias_type_t; #define UNLINK_ID_LINK_ACTIVE 0x0 @@ -224,7 +224,7 @@ struct MonitorAlias { struct Monitor monitor; RefData ref_data; - enum ContextMonitorAliasType alias_type; + context_monitor_alias_type_t alias_type; }; // The other half is called ResourceMonitor and is a linked list of resources @@ -554,7 +554,7 @@ struct Monitor *monitor_new(term monitor_pid, const RefData *ref_data, bool is_m * @param alias_type when the alias is deactivated, see the erlang:monitor/3 alias option * @return the allocated monitor or NULL if allocation failed */ -struct Monitor *monitor_alias_new(const RefData *ref_data, enum ContextMonitorAliasType alias_type); +struct Monitor *monitor_alias_new(const RefData *ref_data, context_monitor_alias_type_t alias_type); /** * @brief Create a monitor on a process by registered name. diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 9919ba8b66..ca3d94ae51 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1427,7 +1427,7 @@ static NativeHandlerResult process_console_mailbox(Context *ctx) return result; } -static term parse_monitor_opts(Context *ctx, term monitor_opts, bool *is_alias, enum ContextMonitorAliasType *alias_type) +static term parse_monitor_opts(Context *ctx, term monitor_opts, bool *is_alias, context_monitor_alias_type_t *alias_type) { *is_alias = false; while (term_is_nonempty_list(monitor_opts)) { @@ -1543,7 +1543,7 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free RefData ref_data; bool is_spawn_monitor = false; bool is_alias = false; - enum ContextMonitorAliasType alias_type; + context_monitor_alias_type_t alias_type; term new_pid = term_from_local_process_id(new_ctx->process_id); // Run every fallible step (option parsing, allocations, result-space reservation) before @@ -5039,7 +5039,7 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) term target_pid; size_t target_proc_size = 0; bool is_alias; - enum ContextMonitorAliasType alias_type; + context_monitor_alias_type_t alias_type; if (object_type != PROCESS_ATOM && object_type != PORT_ATOM) { RAISE_ERROR(BADARG_ATOM); From a4bdbc75d537f005d87583726f439046fbe28138 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Thu, 4 Jun 2026 12:45:00 +0000 Subject: [PATCH 25/87] Drop the module prefix from the static outer-list helper Static functions are file-local and per the coding style should not carry the module prefix; process_outer_list also no longer needs the _internal suffix to avoid colliding with the public mailbox_process_outer_list. No functional change. Signed-off-by: Davide Bettio --- src/libAtomVM/mailbox.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libAtomVM/mailbox.c b/src/libAtomVM/mailbox.c index 7f1bab67c6..dd95268cb6 100644 --- a/src/libAtomVM/mailbox.c +++ b/src/libAtomVM/mailbox.c @@ -366,7 +366,7 @@ void mailbox_reset(Mailbox *mbox) mbox->receive_pointer_prev = NULL; } -static MailboxMessage *mailbox_process_outer_list_internal(Context *ctx, Mailbox *mbox) +static MailboxMessage *process_outer_list(Context *ctx, Mailbox *mbox) { // Empty outer list using CAS MailboxMessage *current = mbox->outer_first; @@ -442,12 +442,12 @@ static MailboxMessage *mailbox_process_outer_list_internal(Context *ctx, Mailbox MailboxMessage *mailbox_process_outer_list(Mailbox *mbox) { - return mailbox_process_outer_list_internal(NULL, mbox); + return process_outer_list(NULL, mbox); } MailboxMessage *mailbox_process_outer_list_with_aliases(Context *ctx) { - return mailbox_process_outer_list_internal(ctx, &ctx->mailbox); + return process_outer_list(ctx, &ctx->mailbox); } void mailbox_next(Mailbox *mbox) From d9ea5915dc4db2329da6dcb93c065cbef3209a0d Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sat, 6 Jun 2026 11:26:19 +0000 Subject: [PATCH 26/87] Process outer-list alias messages in received order mailbox_process_outer_list_with_aliases drained the outer list (a LIFO stack, newest first) and ran each alias's side effects inline during that reverse walk. For a {alias, reply_demonitor} alias, delivering a message deactivates the alias and removes the monitor, so when two messages reached the same alias in one batch the newest was delivered and the older was dropped -- the opposite of OTP, which delivers the first message received via the alias and drops the rest. Reverse the detached outer list into received order first (pure pointer manipulation, no side effects and no allocation), then walk oldest-to-newest, appending to the normal and signal sublists. Alias side effects now run in received order, so the first message to a reply_demonitor alias wins. Plain message ordering and non-alias signal ordering are unchanged. mailbox_message_create_from_term does not initialize the message next pointer, so the converted alias message is explicitly terminated before being appended as the list tail; otherwise inner_last would keep a garbage next and the receiver would crash in mailbox_peek. Add test_reply_demonitor_same_batch_order, which self-sends two messages to its own reply_demonitor alias in a single outer-list batch and checks that the first is delivered and the second dropped. Signed-off-by: Davide Bettio --- src/libAtomVM/mailbox.c | 88 +++++++++++++++++++---------- tests/erlang_tests/test_monitor.erl | 15 +++++ 2 files changed, 74 insertions(+), 29 deletions(-) diff --git a/src/libAtomVM/mailbox.c b/src/libAtomVM/mailbox.c index dd95268cb6..d0459ac1e5 100644 --- a/src/libAtomVM/mailbox.c +++ b/src/libAtomVM/mailbox.c @@ -376,54 +376,84 @@ static MailboxMessage *process_outer_list(Context *ctx, Mailbox *mbox) #else mbox->outer_first = NULL; #endif - // Reverse the list - MailboxMessage *previous_normal = NULL; - MailboxMessage *previous_signal = NULL; - MailboxMessage *last_normal = NULL; + // The outer list is LIFO (outer_first is the newest message). Reverse it into received order + // (oldest first) before doing anything else: this is pure pointer manipulation, with no side + // effects and no allocation. Alias side effects (which can deactivate the alias, e.g. + // reply_demonitor) must run in received order, so that when several messages target the same + // alias in one batch, the first received one is delivered and the later ones dropped, like OTP. + MailboxMessage *received = NULL; + while (current) { + MailboxMessage *next = current->next; + current->next = received; + received = current; + current = next; + } + + // Walk received order (oldest to newest), appending to the normal and signal sublists so both + // keep received order. + MailboxMessage *normal_first = NULL; + MailboxMessage *normal_last = NULL; + MailboxMessage *signal_first = NULL; + MailboxMessage *signal_last = NULL; + current = received; while (current) { MailboxMessage *next = current->next; if (current->type == NormalMessage) { - // Get last normal to update inner_last. - if (last_normal == NULL) { - last_normal = current; + current->next = NULL; + if (normal_last == NULL) { + normal_first = current; + } else { + normal_last->next = current; } - current->next = previous_normal; - previous_normal = current; + normal_last = current; } else if (ctx != NULL && current->type == AliasMessageSignal) { - // Convert an alias message to a normal message in place, validating the alias in the - // owner's own context. Prepending it to the normal stream during this reverse keeps it - // ordered with plain sends from the same sender (alias and pid sends must not be reordered). + // Convert an alias message to a normal message, validating the alias in the owner's own + // context. Walking in received order keeps an alias send ordered with plain sends from + // the same sender (alias and pid sends must not be reordered), and resolves repeated + // sends to the same alias oldest-first. term message = context_process_alias_message_signal(ctx, CONTAINER_OF(current, struct TermSignal, base)); if (!term_is_invalid_term(message)) { MailboxMessage *converted = mailbox_message_create_from_term(NormalMessage, message); + // Best effort: on out of memory the message is dropped. For reply_demonitor the side + // effects (alias deactivation, DemonitorSignal) have already happened inside + // context_process_alias_message_signal, so this drop is not transactional. if (converted != NULL) { - if (last_normal == NULL) { - last_normal = converted; + // mailbox_message_create_from_term does not initialize next. + converted->next = NULL; + if (normal_last == NULL) { + normal_first = converted; + } else { + normal_last->next = converted; } - converted->next = previous_normal; - previous_normal = converted; + normal_last = converted; } } mailbox_message_dispose(current, &ctx->heap); } else { - current->next = previous_signal; - previous_signal = current; + current->next = NULL; + if (signal_last == NULL) { + signal_first = current; + } else { + signal_last->next = current; + } + signal_last = current; } current = next; } - // If we did enqueue some normal messages, lastNormal is the first - // one in outer list (last received one) - if (last_normal) { - // previousNormal is new list head - // If we had no receive_pointer, it should be this list head + + // If we enqueued some normal messages, normal_first is the head (oldest received) and + // normal_last is the tail (newest received). Splice them at the end of the inner list. + if (normal_last) { + // normal_first is the new list head. + // If we had no receive_pointer, it should be this list head. if (mbox->receive_pointer == NULL) { - mbox->receive_pointer = previous_normal; + mbox->receive_pointer = normal_first; // If we had a prev, set the prev's next to the new current. if (mbox->receive_pointer_prev) { - mbox->receive_pointer_prev->next = previous_normal; + mbox->receive_pointer_prev->next = normal_first; } else if (mbox->inner_first == NULL) { // If we had no first, this is the first message. - mbox->inner_first = previous_normal; + mbox->inner_first = normal_first; } } @@ -432,12 +462,12 @@ static MailboxMessage *process_outer_list(Context *ctx, Mailbox *mbox) if (mbox->inner_last) { // This may be mbox->receive_pointer_prev which we // are updating a second time here. - mbox->inner_last->next = previous_normal; + mbox->inner_last->next = normal_first; } - mbox->inner_last = last_normal; + mbox->inner_last = normal_last; } - return previous_signal; + return signal_first; } MailboxMessage *mailbox_process_outer_list(Mailbox *mbox) diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 557627a9e9..09d9b9637a 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -55,6 +55,7 @@ start() -> ok = test_monitor_alias_demonitor_deactivates_on_down(fun spawn_monitor/2), ok = test_monitor_alias_demonitor_deactivates_on_down(fun spawn_and_monitor/2), ok = test_alias_pid_send_order(), + ok = test_reply_demonitor_same_batch_order(), ok = test_monitor_alias_noproc_returns_alias(), ok = test_monitor_alias_self_installs_nothing(), ok = test_spawn_opt_link_monitor_badarg_is_atomic(), @@ -327,6 +328,20 @@ test_reply_demonitor_removes_monitor(SpawnFun) -> timeout = recv_one(), ok. +%% reply_demonitor must resolve two sends to the same alias in one batch in RECEIVED order: +%% deliver the FIRST and drop the SECOND (the first delivery deactivates the alias), like OTP. +%% Self-sending to our own reply_demonitor alias and only then receiving guarantees both alias +%% signals are drained in a single outer-list batch, so this is deterministic on SMP and non-SMP. +test_reply_demonitor_same_batch_order() -> + P = spawn_opt(fun echo_loop/0, []), + Mon = erlang:monitor(process, P, [{alias, reply_demonitor}]), + Mon ! first, + Mon ! second, + first = recv_one(), + timeout = recv_one(), + P ! quit, + ok. + test_monitor_down_alias(SpawnFun) -> {P, Mon} = SpawnFun(fun echo_loop/0, [{alias, demonitor}]), erlang:unalias(Mon), From 0218ade2ce6dc90f6903799f261cbca95f640298 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sat, 6 Jun 2026 12:19:08 +0000 Subject: [PATCH 27/87] Build the self monitor reference with term_from_ref_ticks Monitoring self installs no alias, so the returned reference is always a short ref. The path reserved TERM_BOXED_REFERENCE_SHORT_SIZE but built the term with term_from_ref_data, whose allocation size depends on the embedded pid. At runtime this is safe (process_id is INVALID_PROCESS_ID, so term_from_ref_data returns a short ref of exactly that size), but the static allocation check cannot prove the pid stays invalid and sizes it as a process ref, reporting a 3-vs-2 term over-allocation. Call term_from_ref_ticks directly: it allocates exactly TERM_BOXED_REFERENCE_SHORT_SIZE, matching the reservation, and is identical to what term_from_ref_data does for an invalid pid. Signed-off-by: Davide Bettio --- src/libAtomVM/nifs.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index ca3d94ae51..b24049319c 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -5070,11 +5070,14 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) // with {alias, _}, no alias either -- sends to the returned reference are dropped, // unalias/1 and demonitor(Ref, [info]) return false. if (UNLIKELY(local_process_id == ctx->process_id)) { - RefData ref_data = { .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = INVALID_PROCESS_ID }; if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_SHORT_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - return term_from_ref_data(&ref_data, &ctx->heap); + // No alias is installed, so this is always a short ref. term_from_ref_ticks allocates + // exactly TERM_BOXED_REFERENCE_SHORT_SIZE, matching the reservation; term_from_ref_data + // sizes by pid, which the allocation checker can't prove stays short here. + uint64_t ref_ticks = globalcontext_get_ref_ticks(ctx->global); + return term_from_ref_ticks(ref_ticks, &ctx->heap); } target = globalcontext_get_process_lock(ctx->global, local_process_id); From 550104718eb726593f8b6bd094ce8bfc57660d24 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 9 Jun 2026 10:10:17 +0000 Subject: [PATCH 28/87] Reject the invalid owner process id when decoding a process reference A process reference encodes its owner pid in the third reference word. The decode path bounded it above (> TERM_MAX_LOCAL_PROCESS_ID) but accepted 0, which is INVALID_PROCESS_ID -- the sentinel meaning short ref / no owner. A genuine short reference is encoded as a two-word reference, never as a process reference, so a process reference carrying owner pid 0 is always malformed input; reject it like any other out-of-range pid. Signed-off-by: Davide Bettio --- src/libAtomVM/external_term.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libAtomVM/external_term.c b/src/libAtomVM/external_term.c index 34cc7b7f59..2457461849 100644 --- a/src/libAtomVM/external_term.c +++ b/src/libAtomVM/external_term.c @@ -1013,8 +1013,10 @@ static term parse_external_terms(const uint8_t *external_term_buf, size_t *eterm } else if (len == 3 && node == this_node && creation == this_creation) { uint64_t ticks = ((uint64_t) data[0]) << 32 | data[1]; uint32_t process_id = data[2]; - // Reject a malformed process id (the value comes from untrusted input). - if (process_id > TERM_MAX_LOCAL_PROCESS_ID) { + // Reject a malformed process id (the value comes from untrusted input). pid 0 is + // INVALID_PROCESS_ID, the short-ref sentinel, so it never names a real owner: a + // genuine short ref is encoded as a 2-word reference, not a process reference. + if (process_id == INVALID_PROCESS_ID || process_id > TERM_MAX_LOCAL_PROCESS_ID) { return term_invalid_term(); } return term_make_process_reference(process_id, ticks, heap); From 7efc5711dc4e9241b24a5f7e30ba7a3fbf261980 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 9 Jun 2026 10:10:28 +0000 Subject: [PATCH 29/87] Compare process references in wire word order in term_compare A local process reference was compared as [process_id, ticks_hi, ticks_lo], but term_to_binary and term_get_external_reference_words() lay the words out as [ticks_hi, ticks_lo, process_id]. A local process reference therefore ordered differently from its own serialized (external) form and from the external-vs-external comparison path. Lay the local words out in the same wire order in both the local-vs-local and the mixed local/external arms so all reference comparisons agree. test_refs_ordering is unaffected: its aliases all share one owner pid, so the position of process_id never decides their relative order. Signed-off-by: Davide Bettio --- src/libAtomVM/term.c | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/libAtomVM/term.c b/src/libAtomVM/term.c index d24f766aa7..caef26985d 100644 --- a/src/libAtomVM/term.c +++ b/src/libAtomVM/term.c @@ -705,14 +705,18 @@ TermCompareResult term_compare(term t, term other, TermCompareOpts opts, GlobalC other_data[0] = other_ticks >> 32; other_data[1] = (uint32_t) other_ticks; } else if (len == 3) { - data[0] = term_process_ref_to_process_id(t); + // Process references: compare in the same word order term_to_binary + // writes on the wire ([ticks_hi, ticks_lo, process_id]), so a local + // process reference orders consistently with its serialized form and + // with the external-reference path below. int64_t t_ticks = term_to_ref_ticks(t); - data[1] = t_ticks >> 32; - data[2] = (uint32_t) t_ticks; - other_data[0] = term_process_ref_to_process_id(other); + data[0] = t_ticks >> 32; + data[1] = (uint32_t) t_ticks; + data[2] = term_process_ref_to_process_id(t); int64_t other_ticks = term_to_ref_ticks(other); - other_data[1] = other_ticks >> 32; - other_data[2] = (uint32_t) other_ticks; + other_data[0] = other_ticks >> 32; + other_data[1] = (uint32_t) other_ticks; + other_data[2] = term_process_ref_to_process_id(other); } else { // len == 4 struct RefcBinary *refc = term_resource_refc_binary_ptr(t); @@ -791,24 +795,28 @@ TermCompareResult term_compare(term t, term other, TermCompareOpts opts, GlobalC other_data = local_data; } } else if (len == 3) { - // len == 3 (one is a local process reference) + // len == 3 (process references). Lay a local process + // reference out in wire order ([ticks_hi, ticks_lo, + // process_id]) to match the external reference words, so a + // mixed local/external comparison is consistent (and agrees + // with the local-vs-local and external-vs-external paths). uint32_t local_data[3]; if (term_is_external(t)) { data = term_get_external_reference_words(t); } else { - local_data[0] = term_process_ref_to_process_id(t); int64_t ref_ticks = term_to_ref_ticks(t); - local_data[1] = ref_ticks >> 32; - local_data[2] = (uint32_t) ref_ticks; + local_data[0] = ref_ticks >> 32; + local_data[1] = (uint32_t) ref_ticks; + local_data[2] = term_process_ref_to_process_id(t); data = local_data; } if (term_is_external(other)) { other_data = term_get_external_reference_words(other); } else { - local_data[0] = term_process_ref_to_process_id(other); int64_t ref_ticks = term_to_ref_ticks(other); - local_data[1] = ref_ticks >> 32; - local_data[2] = (uint32_t) ref_ticks; + local_data[0] = ref_ticks >> 32; + local_data[1] = (uint32_t) ref_ticks; + local_data[2] = term_process_ref_to_process_id(other); other_data = local_data; } } else { From d6386a958ced2add1a844f42be28c96d6a50097c Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 9 Jun 2026 10:10:28 +0000 Subject: [PATCH 30/87] Free the alias monitor through its container pointer in context_add_monitor The duplicate-alias branch freed new_monitor (the embedded struct Monitor *) rather than new_alias_monitor (the struct MonitorAlias container already recovered with CONTAINER_OF a few lines above), unlike every sibling branch. It works only because struct Monitor sits at offset 0; free the container pointer like the other branches. Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index d3319fc3c9..87a95ad493 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -1045,7 +1045,7 @@ bool context_add_monitor(Context *ctx, struct Monitor *new_monitor) struct MonitorAlias *existing_alias_monitor = CONTAINER_OF(existing, struct MonitorAlias, monitor); if (UNLIKELY(existing_alias_monitor->alias_type == new_alias_monitor->alias_type && existing_alias_monitor->ref_data.ref_ticks == new_alias_monitor->ref_data.ref_ticks)) { - free(new_monitor); + free(new_alias_monitor); return false; } break; From b5e5d92b27207754cd5b4c8dd0f92ea673ebbdd1 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 9 Jun 2026 10:12:21 +0000 Subject: [PATCH 31/87] Raise badarg on a malformed spawn_opt monitor option spawn_opt(F, [{monitor, BadTerm}]) where BadTerm is neither a list nor the atom 'true' fell through the monitor handling: is_spawn_monitor stayed false and the process was spawned with no monitor and no error, so a typo silently disabled monitoring. OTP raises badarg; raise it too, freeing the not-yet-published link halves and the unstarted context first. Signed-off-by: Davide Bettio --- src/libAtomVM/nifs.c | 9 +++++++++ tests/erlang_tests/test_monitor.erl | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index b24049319c..262e6f44e4 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1625,6 +1625,15 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free context_destroy(new_ctx); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } + } else if (UNLIKELY(!term_is_invalid_term(monitor_term))) { + // {monitor, BadTerm} where BadTerm is neither a list nor 'true': raise badarg like OTP, + // instead of silently spawning an unmonitored process. No link or monitor has been published + // yet, so free the (possibly allocated) link halves and destroy the not-yet-scheduled + // context; monitor_destroy(NULL) is a no-op. + monitor_destroy(new_link); + monitor_destroy(self_link); + context_destroy(new_ctx); + RAISE_ERROR(BADARG_ATOM); } // Nothing can fail from here on: publish the link and the monitors, in this order so the diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 09d9b9637a..07e79925b5 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -59,6 +59,7 @@ start() -> ok = test_monitor_alias_noproc_returns_alias(), ok = test_monitor_alias_self_installs_nothing(), ok = test_spawn_opt_link_monitor_badarg_is_atomic(), + ok = test_spawn_opt_monitor_non_list_badarg(), 0. test_monitor_normal() -> @@ -463,6 +464,24 @@ test_monitor_alias_dead_process() -> {'DOWN', Mon3, process, P, noproc} = recv_one(), ok. +%% spawn_opt(F, [{monitor, BadTerm}]) where BadTerm is neither a list nor 'true' must raise badarg, +%% like OTP -- not silently spawn an unmonitored process. Distinct from {monitor, [BadOption]}, which +%% fails inside the monitor-option parser. +test_spawn_opt_monitor_non_list_badarg() -> + ok = + try spawn_opt(fun() -> ok end, [{monitor, foo}]) of + R1 -> {unexpected, R1} + catch + error:badarg -> ok + end, + ok = + try spawn_opt(fun() -> ok end, [{monitor, 123}]) of + R2 -> {unexpected, R2} + catch + error:badarg -> ok + end, + ok. + spawn_monitor(LoopFun, Opts) -> spawn_opt(LoopFun, [{monitor, Opts}]). From 0a977e73797ac3f043ea52a5f4835c42061b6765 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 9 Jun 2026 10:22:26 +0000 Subject: [PATCH 32/87] Deactivate the alias on a same-batch DOWN before delivering alias messages process_outer_list converts alias messages (checking the alias is still active) while it splits the outer list, but MonitorDownSignal is only collected and applied later by the signal loop. So when a 'DOWN' and an alias send for the same {alias, demonitor} / {alias, reply_demonitor} alias arrive in one drain with the 'DOWN' first, the alias message was converted and delivered even though OTP applies the 'DOWN' first -- deactivating the alias -- and drops the message. Apply the alias deactivation in received order while splitting the outer list, so a later alias send in the same batch is dropped. The rest of the 'DOWN' still runs in the signal loop, where context_find_alias then returns NULL, so that deactivation is an idempotent no-op. The fix lives in the shared process_outer_list, so both the interpreter and JIT signal loops get it. Signed-off-by: Davide Bettio --- src/libAtomVM/mailbox.c | 16 ++++++++++++ tests/erlang_tests/test_monitor.erl | 38 +++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/libAtomVM/mailbox.c b/src/libAtomVM/mailbox.c index d0459ac1e5..4c317306c4 100644 --- a/src/libAtomVM/mailbox.c +++ b/src/libAtomVM/mailbox.c @@ -430,6 +430,22 @@ static MailboxMessage *process_outer_list(Context *ctx, Mailbox *mbox) } mailbox_message_dispose(current, &ctx->heap); } else { + // A 'DOWN' that auto-removes a {alias, demonitor} / {alias, reply_demonitor} monitor also + // deactivates its alias (see context_process_monitor_down_signal). Apply just that + // deactivation here, in received order, so an alias send arriving later in this SAME batch + // is dropped like OTP: the alias messages above are converted while the outer list is + // split, before the signal loop ever runs this 'DOWN'. The rest of the 'DOWN' (monitor + // removal, delivering the message) still happens when the signal loop calls + // context_process_monitor_down_signal; context_find_alias returns NULL there by then, so + // that deactivation is an idempotent no-op. + if (ctx != NULL && current->type == MonitorDownSignal) { + struct TermSignal *down_signal = CONTAINER_OF(current, struct TermSignal, base); + uint64_t ref_ticks = term_to_ref_ticks(term_get_tuple_element(down_signal->signal_term, 1)); + struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); + if (alias != NULL && alias->alias_type != ContextMonitorAliasExplicitUnalias) { + context_unalias(alias); + } + } current->next = NULL; if (signal_last == NULL) { signal_first = current; diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 07e79925b5..5d37670561 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -60,6 +60,7 @@ start() -> ok = test_monitor_alias_self_installs_nothing(), ok = test_spawn_opt_link_monitor_badarg_is_atomic(), ok = test_spawn_opt_monitor_non_list_badarg(), + ok = test_monitor_alias_down_before_send_same_batch(), 0. test_monitor_normal() -> @@ -482,6 +483,35 @@ test_spawn_opt_monitor_non_list_badarg() -> end, ok. +%% A 'DOWN' that deactivates a {alias, demonitor} alias must drop an alias send that lands in the +%% SAME mailbox drain, even though alias messages are converted before the 'DOWN' signal is applied: +%% the deactivation now runs in received order while the outer list is split. A relay forces both +%% into one batch -- the owner busy-waits on whereis/1 (which does not drain its mailbox), so its +%% first receive drains the 'DOWN' (received first) together with the later alias send. +test_monitor_alias_down_before_send_same_batch() -> + P = spawn_opt(fun() -> receive quit -> ok end end, []), + %% Monitor P before spawning the relay, so P holds the owner's monitor ahead of the relay's and + %% posts the owner's 'DOWN' first when it exits. + Mon = erlang:monitor(process, P, [{alias, demonitor}]), + Relay = spawn_opt( + fun() -> + erlang:monitor(process, P), + receive + {'DOWN', _, process, P, _} -> + Mon ! should_drop, + register(down_batch_relay, self()), + receive release -> ok end + end + end, + [] + ), + P ! quit, + ok = wait_registered(down_batch_relay, 5000000), + {'DOWN', Mon, process, P, normal} = recv_one(), + timeout = recv_one(), + Relay ! release, + ok. + spawn_monitor(LoopFun, Opts) -> spawn_opt(LoopFun, [{monitor, Opts}]). @@ -509,3 +539,11 @@ recv_one() -> Msg -> Msg after 500 -> timeout end. + +wait_registered(_Name, 0) -> + timeout; +wait_registered(Name, N) -> + case whereis(Name) of + undefined -> wait_registered(Name, N - 1); + _ -> ok + end. From fe199be0357f311461644b429e32b2748cb4a88c Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 9 Jun 2026 10:28:26 +0000 Subject: [PATCH 33/87] Re-type alias messages in place instead of copying The alias-to-normal-message conversion copied the message term into a freshly allocated Message and, on out of memory, dropped the message -- which is not transactional for reply_demonitor, whose alias deactivation and DemonitorSignal already ran inside context_process_alias_message_signal. struct TermSignal and struct Message have an identical layout (enforced by the _Static_asserts) and the message term already lives in the signal's own storage, so re-type the signal as a normal message in place. This removes the OOM drop and saves an allocation and free per alias send on the hot mailbox path. Signed-off-by: Davide Bettio --- src/libAtomVM/mailbox.c | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/libAtomVM/mailbox.c b/src/libAtomVM/mailbox.c index 4c317306c4..5b56734e66 100644 --- a/src/libAtomVM/mailbox.c +++ b/src/libAtomVM/mailbox.c @@ -413,22 +413,26 @@ static MailboxMessage *process_outer_list(Context *ctx, Mailbox *mbox) // sends to the same alias oldest-first. term message = context_process_alias_message_signal(ctx, CONTAINER_OF(current, struct TermSignal, base)); if (!term_is_invalid_term(message)) { - MailboxMessage *converted = mailbox_message_create_from_term(NormalMessage, message); - // Best effort: on out of memory the message is dropped. For reply_demonitor the side - // effects (alias deactivation, DemonitorSignal) have already happened inside - // context_process_alias_message_signal, so this drop is not transactional. - if (converted != NULL) { - // mailbox_message_create_from_term does not initialize next. - converted->next = NULL; - if (normal_last == NULL) { - normal_first = converted; - } else { - normal_last->next = converted; - } - normal_last = converted; + // Re-type the signal as a normal message in place: struct TermSignal and struct + // Message share an identical layout (the _Static_asserts above) and the message term + // already lives in this signal's own storage, so nothing is copied. Being + // allocation-free, the conversion can no longer drop a message on out of memory -- + // which matters for reply_demonitor, whose alias deactivation and DemonitorSignal + // have already run inside context_process_alias_message_signal. + Message *converted = CONTAINER_OF(current, Message, base); + converted->base.type = NormalMessage; + converted->message = message; + converted->base.next = NULL; + if (normal_last == NULL) { + normal_first = &converted->base; + } else { + normal_last->next = &converted->base; } + normal_last = &converted->base; + } else { + // Inactive alias: drop the message and free the signal. + mailbox_message_dispose(current, &ctx->heap); } - mailbox_message_dispose(current, &ctx->heap); } else { // A 'DOWN' that auto-removes a {alias, demonitor} / {alias, reply_demonitor} monitor also // deactivates its alias (see context_process_monitor_down_signal). Apply just that From 7bf7943c3b98b2317a66ba063b5f5129a08166bb Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 9 Jun 2026 10:29:41 +0000 Subject: [PATCH 34/87] Note the unchecked mailbox_send_monitor_signal OOM with a FIXME mailbox_send_monitor_signal returns void, so an allocation failure is silently dropped: the monitor leaks and the caller believes it was installed. Pre-existing; flag it for a future fix (return bool and let the caller clean up and raise). Signed-off-by: Davide Bettio --- src/libAtomVM/mailbox.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libAtomVM/mailbox.c b/src/libAtomVM/mailbox.c index 5b56734e66..a76d4411b3 100644 --- a/src/libAtomVM/mailbox.c +++ b/src/libAtomVM/mailbox.c @@ -339,6 +339,9 @@ void mailbox_send_monitor_signal(Context *c, enum MessageType type, struct Monit { struct MonitorPointerSignal *monitor_signal = malloc(sizeof(struct MonitorPointerSignal)); if (IS_NULL_PTR(monitor_signal)) { + // FIXME (pre-existing): this function returns void, so an out-of-memory here is silently + // dropped -- the monitor is leaked and the caller believes it was installed. It should return + // bool so the caller can free the monitor and raise out_of_memory. fprintf(stderr, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); return; } From 5d6a1ead61602961d299a51ebc0f18b132e0ce85 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 9 Jun 2026 19:06:54 +0000 Subject: [PATCH 35/87] Fix the REF_SIZE deprecation pragma to emit a proper warning Using REF_SIZE produced "warning: ignoring #pragma REF_SIZE is" because the pragma text is not a recognized pragma. Wrap it in the GCC warning pragma so the deprecation message is printed verbatim at any use site. REF_SIZE has no remaining users in the tree. Signed-off-by: Davide Bettio --- src/libAtomVM/term.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libAtomVM/term.h b/src/libAtomVM/term.h index d2c56e5e35..47ad005eb1 100644 --- a/src/libAtomVM/term.h +++ b/src/libAtomVM/term.h @@ -162,7 +162,7 @@ extern "C" { // If you change a reference size, make sure it doesn't // conflict with other reference sizes on all architectures. #define TERM_BOXED_REFERENCE_SHORT_SIZE ((int) ((sizeof(uint64_t) / sizeof(term)) + 1)) -#define REF_SIZE _Pragma("REF_SIZE is deprecated, use TERM_BOXED_REFERENCE_SHORT_SIZE instead") TERM_BOXED_REFERENCE_SHORT_SIZE +#define REF_SIZE _Pragma("GCC warning \"REF_SIZE is deprecated, use TERM_BOXED_REFERENCE_SHORT_SIZE instead\"") TERM_BOXED_REFERENCE_SHORT_SIZE #define TERM_BOXED_REFERENCE_PROCESS_SIZE (TERM_BOXED_REFERENCE_SHORT_SIZE + 1) #define TERM_BOXED_REFERENCE_PROCESS_HEADER (((TERM_BOXED_REFERENCE_PROCESS_SIZE - 1) << 6) | TERM_BOXED_REF) #if TERM_BYTES == 8 From 4120a28052b5ff83c118c40dfef98ec8b7c2342f Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 9 Jun 2026 19:06:54 +0000 Subject: [PATCH 36/87] Restore the TODO comment spacing in the esp32 network driver The alias feature commit accidentally dropped the space in "// TODO" while sweeping REF_SIZE through network_driver.c. Restore the original comment; the line is unrelated to process aliases and clang-format flags the new form. Signed-off-by: Davide Bettio --- src/platforms/esp32/components/avm_builtins/network_driver.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platforms/esp32/components/avm_builtins/network_driver.c b/src/platforms/esp32/components/avm_builtins/network_driver.c index 84eeb21167..dfe0fc5941 100644 --- a/src/platforms/esp32/components/avm_builtins/network_driver.c +++ b/src/platforms/esp32/components/avm_builtins/network_driver.c @@ -1886,7 +1886,7 @@ static NativeHandlerResult consume_mailbox(Context *ctx) return NativeContinue; } - //TODO: port this code to standard port (and gen_message) + // TODO: port this code to standard port (and gen_message) term pid = term_get_tuple_element(msg, 0); term ref = term_get_tuple_element(msg, 1); term cmd = term_get_tuple_element(msg, 2); From 2884d98ab6ef795c66c485005c5b4f2cdfcbf592 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 9 Jun 2026 21:16:43 +0000 Subject: [PATCH 37/87] Return false from unalias/1 on an external reference unalias/1 validated its argument with term_is_local_reference, raising badarg for a reference from another node. OTP 29 returns false instead (an external reference can never be an alias of the calling process); only non-references raise badarg. Sends to external references are already silently dropped, like OTP when the node is not alive; the new test pins both behaviours, BEAM-validated. Signed-off-by: Davide Bettio --- src/libAtomVM/nifs.c | 7 ++++++- tests/erlang_tests/test_monitor.erl | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 262e6f44e4..5760b9a685 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -7642,7 +7642,12 @@ static term nif_erlang_unalias(Context *ctx, int argc, term argv[]) UNUSED(argc); term process_ref = argv[0]; - VALIDATE_VALUE(process_ref, term_is_local_reference); + VALIDATE_VALUE(process_ref, term_is_reference); + if (!term_is_local_reference(process_ref)) { + // An external reference cannot be an alias of the calling process: return false like + // OTP, instead of raising badarg. + return FALSE_ATOM; + } uint64_t ref_ticks = term_to_ref_ticks(process_ref); struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 5d37670561..a93aea794d 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -61,6 +61,7 @@ start() -> ok = test_spawn_opt_link_monitor_badarg_is_atomic(), ok = test_spawn_opt_monitor_non_list_badarg(), ok = test_monitor_alias_down_before_send_same_batch(), + ok = test_unalias_and_send_non_local_refs(), 0. test_monitor_normal() -> @@ -512,6 +513,27 @@ test_monitor_alias_down_before_send_same_batch() -> Relay ! release, ok. +%% A reference from another node cannot be an alias of this process: unalias/1 returns false +%% (it does not raise), and a send to it is silently dropped, exactly like a send to a plain +%% local reference that never was an alias. (Verified against OTP 29.) +test_unalias_and_send_non_local_refs() -> + %% NEWER_REFERENCE_EXT (90): Len:16, Node atom, Creation:32, Len x 4-byte words. + ExtRef = binary_to_term( + <<131, 90, 2:16/integer-unsigned-big, 119, 3, "x@x", 1:32/integer-unsigned-big, + 1:32/integer-unsigned-big, 2:32/integer-unsigned-big>> + ), + true = is_reference(ExtRef), + false = unalias(ExtRef), + hello = (ExtRef ! hello), + false = unalias(make_ref()), + hello = (make_ref() ! hello), + ok = + receive + Unexpected -> {unexpected_message, Unexpected} + after 100 -> ok + end, + ok. + spawn_monitor(LoopFun, Opts) -> spawn_opt(LoopFun, [{monitor, Opts}]). From b9b7aa01233fa1ff759c2eb47651a58c37229081 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 9 Jun 2026 21:17:31 +0000 Subject: [PATCH 38/87] Echo the io request ReplyAs term back verbatim from the console The console driver rebuilt the io_reply reference from the request reference ticks. That erases the flavor of a process reference: an alias passed as ReplyAs came back as a short reference with the same ticks, which the requester's receive does not match, so an io request through an alias hung. It also crashed on a non-reference ReplyAs (the io protocol allows any term) by reading ref ticks from an arbitrary term. Put the incoming ReplyAs term in the reply instead: it lives in the request message storage until the handler returns and port_send_message copies it. This also drops a reference allocation from every console reply. Signed-off-by: Davide Bettio --- src/libAtomVM/nifs.c | 11 ++++++----- tests/erlang_tests/test_monitor.erl | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 5760b9a685..24c768ab4b 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1335,7 +1335,7 @@ static NativeHandlerResult process_console_message(Context *ctx, term msg) { // msg is not in the port's heap NativeHandlerResult result = NativeContinue; - if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(3) + TERM_BOXED_REFERENCE_PROCESS_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(3), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { fprintf(stderr, "Unable to allocate sufficient memory for console driver.\n"); AVM_ABORT(); } @@ -1353,7 +1353,6 @@ static NativeHandlerResult process_console_message(Context *ctx, term msg) term pid = term_get_tuple_element(msg, 1); term ref = term_get_tuple_element(msg, 2); term req = term_get_tuple_element(msg, 3); - uint64_t ref_ticks = term_to_ref_ticks(ref); if (is_tagged_tuple(req, PUT_CHARS_ATOM, 3)) { term chars = term_get_tuple_element(req, 2); @@ -1363,11 +1362,13 @@ static NativeHandlerResult process_console_message(Context *ctx, term msg) printf("%s", str); free(str); - term refcopy = term_from_ref_ticks(ref_ticks, &ctx->heap); - term reply = term_alloc_tuple(3, &ctx->heap); term_put_tuple_element(reply, 0, IO_REPLY_ATOM); - term_put_tuple_element(reply, 1, refcopy); + // Echo ReplyAs back verbatim (it still lives in msg's storage until this handler + // returns, and port_send_message copies it). Rebuilding it from its ref ticks would + // turn an alias (process reference) into a short reference that the requester's + // receive would not match -- and ReplyAs does not have to be a reference at all. + term_put_tuple_element(reply, 1, ref); term_put_tuple_element(reply, 2, OK_ATOM); port_send_message(ctx->global, pid, reply); diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index a93aea794d..7dc4c8ef22 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -62,6 +62,7 @@ start() -> ok = test_spawn_opt_monitor_non_list_badarg(), ok = test_monitor_alias_down_before_send_same_batch(), ok = test_unalias_and_send_non_local_refs(), + ok = test_io_request_alias_reply(), 0. test_monitor_normal() -> @@ -534,6 +535,27 @@ test_unalias_and_send_non_local_refs() -> end, ok. +%% The io protocol echoes ReplyAs back verbatim: an alias passed as ReplyAs must come back as +%% the very same reference -- not as a short reference rebuilt from its ticks, which would not +%% match it. (Verified against OTP 29.) +test_io_request_alias_reply() -> + %% On the BEAM the group leader is a full io server; on AtomVM the test process has no + %% group leader, so talk to the console port driver directly. + IoServer = + case erlang:system_info(machine) of + "BEAM" -> group_leader(); + _ -> open_port({spawn, "console"}, []) + end, + Alias = erlang:alias(), + IoServer ! {io_request, self(), Alias, {put_chars, unicode, <<>>}}, + ok = + receive + {io_reply, Alias, ok} -> ok + after 5000 -> io_reply_did_not_match_alias + end, + true = erlang:unalias(Alias), + ok. + spawn_monitor(LoopFun, Opts) -> spawn_opt(LoopFun, [{monitor, Opts}]). From a3a86b5b9904826a6aba1bf6070cf3d1131f82ea Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 9 Jun 2026 21:40:15 +0000 Subject: [PATCH 39/87] Reject process aliases as select handles The select machinery (socket nowait handles, enif_select refs used by the posix nifs) stores only the reference ticks and rebuilds the handle as a plain reference in the select notification. An alias passed as a handle would therefore come back as a reference the alias does not match, and the caller would hang waiting for the notification. On the BEAM an alias is a valid select handle and is echoed back verbatim. Supporting that would mean carrying the owner process id through the select machinery for a corner case with no practical use, so raise badarg instead and keep the code simple; the divergence is called out in comments at both validation sites. Signed-off-by: Davide Bettio --- src/libAtomVM/otp_socket.c | 7 +++++++ src/libAtomVM/resources.c | 7 +++++++ tests/libs/eavmlib/test_file.erl | 10 ++++++++++ tests/libs/estdlib/test_udp_socket.erl | 22 ++++++++++++++++++++++ 4 files changed, 46 insertions(+) diff --git a/src/libAtomVM/otp_socket.c b/src/libAtomVM/otp_socket.c index 37381fe344..ffe67fcebf 100644 --- a/src/libAtomVM/otp_socket.c +++ b/src/libAtomVM/otp_socket.c @@ -1032,6 +1032,13 @@ static term nif_socket_select_read(Context *ctx, int argc, term argv[]) term select_ref_term = argv[1]; if (select_ref_term != UNDEFINED_ATOM) { VALIDATE_VALUE(select_ref_term, term_is_local_reference); + // On the BEAM a process alias is also accepted as a select handle and is echoed back + // verbatim in the select notification. AtomVM stores only the reference ticks, which + // would rebuild the handle as a plain reference that the caller's alias would not + // match, so reject aliases here instead of supporting that corner case. + if (UNLIKELY(term_is_process_reference(select_ref_term))) { + RAISE_ERROR(BADARG_ATOM); + } } struct SocketResource *rsrc_obj; if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { diff --git a/src/libAtomVM/resources.c b/src/libAtomVM/resources.c index 8ebabb5166..2645cc458c 100644 --- a/src/libAtomVM/resources.c +++ b/src/libAtomVM/resources.c @@ -238,6 +238,13 @@ int enif_select(ErlNifEnv *env, ErlNifEvent event, enum ErlNifSelectFlags mode, if (UNLIKELY(mode & (ERL_NIF_SELECT_READ | ERL_NIF_SELECT_WRITE) && !term_is_local_reference(ref) && ref != UNDEFINED_ATOM)) { return ERL_NIF_SELECT_BADARG; } + // On the BEAM a process alias is also a valid enif_select ref and comes back verbatim in + // the select notification. AtomVM stores only the reference ticks, which would rebuild the + // ref as a plain reference that an alias would not match, so reject aliases instead of + // supporting that corner case. + if (UNLIKELY(term_is_process_reference(ref))) { + return ERL_NIF_SELECT_BADARG; + } return enif_select_common(env, event, mode, obj, pid, ref, NULL); } diff --git a/tests/libs/eavmlib/test_file.erl b/tests/libs/eavmlib/test_file.erl index c69a3a9f24..f0aff3d21f 100644 --- a/tests/libs/eavmlib/test_file.erl +++ b/tests/libs/eavmlib/test_file.erl @@ -66,6 +66,16 @@ test_fifo_select(_HasSelect) -> ok = atomvm:posix_mkfifo(Path, 8#644), {ok, RdFd} = atomvm:posix_open(Path, [o_rdonly]), {ok, WrFd} = atomvm:posix_open(Path, [o_wronly]), + %% A process alias is rejected as a select ref: AtomVM stores only the reference ticks, + %% so the notification could not carry the alias back (the BEAM's enif_select accepts it). + Alias = alias(), + ok = + try atomvm:posix_select_write(WrFd, self(), Alias) of + R -> {unexpected, R} + catch + error:badarg -> ok + end, + true = unalias(Alias), SelectWriteRef = make_ref(), ok = atomvm:posix_select_write(WrFd, self(), SelectWriteRef), ok = diff --git a/tests/libs/estdlib/test_udp_socket.erl b/tests/libs/estdlib/test_udp_socket.erl index 359b001bee..231ada6298 100644 --- a/tests/libs/estdlib/test_udp_socket.erl +++ b/tests/libs/estdlib/test_udp_socket.erl @@ -213,6 +213,7 @@ test_nowait() -> ok = test_nowait(fun receive_loop_nowait_ref/2), ok = test_nowait(fun receive_loop_recvfrom_nowait/2), ok = test_nowait(fun receive_loop_recvfrom_nowait_ref/2), + ok = test_alias_select_handle_rejected(), ok. test_nowait(ReceiveFun) -> @@ -267,6 +268,27 @@ receive_loop_nowait_ref(Socket, Packet) -> Error end. +%% On the BEAM a process alias is a valid select handle (select_handle() is any reference()) +%% and the select notification carries it back verbatim. AtomVM stores only the reference +%% ticks, which would rebuild the handle as a plain reference the alias would not match, so +%% it rejects aliases as select handles with badarg instead of supporting that corner case. +test_alias_select_handle_rejected() -> + case erlang:system_info(machine) of + "BEAM" -> + ok; + _ -> + {ok, Socket} = socket:open(inet, dgram, udp), + Alias = alias(), + ok = + try socket:recv(Socket, 1, Alias) of + R -> {unexpected, R} + catch + error:badarg -> ok + end, + true = unalias(Alias), + ok = socket:close(Socket) + end. + receive_loop_recvfrom_nowait(Socket, Packet) -> case socket:recvfrom(Socket, byte_size(Packet), nowait) of {ok, {_Source, ReceivedPacket}} when ReceivedPacket =:= Packet -> From 17814a18f8c4421b7d987bcaf12a56a8a7a3686a Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 9 Jun 2026 21:42:46 +0000 Subject: [PATCH 40/87] Make the alias tests deterministic on slow runners The alias tests used a single 500 ms recv_one window both for positive receives (the file convention elsewhere is 5000 ms) and for asserting that a message does NOT arrive. Under valgrind or a loaded CI runner the positive uses are the most likely false failure, and the two negative uses were detection windows a reintroduced bug could outlast. Raise recv_one to 5000 ms and assert the negatives behind deterministic fences instead: a later monitor whose DOWN fires after the buggy one (monitors fire in installation order), or a fence reply through the same echo process (sends from one process keep their order). Bound the one unbounded receive, and document that the same-batch DOWN test exercises the same-batch path reliably only on SMP builds. Signed-off-by: Davide Bettio --- tests/erlang_tests/test_monitor.erl | 42 ++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 7dc4c8ef22..186178b502 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -328,8 +328,13 @@ test_reply_demonitor_removes_monitor(SpawnFun) -> Ref = make_ref(), P ! {{reply, Ref}, Mon}, {reply, Ref} = recv_one(), + %% Fence: monitors fire in installation order, so if the reply had not removed the + %% monitor, its 'DOWN' would be enqueued before the fence's and recv_one would return + %% it first. (On the BEAM the buggy 'DOWN' does not exist, so order is irrelevant.) + Fence = monitor(process, P), P ! quit, - timeout = recv_one(), + {'DOWN', Fence, process, P, normal} = recv_one(), + ok = assert_no_message(), ok. %% reply_demonitor must resolve two sends to the same alias in one batch in RECEIVED order: @@ -342,7 +347,7 @@ test_reply_demonitor_same_batch_order() -> Mon ! first, Mon ! second, first = recv_one(), - timeout = recv_one(), + ok = assert_no_message(), P ! quit, ok. @@ -364,7 +369,11 @@ test_monitor_alias_demonitor_deactivates_on_down(SpawnFun) -> {'DOWN', Mon, process, P, normal} = recv_one(), Echo = spawn_opt(fun echo_loop/0, []), Echo ! {should_drop, Mon}, - timeout = recv_one(), + %% Fence through the same echo: sends from one process keep their order, so once the + %% fence reply arrives the dropped alias message can no longer show up afterwards. + Echo ! {fence, self()}, + fence = recv_one(), + ok = assert_no_message(), Echo ! quit, ok. @@ -392,9 +401,11 @@ test_alias_pid_send_order() -> %% immediate noproc DOWN), like OTP, not a plain reference. test_monitor_alias_noproc_returns_alias() -> {P, _} = spawn_opt(fun() -> ok end, [monitor]), - receive - {'DOWN', _, _, P, _} -> ok - end, + ok = + receive + {'DOWN', _, _, P, _} -> ok + after 5000 -> timeout + end, Mon = erlang:monitor(process, P, [{alias, explicit_unalias}]), {'DOWN', Mon, process, P, noproc} = recv_one(), Echo = spawn_opt(fun echo_loop/0, []), @@ -410,7 +421,7 @@ test_monitor_alias_noproc_returns_alias() -> test_monitor_alias_self_installs_nothing() -> Mon = erlang:monitor(process, self(), [{alias, explicit_unalias}]), Mon ! hello, - timeout = recv_one(), + ok = assert_no_message(), false = erlang:unalias(Mon), false = erlang:demonitor(Mon, [info]), ok. @@ -490,6 +501,10 @@ test_spawn_opt_monitor_non_list_badarg() -> %% the deactivation now runs in received order while the outer list is split. A relay forces both %% into one batch -- the owner busy-waits on whereis/1 (which does not drain its mailbox), so its %% first receive drains the 'DOWN' (received first) together with the later alias send. +%% On a single-scheduler build the owner may drain the 'DOWN' alone before the relay runs; +%% the test then still passes through the cross-batch deactivation path -- the same-batch +%% path is reliably exercised on SMP builds (a schedule-in drain is a VM property no Erlang +%% code can pin down). test_monitor_alias_down_before_send_same_batch() -> P = spawn_opt(fun() -> receive quit -> ok end end, []), %% Monitor P before spawning the relay, so P holds the owner's monitor ahead of the relay's and @@ -510,7 +525,7 @@ test_monitor_alias_down_before_send_same_batch() -> P ! quit, ok = wait_registered(down_batch_relay, 5000000), {'DOWN', Mon, process, P, normal} = recv_one(), - timeout = recv_one(), + ok = assert_no_message(), Relay ! release, ok. @@ -581,7 +596,16 @@ echo_loop() -> recv_one() -> receive Msg -> Msg - after 500 -> timeout + after 5000 -> timeout + end. + +%% Assert that no message arrives. Only call this when the would-be message is already +%% settled (delivered or dropped) -- behind a fence reply or after a same-process send -- +%% so the short window is not a race. +assert_no_message() -> + receive + Msg -> {unexpected_message, Msg} + after 100 -> ok end. wait_registered(_Name, 0) -> From 716e0a9801f0d78354b785922bab4f923f36c0c5 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 9 Jun 2026 21:44:07 +0000 Subject: [PATCH 41/87] Tidy the alias tests Quit the echo workers the alias tests spawn (the file convention; harmless today only because each module runs in a fresh VM), rewrap a comment that exceeded the 100-column print width, and restore the blank line between two test functions in test_binary_to_term. Signed-off-by: Davide Bettio --- tests/erlang_tests/test_binary_to_term.erl | 1 + tests/erlang_tests/test_monitor.erl | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/erlang_tests/test_binary_to_term.erl b/tests/erlang_tests/test_binary_to_term.erl index d499e31e43..4ae419a086 100644 --- a/tests/erlang_tests/test_binary_to_term.erl +++ b/tests/erlang_tests/test_binary_to_term.erl @@ -995,6 +995,7 @@ test_encode_resource(OTPVersion) -> AlteredResource4 = binary_to_term(AlteredResourceBin4), false = AlteredResource4 =:= Resource, ok. + test_encode_process_ref() -> ProcessRef = erlang:alias(), ProcessRef = binary_to_term(term_to_binary(ProcessRef)), diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 186178b502..99105ded6b 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -262,6 +262,7 @@ test_alias() -> P = spawn_opt(fun echo_loop/0, []), Alias = erlang:alias(), do_test_alias(P, Alias), + P ! quit, ok. test_multiple_aliases() -> @@ -272,6 +273,7 @@ test_multiple_aliases() -> do_test_alias(P, A1), do_test_alias(P, A3), do_test_alias(P, A2), + P ! quit, ok. test_multiple_unaliases() -> @@ -288,6 +290,7 @@ test_unalias_from_wrong_process() -> false = recv_one(), P = spawn_opt(fun echo_loop/0, []), do_test_alias(P, A), + P ! quit, ok. do_test_alias(P, Alias) -> @@ -306,6 +309,7 @@ do_test_alias(P, Alias, UnaliasFun) -> test_monitor_alias_demonitor(SpawnFun) -> {P, Mon} = SpawnFun(fun echo_loop/0, [{alias, demonitor}]), do_test_alias(P, Mon, fun demonitor/1), + P ! quit, ok. test_monitor_alias_explicit_unalias(SpawnFun) -> @@ -314,11 +318,13 @@ test_monitor_alias_explicit_unalias(SpawnFun) -> m1 = recv_one(), demonitor(Mon), do_test_alias(P, Mon), + P ! quit, ok. test_monitor_alias_reply_demonitor(SpawnFun) -> {P, Mon} = SpawnFun(fun echo_loop/0, [{alias, reply_demonitor}]), do_test_alias(P, Mon, fun(_Mon) -> ok end), + P ! quit, ok. %% reply_demonitor must, on the first reply through the alias, also remove the underlying monitor @@ -479,8 +485,8 @@ test_monitor_alias_dead_process() -> ok. %% spawn_opt(F, [{monitor, BadTerm}]) where BadTerm is neither a list nor 'true' must raise badarg, -%% like OTP -- not silently spawn an unmonitored process. Distinct from {monitor, [BadOption]}, which -%% fails inside the monitor-option parser. +%% like OTP -- not silently spawn an unmonitored process. Distinct from +%% {monitor, [BadOption]}, which fails inside the monitor-option parser. test_spawn_opt_monitor_non_list_badarg() -> ok = try spawn_opt(fun() -> ok end, [{monitor, foo}]) of From 763a87354cba027806e879a952174a04ced075ed Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 9 Jun 2026 21:44:37 +0000 Subject: [PATCH 42/87] Rename the spawn_monitor test helper to spawn_opt_monitor The helper shadowed the auto-imported spawn_monitor/2 BIF and made erlc emit seven "ambiguous call of overridden auto-imported BIF" warnings on every build of the test sources. Resolution picked the local function, so behavior was correct, but the warnings were noise this branch introduced. Signed-off-by: Davide Bettio --- tests/erlang_tests/test_monitor.erl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 99105ded6b..6e94a315fb 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -40,19 +40,19 @@ start() -> ok = test_multiple_unaliases(), ok = test_unalias_from_wrong_process(), ok = test_monitor_alias_dead_process(), - ok = test_monitor_multiple_aliases_monitors(fun spawn_monitor/2), + ok = test_monitor_multiple_aliases_monitors(fun spawn_opt_monitor/2), ok = test_monitor_multiple_aliases_monitors(fun spawn_and_monitor/2), - ok = test_monitor_alias_demonitor(fun spawn_monitor/2), + ok = test_monitor_alias_demonitor(fun spawn_opt_monitor/2), ok = test_monitor_alias_demonitor(fun spawn_and_monitor/2), - ok = test_monitor_alias_explicit_unalias(fun spawn_monitor/2), + ok = test_monitor_alias_explicit_unalias(fun spawn_opt_monitor/2), ok = test_monitor_alias_explicit_unalias(fun spawn_and_monitor/2), - ok = test_monitor_alias_reply_demonitor(fun spawn_monitor/2), + ok = test_monitor_alias_reply_demonitor(fun spawn_opt_monitor/2), ok = test_monitor_alias_reply_demonitor(fun spawn_and_monitor/2), - ok = test_reply_demonitor_removes_monitor(fun spawn_monitor/2), + ok = test_reply_demonitor_removes_monitor(fun spawn_opt_monitor/2), ok = test_reply_demonitor_removes_monitor(fun spawn_and_monitor/2), - ok = test_monitor_down_alias(fun spawn_monitor/2), + ok = test_monitor_down_alias(fun spawn_opt_monitor/2), ok = test_monitor_down_alias(fun spawn_and_monitor/2), - ok = test_monitor_alias_demonitor_deactivates_on_down(fun spawn_monitor/2), + ok = test_monitor_alias_demonitor_deactivates_on_down(fun spawn_opt_monitor/2), ok = test_monitor_alias_demonitor_deactivates_on_down(fun spawn_and_monitor/2), ok = test_alias_pid_send_order(), ok = test_reply_demonitor_same_batch_order(), @@ -577,7 +577,7 @@ test_io_request_alias_reply() -> true = erlang:unalias(Alias), ok. -spawn_monitor(LoopFun, Opts) -> +spawn_opt_monitor(LoopFun, Opts) -> spawn_opt(LoopFun, [{monitor, Opts}]). spawn_and_monitor(LoopFun, Opts) -> From 0e3df1f93b28aa59600bc80a312e0488716e20e1 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 9 Jun 2026 21:46:32 +0000 Subject: [PATCH 43/87] Describe the alias modes precisely in the monitor/3 documentation The demonitor mode also deactivates on DOWN delivery (not only on demonitor/1), and reply_demonitor removes the monitor as well as the alias when the first message sent via the alias is delivered (verified against OTP 29). Also note that the OTP {tag, Term} option raises unsupported. Signed-off-by: Davide Bettio --- libs/estdlib/src/erlang.erl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/libs/estdlib/src/erlang.erl b/libs/estdlib/src/erlang.erl index 369e0cc47e..09d3d20ce6 100644 --- a/libs/estdlib/src/erlang.erl +++ b/libs/estdlib/src/erlang.erl @@ -1329,10 +1329,12 @@ monitor(_Type, _PidOrPort) -> %% makes the monitor also an alias on the calling process (see `alias/0'). %% `AliasMode' defines the behaviour of the alias: %% - explicit_unalias - the alias can be only removed with `unalias/1', -%% - demonitor - the alias is also removed when `demonitor/1' is called -%% on the monitor, -%% - reply_demonitor - the alias is also removed after a first message -%% is sent via it. +%% - demonitor - the alias is also removed when the monitor is removed, +%% by `demonitor/1' or by the delivery of a `DOWN' message, +%% - reply_demonitor - additionally, the alias is deactivated and the +%% monitor removed (as by `demonitor/1') when the +%% first message sent via the alias is delivered. +%% The OTP `{tag, Term}' option is not supported and raises `unsupported'. %% @end %%----------------------------------------------------------------------------- -spec monitor From e3207bd907a2ed5883ca7833ea979fa17f971696 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 9 Jun 2026 21:46:32 +0000 Subject: [PATCH 44/87] Add alias coverage for map and ets keys and demonitor flush Two conformance cells the alias tests did not pin down yet: an alias used as a map and ets key (ordinary reference term handling, value comparison), and demonitor(Mon, [flush]) on an {alias, demonitor} monitor, where the alias died with the monitor at DOWN delivery and flush removes the queued DOWN. Both BEAM-validated. Signed-off-by: Davide Bettio --- tests/erlang_tests/test_monitor.erl | 44 +++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 6e94a315fb..d84bb847ea 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -63,6 +63,8 @@ start() -> ok = test_monitor_alias_down_before_send_same_batch(), ok = test_unalias_and_send_non_local_refs(), ok = test_io_request_alias_reply(), + ok = test_alias_as_key(), + ok = test_monitor_alias_demonitor_flush(), 0. test_monitor_normal() -> @@ -577,6 +579,48 @@ test_io_request_alias_reply() -> true = erlang:unalias(Alias), ok. +%% An alias is an ordinary reference for term handling: usable as a map and ets key, +%% compared by value and distinct from any plain reference. +test_alias_as_key() -> + Alias = erlang:alias(), + Plain = make_ref(), + Map = #{Alias => alias_value, Plain => plain_value}, + alias_value = maps:get(Alias, Map), + plain_value = maps:get(Plain, Map), + Tid = ets:new(alias_key_table, []), + true = ets:insert(Tid, {Alias, alias_value}), + true = ets:insert(Tid, {Plain, plain_value}), + [{Alias, alias_value}] = ets:lookup(Tid, Alias), + [{Plain, plain_value}] = ets:lookup(Tid, Plain), + true = ets:delete(Tid, Alias), + [] = ets:lookup(Tid, Alias), + [{Plain, plain_value}] = ets:lookup(Tid, Plain), + true = erlang:unalias(Alias), + ok. + +%% demonitor(Mon, [flush]) on an {alias, demonitor} monitor: the alias died with the monitor +%% at 'DOWN' delivery, and flush removes the already-delivered 'DOWN' from the queue. +test_monitor_alias_demonitor_flush() -> + P = spawn_opt(fun() -> receive quit -> ok end end, []), + Mon = erlang:monitor(process, P, [{alias, demonitor}]), + Fence = monitor(process, P), + P ! quit, + %% Selective receive: Mon's 'DOWN' was enqueued first (installation order) and stays queued. + ok = + receive + {'DOWN', Fence, process, P, normal} -> ok + after 5000 -> timeout + end, + true = demonitor(Mon, [flush]), + ok = assert_no_message(), + Echo = spawn_opt(fun echo_loop/0, []), + Echo ! {should_drop, Mon}, + Echo ! {fence, self()}, + fence = recv_one(), + ok = assert_no_message(), + Echo ! quit, + ok. + spawn_opt_monitor(LoopFun, Opts) -> spawn_opt(LoopFun, [{monitor, Opts}]). From 9e15abd26752d5d3d8f3204a242c92036f6cf3d4 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 10 Jun 2026 07:28:30 +0000 Subject: [PATCH 45/87] Skip alias lookups for processes without active aliases context_find_alias walks the whole monitor list and runs for every DOWN delivery (twice: once in the outer-list split for same-batch ordering, once in the down handler) and for every demonitor -- also for the vast majority of processes that never create an alias. In a DOWN-heavy benchmark (50 batches of 1000 monitored process exits) this cost 25% over the pre-alias baseline. Track the number of active aliases in a 16-bit slice of the existing Context bitfield word (no struct growth, owner-written only like the surrounding bits) and short-circuit context_find_alias when it is zero. The benchmark returns to the baseline noise envelope; processes that do use aliases keep the previous behavior. Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 20 ++++++++++++++++---- src/libAtomVM/context.h | 6 +++++- src/libAtomVM/mailbox.c | 2 +- src/libAtomVM/nifs.c | 2 +- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index 87a95ad493..1ee6d2c3c3 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -83,6 +83,7 @@ Context *context_new(GlobalContext *glb) ctx->heap_growth_strategy = BoundedFreeHeapGrowth; ctx->has_min_heap_size = 0; ctx->has_max_heap_size = 0; + ctx->active_alias_count = 0; mailbox_init(&ctx->mailbox); @@ -449,7 +450,7 @@ void context_process_monitor_down_signal(Context *ctx, struct TermSignal *signal // monitor is removed, including the automatic removal at 'DOWN' delivery. struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); if (alias != NULL && alias->alias_type != ContextMonitorAliasExplicitUnalias) { - context_unalias(alias); + context_unalias(ctx, alias); } // Enqueue the term as a message. @@ -476,7 +477,7 @@ void context_process_monitor_down_signal(Context *ctx, struct TermSignal *signal // monitor's automatic removal at 'DOWN' delivery. struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); if (alias != NULL && alias->alias_type != ContextMonitorAliasExplicitUnalias) { - context_unalias(alias); + context_unalias(ctx, alias); } free(monitoring_monitor); @@ -875,6 +876,7 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) } case CONTEXT_MONITOR_ALIAS: { struct MonitorAlias *alias = CONTAINER_OF(monitor, struct MonitorAlias, monitor); + ctx->active_alias_count--; free(alias); break; } @@ -1075,6 +1077,9 @@ bool context_add_monitor(Context *ctx, struct Monitor *new_monitor) } } list_append(&ctx->monitors_head, &new_monitor->monitor_list_head); + if (new_monitor->monitor_type == CONTEXT_MONITOR_ALIAS) { + ctx->active_alias_count++; + } return true; } @@ -1185,7 +1190,7 @@ void context_demonitor(Context *ctx, uint64_t ref_ticks) { struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); if (alias != NULL && alias->alias_type != ContextMonitorAliasExplicitUnalias) { - context_unalias(alias); + context_unalias(ctx, alias); } struct ListHead *item; @@ -1230,6 +1235,11 @@ void context_demonitor(Context *ctx, uint64_t ref_ticks) struct MonitorAlias *context_find_alias(Context *ctx, uint64_t ref_ticks) { + // The vast majority of processes never create an alias: skip the monitor list walk + // entirely for them (this runs for every 'DOWN' and demonitor, not only for aliases). + if (LIKELY(ctx->active_alias_count == 0)) { + return NULL; + } struct ListHead *item; LIST_FOR_EACH (item, &ctx->monitors_head) { struct Monitor *monitor = GET_LIST_ENTRY(item, struct Monitor, monitor_list_head); @@ -1244,9 +1254,11 @@ struct MonitorAlias *context_find_alias(Context *ctx, uint64_t ref_ticks) return NULL; } -void context_unalias(struct MonitorAlias *alias) +void context_unalias(Context *ctx, struct MonitorAlias *alias) { TERM_DEBUG_ASSERT(alias != NULL); + TERM_DEBUG_ASSERT(ctx->active_alias_count > 0); + ctx->active_alias_count--; struct Monitor *monitor = &alias->monitor; list_remove(&monitor->monitor_list_head); free(alias); diff --git a/src/libAtomVM/context.h b/src/libAtomVM/context.h index cf36d599c0..29a2fc87de 100644 --- a/src/libAtomVM/context.h +++ b/src/libAtomVM/context.h @@ -138,6 +138,9 @@ struct Context unsigned int leader : 1; unsigned int has_min_heap_size : 1; unsigned int has_max_heap_size : 1; + // Number of active aliases on monitors_head. Owner-written only, like the surrounding + // bits; lets the alias lookups short-circuit for the common alias-free process. + unsigned int active_alias_count : 16; bool trap_exit : 1; #ifndef AVM_NO_EMU @@ -645,9 +648,10 @@ struct MonitorAlias *context_find_alias(Context *ctx, uint64_t ref_ticks); /** * @brief Remove an alias of a process * + * @param ctx the context owning the alias * @param alias The alias to remove, can be obtained using context_find_alias */ -void context_unalias(struct MonitorAlias *alias); +void context_unalias(Context *ctx, struct MonitorAlias *alias); /** * @brief Get target of a monitor. diff --git a/src/libAtomVM/mailbox.c b/src/libAtomVM/mailbox.c index a76d4411b3..7782fa0f5f 100644 --- a/src/libAtomVM/mailbox.c +++ b/src/libAtomVM/mailbox.c @@ -450,7 +450,7 @@ static MailboxMessage *process_outer_list(Context *ctx, Mailbox *mbox) uint64_t ref_ticks = term_to_ref_ticks(term_get_tuple_element(down_signal->signal_term, 1)); struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); if (alias != NULL && alias->alias_type != ContextMonitorAliasExplicitUnalias) { - context_unalias(alias); + context_unalias(ctx, alias); } } current->next = NULL; diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 24c768ab4b..fc47c388f2 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -7655,7 +7655,7 @@ static term nif_erlang_unalias(Context *ctx, int argc, term argv[]) if (IS_NULL_PTR(alias)) { return FALSE_ATOM; } else { - context_unalias(alias); + context_unalias(ctx, alias); return TRUE_ATOM; } } From 14a5259000583b41c29c6ad071abacc451cc5922 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 10 Jun 2026 07:30:57 +0000 Subject: [PATCH 46/87] Add alias coverage for duplicate options and a name-of-self monitor Pin two more behaviors verified against OTP 29: when {alias, _} appears twice in the monitor options the last occurrence wins, and monitoring the caller through its own registered name installs nothing, exactly like monitoring self() directly. Signed-off-by: Davide Bettio --- tests/erlang_tests/test_monitor.erl | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index d84bb847ea..8b383dfb39 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -65,6 +65,8 @@ start() -> ok = test_io_request_alias_reply(), ok = test_alias_as_key(), ok = test_monitor_alias_demonitor_flush(), + ok = test_monitor_alias_duplicate_option(), + ok = test_monitor_alias_registered_self_installs_nothing(), 0. test_monitor_normal() -> @@ -621,6 +623,28 @@ test_monitor_alias_demonitor_flush() -> Echo ! quit, ok. +%% Duplicate {alias, _} options: the last one wins, like OTP 29 (probed in both orders). +test_monitor_alias_duplicate_option() -> + P = spawn_opt(fun echo_loop/0, []), + Mon = erlang:monitor(process, P, [{alias, demonitor}, {alias, explicit_unalias}]), + true = demonitor(Mon), + %% explicit_unalias won: the alias survives the demonitor. + do_test_alias(P, Mon), + P ! quit, + ok. + +%% monitor(process, NameOfSelf, [{alias, _}]) resolves the name first and installs nothing, +%% exactly like monitoring self() directly. (Verified against OTP 29.) +test_monitor_alias_registered_self_installs_nothing() -> + true = register(alias_self_name, self()), + Mon = erlang:monitor(process, alias_self_name, [{alias, explicit_unalias}]), + Mon ! hello, + ok = assert_no_message(), + false = erlang:unalias(Mon), + false = erlang:demonitor(Mon, [info]), + true = unregister(alias_self_name), + ok. + spawn_opt_monitor(LoopFun, Opts) -> spawn_opt(LoopFun, [{monitor, Opts}]). From ddbdb2bdb6117196978c39be203877d1790730d8 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 10 Jun 2026 07:33:46 +0000 Subject: [PATCH 47/87] Implement erlang:alias/1 alias/0 becomes alias([]) with explicit_unalias as the default mode. The reply option needs no new machinery: it maps onto the reply_demonitor alias type, whose conversion path deactivates the alias when the first message sent via it is delivered -- with no monitor to remove that is exactly the OTP reply behavior (verified against OTP 29, including that a second same-batch message is dropped). Bad options and a non-list argument raise badarg before any side effect. Signed-off-by: Davide Bettio --- CHANGELOG.md | 2 +- libs/estdlib/src/erlang.erl | 14 ++++++++++ src/libAtomVM/defaultatoms.def | 1 + src/libAtomVM/nifs.c | 26 ++++++++++++++++--- src/libAtomVM/nifs.gperf | 1 + tests/erlang_tests/test_monitor.erl | 40 +++++++++++++++++++++++++++++ 6 files changed, 80 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be41f064ff..52544fff38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `"USB_SERIAL_JTAG"` peripheral to the ESP32 `uart` module on chips with a built-in USB-Serial-JTAG controller (C3/C5/C6/C61/H2/H21/H4/P4/S3) - Added support for the `safe` option in `erlang:binary_to_term/2` -- Added support for process aliases (OTP ≥ 26 semantics): `erlang:alias/0`, `erlang:unalias/1`, +- Added support for process aliases (OTP ≥ 26 semantics): `erlang:alias/0,1`, `erlang:unalias/1`, `erlang:monitor/3` with the `{alias, Mode}` option, `spawn_opt` `{monitor, MonitorOpts}` and sending to an alias reference - Added xtensa JIT backend for esp32 platform diff --git a/libs/estdlib/src/erlang.erl b/libs/estdlib/src/erlang.erl index 09d3d20ce6..09c7c41339 100644 --- a/libs/estdlib/src/erlang.erl +++ b/libs/estdlib/src/erlang.erl @@ -177,6 +177,7 @@ tuple_size/1, tuple_to_list/1, alias/0, + alias/1, unalias/1 ]). @@ -2197,6 +2198,19 @@ tuple_to_list(_Tuple) -> alias() -> erlang:nif_error(undefined). +%%----------------------------------------------------------------------------- +%% @param Options alias options +%% @returns A reference aliasing the calling process. +%% @doc Creates an alias for the calling process, like `alias/0'. +%% With `explicit_unalias' (the default, so `alias([])' is `alias/0') +%% the alias stays active until `unalias/1'; with `reply' it is +%% deactivated when the first message sent via the alias is delivered. +%% @end +%%----------------------------------------------------------------------------- +-spec alias(Options) -> Alias when Options :: [explicit_unalias | reply], Alias :: reference(). +alias(_Options) -> + erlang:nif_error(undefined). + %%----------------------------------------------------------------------------- %% @param Alias the alias to be removed. %% @returns `true' if alias was removed, `false' if it was not found diff --git a/src/libAtomVM/defaultatoms.def b/src/libAtomVM/defaultatoms.def index 229271306f..25580005f5 100644 --- a/src/libAtomVM/defaultatoms.def +++ b/src/libAtomVM/defaultatoms.def @@ -227,4 +227,5 @@ X(ALIAS_ATOM, "\x5", "alias") X(DEMONITOR_ATOM, "\x9", "demonitor") X(EXPLICIT_UNALIAS_ATOM, "\x10", "explicit_unalias") X(REPLY_DEMONITOR_ATOM, "\xF", "reply_demonitor") +X(REPLY_ATOM, "\x5", "reply") X(TAG_ATOM, "\x3", "tag") diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index fc47c388f2..272692d0e9 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -7621,8 +7621,28 @@ static term nif_erlang_crc32_combine_3(Context *ctx, int argc, term argv[]) static term nif_erlang_alias(Context *ctx, int argc, term argv[]) { - UNUSED(argc); - UNUSED(argv); + // alias/0 behaves as alias([]); the default mode is explicit_unalias. The reply option + // maps onto the reply_demonitor machinery: with no monitor to remove, the alias is just + // deactivated when the first message sent via it is delivered, which is the OTP behavior. + context_monitor_alias_type_t alias_type = ContextMonitorAliasExplicitUnalias; + if (argc == 1) { + term opts = argv[0]; + VALIDATE_VALUE(opts, term_is_list); + while (term_is_nonempty_list(opts)) { + term option = term_get_list_head(opts); + if (option == EXPLICIT_UNALIAS_ATOM) { + alias_type = ContextMonitorAliasExplicitUnalias; + } else if (option == REPLY_ATOM) { + alias_type = ContextMonitorAliasReplyDemonitor; + } else { + RAISE_ERROR(BADARG_ATOM); + } + opts = term_get_list_tail(opts); + } + if (UNLIKELY(!term_is_nil(opts))) { + RAISE_ERROR(BADARG_ATOM); + } + } if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_PROCESS_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); @@ -7630,7 +7650,7 @@ static term nif_erlang_alias(Context *ctx, int argc, term argv[]) RefData ref_data = { .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = ctx->process_id }; term process_ref = term_from_ref_data(&ref_data, &ctx->heap); - struct Monitor *monitor = monitor_alias_new(&ref_data, ContextMonitorAliasExplicitUnalias); + struct Monitor *monitor = monitor_alias_new(&ref_data, alias_type); if (IS_NULL_PTR(monitor)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } diff --git a/src/libAtomVM/nifs.gperf b/src/libAtomVM/nifs.gperf index ac9ffd59bb..b02cb5ac6a 100644 --- a/src/libAtomVM/nifs.gperf +++ b/src/libAtomVM/nifs.gperf @@ -160,6 +160,7 @@ erlang:module_loaded/1,&module_loaded_nif erlang:nif_error/1,&nif_error_nif erlang:list_to_bitstring/1, &list_to_bitstring_nif erlang:alias/0, &erlang_alias_nif +erlang:alias/1, &erlang_alias_nif erlang:unalias/1, &erlang_unalias_nif erts_debug:flat_size/1, &flat_size_nif erts_internal:cmp_term/2, &erts_internal_cmp_term_nif diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 8b383dfb39..bd2dfbfcf9 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -67,6 +67,8 @@ start() -> ok = test_monitor_alias_demonitor_flush(), ok = test_monitor_alias_duplicate_option(), ok = test_monitor_alias_registered_self_installs_nothing(), + ok = test_alias_1(), + ok = test_alias_reply_mode(), 0. test_monitor_normal() -> @@ -645,6 +647,44 @@ test_monitor_alias_registered_self_installs_nothing() -> true = unregister(alias_self_name), ok. +%% alias/1: alias([]) and alias([explicit_unalias]) behave like alias/0; bad options and a +%% non-list argument raise badarg. (Verified against OTP 29.) +test_alias_1() -> + A1 = alias([]), + A1 ! x1, + x1 = recv_one(), + true = unalias(A1), + A2 = alias([explicit_unalias]), + A2 ! x2, + x2 = recv_one(), + true = unalias(A2), + ok = + try alias([bogus]) of + R1 -> {unexpected, R1} + catch + error:badarg -> ok + end, + ok = + try alias(explicit_unalias) of + R2 -> {unexpected, R2} + catch + error:badarg -> ok + end, + ok. + +%% alias([reply]): the alias is deactivated when the first message sent via it is delivered, +%% so a second message in the same batch is dropped and later sends are dropped too, like OTP. +test_alias_reply_mode() -> + A = alias([reply]), + A ! m1, + A ! m2, + m1 = recv_one(), + ok = assert_no_message(), + A ! m3, + ok = assert_no_message(), + false = unalias(A), + ok. + spawn_opt_monitor(LoopFun, Opts) -> spawn_opt(LoopFun, [{monitor, Opts}]). From ae038ff6ce10d8d53effbf1922c7a480427982da Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 10 Jun 2026 07:35:23 +0000 Subject: [PATCH 48/87] Return the message from a JIT send to a dropped reference The JIT send sets x0 to the message in every delivering branch, but the fall-through that silently drops a send to a non-alias reference left x0 holding the recipient, so Ref ! Msg returned the reference instead of Msg on the JIT flavor (the interpreter assigns x0 after the dispatch and was correct). Caught by the new non-local-refs send test, which only ran on the interpreter flavor until now. Signed-off-by: Davide Bettio --- src/libAtomVM/jit.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/libAtomVM/jit.c b/src/libAtomVM/jit.c index 54abbd716b..6858a76a1f 100644 --- a/src/libAtomVM/jit.c +++ b/src/libAtomVM/jit.c @@ -866,10 +866,13 @@ static bool jit_send(Context *ctx, JITState *jit_state) } else if (!term_is_reference(recipient_term)) { set_error(ctx, jit_state, 0, BADARG_ATOM); return false; + } else { + // A reference that is not a local process reference (short/resource/external ref): the + // message is silently dropped, as OTP drops a send to a non-active-alias reference, but + // send still returns the message. Distributed aliases (external references) are + // unsupported, so they are lost. + ctx->x[0] = ctx->x[1]; } - // else: a reference that is not a local process reference (short/resource/external ref) is silently - // dropped, as OTP drops a send to a non-active-alias reference. Distributed aliases (external - // references) are unsupported, so they are lost. return true; } From efdd536025a3832b73055ee8b1246b0fb09b64b9 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 10 Jun 2026 14:23:30 +0000 Subject: [PATCH 49/87] Pad process references instead of resource references on 32-bit Reference kinds are distinguished by their boxed size, and on 32-bit the natural process reference size (header + 2 ticks words + pid = 4) collides with the resource reference. Resolving the collision by growing the resource reference put the extra word on the wrong side: resource references are used all over the embedded targets (sockets, files, NIF resources), while process references only exist when aliases are used. Move the padding word to the process reference, so 32-bit resource references return to 4 words and only alias users pay the extra word. The padding word is initialized to nil so GC copies a defined value. The size static asserts now check pairwise distinctness, since the sizes are no longer monotonically ordered (32-bit: short 3, resource 4, process 5; 64-bit: short 2, process 3, resource 4). The wire format is unchanged (the padding word is not serialized), and all allocation sites size through TERM_BOXED_REFERENCE_PROCESS_SIZE. Signed-off-by: Davide Bettio --- src/libAtomVM/term.h | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/libAtomVM/term.h b/src/libAtomVM/term.h index 47ad005eb1..f5da9a1fb7 100644 --- a/src/libAtomVM/term.h +++ b/src/libAtomVM/term.h @@ -129,14 +129,7 @@ extern "C" { #define TERM_BOXED_REFC_BINARY_SIZE 6 #define TERM_BOXED_BIN_MATCH_STATE_SIZE 4 #define TERM_BOXED_SUB_BINARY_SIZE 4 -#if TERM_BYTES == 8 #define TERM_BOXED_REFERENCE_RESOURCE_SIZE 4 -#else -// Enough size would be 4, but reference types -// are distinguished by size and 4 conflicts with -// TERM_BOXED_REFERENCE_PROCESS_SIZE on 32bit arch. -#define TERM_BOXED_REFERENCE_RESOURCE_SIZE 5 -#endif #define TERM_BOXED_REFERENCE_RESOURCE_HEADER (((TERM_BOXED_REFERENCE_RESOURCE_SIZE - 1) << 6) | TERM_BOXED_REF) #define TERM_BOXED_RESOURCE_SIZE TERM_BOXED_REFERENCE_RESOURCE_SIZE @@ -163,7 +156,15 @@ extern "C" { // conflict with other reference sizes on all architectures. #define TERM_BOXED_REFERENCE_SHORT_SIZE ((int) ((sizeof(uint64_t) / sizeof(term)) + 1)) #define REF_SIZE _Pragma("GCC warning \"REF_SIZE is deprecated, use TERM_BOXED_REFERENCE_SHORT_SIZE instead\"") TERM_BOXED_REFERENCE_SHORT_SIZE +#if TERM_BYTES == 8 #define TERM_BOXED_REFERENCE_PROCESS_SIZE (TERM_BOXED_REFERENCE_SHORT_SIZE + 1) +#else +// Enough size would be 3 + 1, but that is the resource reference size on 32-bit, and +// reference types are distinguished by size. Pad the process reference instead of the +// resource reference: process references only exist when aliases are used, while +// resource references are everywhere on the embedded targets. +#define TERM_BOXED_REFERENCE_PROCESS_SIZE (TERM_BOXED_REFERENCE_SHORT_SIZE + 2) +#endif #define TERM_BOXED_REFERENCE_PROCESS_HEADER (((TERM_BOXED_REFERENCE_PROCESS_SIZE - 1) << 6) | TERM_BOXED_REF) #if TERM_BYTES == 8 #define EXTERNAL_PID_SIZE 3 @@ -182,9 +183,11 @@ extern "C" { #endif #define EXTERNAL_REF_MAX_WORDS 5 #define TERM_BOXED_REFERENCE_MAX_SIZE EXTERNAL_REF_SIZE(EXTERNAL_REF_MAX_WORDS) -_Static_assert(TERM_BOXED_REFERENCE_SHORT_SIZE < TERM_BOXED_REFERENCE_PROCESS_SIZE, "Short ref size must be smaller than process ref size"); -_Static_assert(TERM_BOXED_REFERENCE_PROCESS_SIZE < TERM_BOXED_REFERENCE_RESOURCE_SIZE, "Process ref size must be smaller than reference resource size"); +_Static_assert(TERM_BOXED_REFERENCE_SHORT_SIZE != TERM_BOXED_REFERENCE_PROCESS_SIZE, "Short ref size must differ from process ref size"); +_Static_assert(TERM_BOXED_REFERENCE_SHORT_SIZE != TERM_BOXED_REFERENCE_RESOURCE_SIZE, "Short ref size must differ from reference resource size"); +_Static_assert(TERM_BOXED_REFERENCE_PROCESS_SIZE != TERM_BOXED_REFERENCE_RESOURCE_SIZE, "Process ref size must differ from reference resource size"); _Static_assert(TERM_BOXED_REFERENCE_PROCESS_SIZE <= TERM_BOXED_REFERENCE_MAX_SIZE, "Max ref size can't be smaller than all other ref sizes"); +_Static_assert(TERM_BOXED_REFERENCE_RESOURCE_SIZE <= TERM_BOXED_REFERENCE_MAX_SIZE, "Max ref size can't be smaller than all other ref sizes"); #define TUPLE_SIZE(elems) ((int) (elems + 1)) #define CONS_SIZE 2 #define REFC_BINARY_CONS_OFFSET 4 @@ -2283,6 +2286,12 @@ static inline term term_make_process_reference(int32_t process_id, uint64_t ref_ #error "terms must be either 32 or 64 bit wide" #endif boxed_value[REFERENCE_PROCESS_PID_OFFSET] = process_id; +#if TERM_BYTES == 4 + // On 32-bit the process reference is one word larger than header + ticks + pid (so reference + // shapes stay size-distinguishable); initialize the trailing padding word so GC copies a + // defined value instead of uninitialized memory. + boxed_value[REFERENCE_PROCESS_PID_OFFSET + 1] = term_nil(); +#endif return ((term) boxed_value) | TERM_PRIMARY_BOXED; } @@ -3074,12 +3083,6 @@ static inline term term_from_resource(void *resource, Heap *heap) term *boxed_value = memory_heap_alloc(heap, TERM_BOXED_REFERENCE_RESOURCE_SIZE); boxed_value[0] = TERM_BOXED_REFERENCE_RESOURCE_HEADER; boxed_value[1] = (term) refc; -#if TERM_BOXED_REFERENCE_RESOURCE_SIZE > (REFERENCE_RESOURCE_CONS_OFFSET + CONS_SIZE) - // On 32-bit the resource reference is one word larger than header + refc + mso cons (so reference - // shapes stay size-distinguishable); initialize the trailing padding word so GC copies a defined - // value instead of uninitialized memory. - boxed_value[REFERENCE_RESOURCE_CONS_OFFSET + CONS_SIZE] = term_nil(); -#endif // Add the resource to the mso list refc_binary_add_refcount(refc, 1); term ret = ((term) boxed_value) | TERM_PRIMARY_BOXED; From 54266fa215475932263dafa8fae549765777c4bc Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 10 Jun 2026 14:33:53 +0000 Subject: [PATCH 50/87] DO NOT SQUASH: Encode the DOWN ref shape in the monitor type instead of RefData Storing a RefData (ref ticks + owner pid) in every local monitor grew each MonitorLocalMonitor, MonitorLocalRegisteredNameMonitor and MonitorAlias by 8 bytes (uint64_t alignment), paid by every plain monitor/2 -- i.e. every gen_server:call -- on the embedded targets. The pid was redundant everywhere: on the monitored side it duplicates monitor_obj (the monitoring process, which is exactly the alias owner), on the monitoring, registered-name and alias entries it was never read. Keep a plain uint64_t ref_ticks in the monitor structs (their baseline layout) and carry the only new bit of information -- "the 'DOWN' reference must be rebuilt alias-shaped" -- as a monitor type, CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS. The 'DOWN' path derives the process reference pid from monitor_obj. monitor_new now takes the monitor type directly, replacing the is_monitoring bool as well. RefData stays as the NIF-local value type used to build result references (term_from_ref_data) in monitor/3, spawn_opt and alias/1. Behavior is unchanged and pinned by the existing tests (test_monitor_down_alias asserts the 'DOWN' carries the very alias reference; process_info(monitored_by) keeps listing alias-shaped monitors). Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 87 ++++++++++++++++++++++++----------------- src/libAtomVM/context.h | 28 +++++++------ src/libAtomVM/nifs.c | 20 +++++----- 3 files changed, 79 insertions(+), 56 deletions(-) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index 1ee6d2c3c3..1a6d362ce7 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -275,6 +275,7 @@ void context_destroy(Context *ctx) } case CONTEXT_MONITOR_LINK_LOCAL: case CONTEXT_MONITOR_MONITORED_LOCAL: + case CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS: case CONTEXT_MONITOR_MONITORING_LOCAL: case CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME: case CONTEXT_MONITOR_ALIAS: @@ -441,7 +442,7 @@ void context_process_monitor_down_signal(Context *ctx, struct TermSignal *signal struct Monitor *monitor = GET_LIST_ENTRY(item, struct Monitor, monitor_list_head); if (monitor->monitor_type == CONTEXT_MONITOR_MONITORING_LOCAL) { struct MonitorLocalMonitor *monitoring_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); - if (monitoring_monitor->monitor_obj == monitor_obj && monitoring_monitor->ref_data.ref_ticks == ref_ticks) { + if (monitoring_monitor->monitor_obj == monitor_obj && monitoring_monitor->ref_ticks == ref_ticks) { // Remove link list_remove(&monitor->monitor_list_head); free(monitoring_monitor); @@ -460,7 +461,7 @@ void context_process_monitor_down_signal(Context *ctx, struct TermSignal *signal } else if (monitor->monitor_type == CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME) { int32_t monitor_process_id = term_to_local_process_id(monitor_obj); struct MonitorLocalRegisteredNameMonitor *monitoring_monitor = CONTAINER_OF(monitor, struct MonitorLocalRegisteredNameMonitor, monitor); - if (monitoring_monitor->monitor_process_id == monitor_process_id && monitoring_monitor->ref_data.ref_ticks == ref_ticks) { + if (monitoring_monitor->monitor_process_id == monitor_process_id && monitoring_monitor->ref_ticks == ref_ticks) { // Remove link list_remove(&monitor->monitor_list_head); @@ -622,7 +623,8 @@ bool context_get_process_info(Context *ctx, term *out, size_t *term_size, term a ret_size = TUPLE_SIZE(2); LIST_FOR_EACH (item, &ctx->monitors_head) { struct Monitor *monitor = GET_LIST_ENTRY(item, struct Monitor, monitor_list_head); - if (monitor->monitor_type == CONTEXT_MONITOR_MONITORED_LOCAL) { + if (monitor->monitor_type == CONTEXT_MONITOR_MONITORED_LOCAL + || monitor->monitor_type == CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS) { ret_size += CONS_SIZE; } else if (monitor->monitor_type == CONTEXT_MONITOR_RESOURCE) { ret_size += CONS_SIZE + TERM_BOXED_REFERENCE_RESOURCE_SIZE; @@ -731,7 +733,8 @@ bool context_get_process_info(Context *ctx, term *out, size_t *term_size, term a struct ListHead *item; LIST_FOR_EACH (item, &ctx->monitors_head) { struct Monitor *monitor = GET_LIST_ENTRY(item, struct Monitor, monitor_list_head); - if (monitor->monitor_type == CONTEXT_MONITOR_MONITORED_LOCAL) { + if (monitor->monitor_type == CONTEXT_MONITOR_MONITORED_LOCAL + || monitor->monitor_type == CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS) { struct MonitorLocalMonitor *monitored_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); list = term_list_prepend(monitored_monitor->monitor_obj, list, heap); } else if (monitor->monitor_type == CONTEXT_MONITOR_RESOURCE) { @@ -789,7 +792,7 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) Context *target = globalcontext_get_process_nolock(glb, local_process_id); if (LIKELY(target != NULL)) { // target can be null if we didn't process a MonitorDownSignal - mailbox_send_ref_signal(target, DemonitorSignal, monitoring_monitor->ref_data.ref_ticks); + mailbox_send_ref_signal(target, DemonitorSignal, monitoring_monitor->ref_ticks); } free(monitoring_monitor); break; @@ -801,7 +804,7 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) Context *target = globalcontext_get_process_nolock(glb, local_process_id); if (LIKELY(target != NULL)) { // target can be null if we didn't process a MonitorDownSignal - mailbox_send_ref_signal(target, DemonitorSignal, monitoring_monitor->ref_data.ref_ticks); + mailbox_send_ref_signal(target, DemonitorSignal, monitoring_monitor->ref_ticks); } free(monitoring_monitor); break; @@ -843,7 +846,8 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) } break; } - case CONTEXT_MONITOR_MONITORED_LOCAL: { + case CONTEXT_MONITOR_MONITORED_LOCAL: + case CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS: { struct MonitorLocalMonitor *monitored_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); int32_t local_process_id = term_to_local_process_id(monitored_monitor->monitor_obj); Context *target = globalcontext_get_process_nolock(glb, local_process_id); @@ -856,8 +860,15 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) globalcontext_get_process_unlock(glb, target); AVM_ABORT(); } - // Prepare the message on ctx's heap which will be freed afterwards. - term ref = term_from_ref_data(&monitored_monitor->ref_data, &ctx->heap); + // Prepare the message on ctx's heap which will be freed afterwards. A monitor + // created with the {alias, _} option must carry the same alias-shaped process + // reference the monitoring process got, whose pid is the monitoring process. + term ref; + if (monitor->monitor_type == CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS) { + ref = term_make_process_reference(local_process_id, monitored_monitor->ref_ticks, &ctx->heap); + } else { + ref = term_from_ref_ticks(monitored_monitor->ref_ticks, &ctx->heap); + } term port_or_process = term_pid_or_port_from_context(ctx); term port_or_process_atom @@ -919,24 +930,23 @@ struct Monitor *monitor_link_new(term link_pid) } } -struct Monitor *monitor_new(term monitor_pid, const RefData *ref_data, bool is_monitoring) +struct Monitor *monitor_new(term monitor_pid, uint64_t ref_ticks, enum ContextMonitorType monitor_type) { + assert(monitor_type == CONTEXT_MONITOR_MONITORING_LOCAL + || monitor_type == CONTEXT_MONITOR_MONITORED_LOCAL + || monitor_type == CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS); struct MonitorLocalMonitor *monitor = malloc(sizeof(struct MonitorLocalMonitor)); if (IS_NULL_PTR(monitor)) { return NULL; } - if (is_monitoring) { - monitor->monitor.monitor_type = CONTEXT_MONITOR_MONITORING_LOCAL; - } else { - monitor->monitor.monitor_type = CONTEXT_MONITOR_MONITORED_LOCAL; - } + monitor->monitor.monitor_type = monitor_type; monitor->monitor_obj = monitor_pid; - monitor->ref_data = *ref_data; + monitor->ref_ticks = ref_ticks; return &monitor->monitor; } -struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, const RefData *ref_data) +struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, uint64_t ref_ticks) { struct MonitorLocalRegisteredNameMonitor *monitor = malloc(sizeof(struct MonitorLocalRegisteredNameMonitor)); if (IS_NULL_PTR(monitor)) { @@ -945,19 +955,19 @@ struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, t monitor->monitor.monitor_type = CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME; monitor->monitor_process_id = monitor_process_id; monitor->monitor_name = monitor_name; - monitor->ref_data = *ref_data; + monitor->ref_ticks = ref_ticks; return &monitor->monitor; } -struct Monitor *monitor_alias_new(const RefData *ref_data, context_monitor_alias_type_t alias_type) +struct Monitor *monitor_alias_new(uint64_t ref_ticks, context_monitor_alias_type_t alias_type) { struct MonitorAlias *monitor = malloc(sizeof(struct MonitorAlias)); if (IS_NULL_PTR(monitor)) { return NULL; } monitor->monitor.monitor_type = CONTEXT_MONITOR_ALIAS; - monitor->ref_data = *ref_data; + monitor->ref_ticks = ref_ticks; monitor->alias_type = alias_type; return &monitor->monitor; @@ -991,6 +1001,7 @@ void monitor_destroy(struct Monitor *monitor) break; case CONTEXT_MONITOR_MONITORING_LOCAL: case CONTEXT_MONITOR_MONITORED_LOCAL: + case CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS: free(CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor)); break; case CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME: @@ -1022,10 +1033,11 @@ bool context_add_monitor(Context *ctx, struct Monitor *new_monitor) break; } case CONTEXT_MONITOR_MONITORING_LOCAL: - case CONTEXT_MONITOR_MONITORED_LOCAL: { + case CONTEXT_MONITOR_MONITORED_LOCAL: + case CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS: { struct MonitorLocalMonitor *new_local_monitor = CONTAINER_OF(new_monitor, struct MonitorLocalMonitor, monitor); struct MonitorLocalMonitor *existing_local_monitor = CONTAINER_OF(existing, struct MonitorLocalMonitor, monitor); - if (UNLIKELY(existing_local_monitor->monitor_obj == new_local_monitor->monitor_obj && existing_local_monitor->ref_data.ref_ticks == new_local_monitor->ref_data.ref_ticks)) { + if (UNLIKELY(existing_local_monitor->monitor_obj == new_local_monitor->monitor_obj && existing_local_monitor->ref_ticks == new_local_monitor->ref_ticks)) { free(new_local_monitor); return false; } @@ -1036,7 +1048,7 @@ bool context_add_monitor(Context *ctx, struct Monitor *new_monitor) struct MonitorLocalRegisteredNameMonitor *existing_local_registeredname_monitor = CONTAINER_OF(existing, struct MonitorLocalRegisteredNameMonitor, monitor); if (UNLIKELY(existing_local_registeredname_monitor->monitor_process_id == new_local_registeredname_monitor->monitor_process_id && existing_local_registeredname_monitor->monitor_name == new_local_registeredname_monitor->monitor_name - && existing_local_registeredname_monitor->ref_data.ref_ticks == new_local_registeredname_monitor->ref_data.ref_ticks)) { + && existing_local_registeredname_monitor->ref_ticks == new_local_registeredname_monitor->ref_ticks)) { free(new_local_registeredname_monitor); return false; } @@ -1046,7 +1058,7 @@ bool context_add_monitor(Context *ctx, struct Monitor *new_monitor) struct MonitorAlias *new_alias_monitor = CONTAINER_OF(new_monitor, struct MonitorAlias, monitor); struct MonitorAlias *existing_alias_monitor = CONTAINER_OF(existing, struct MonitorAlias, monitor); - if (UNLIKELY(existing_alias_monitor->alias_type == new_alias_monitor->alias_type && existing_alias_monitor->ref_data.ref_ticks == new_alias_monitor->ref_data.ref_ticks)) { + if (UNLIKELY(existing_alias_monitor->alias_type == new_alias_monitor->alias_type && existing_alias_monitor->ref_ticks == new_alias_monitor->ref_ticks)) { free(new_alias_monitor); return false; } @@ -1198,9 +1210,10 @@ void context_demonitor(Context *ctx, uint64_t ref_ticks) struct Monitor *monitor = GET_LIST_ENTRY(item, struct Monitor, monitor_list_head); switch (monitor->monitor_type) { case CONTEXT_MONITOR_MONITORING_LOCAL: - case CONTEXT_MONITOR_MONITORED_LOCAL: { + case CONTEXT_MONITOR_MONITORED_LOCAL: + case CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS: { struct MonitorLocalMonitor *local_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); - if (local_monitor->ref_data.ref_ticks == ref_ticks) { + if (local_monitor->ref_ticks == ref_ticks) { list_remove(&monitor->monitor_list_head); free(local_monitor); return; @@ -1209,7 +1222,7 @@ void context_demonitor(Context *ctx, uint64_t ref_ticks) } case CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME: { struct MonitorLocalRegisteredNameMonitor *local_registeredname_monitor = CONTAINER_OF(monitor, struct MonitorLocalRegisteredNameMonitor, monitor); - if (local_registeredname_monitor->ref_data.ref_ticks == ref_ticks) { + if (local_registeredname_monitor->ref_ticks == ref_ticks) { list_remove(&monitor->monitor_list_head); free(local_registeredname_monitor); return; @@ -1245,7 +1258,7 @@ struct MonitorAlias *context_find_alias(Context *ctx, uint64_t ref_ticks) struct Monitor *monitor = GET_LIST_ENTRY(item, struct Monitor, monitor_list_head); if (monitor->monitor_type == CONTEXT_MONITOR_ALIAS) { struct MonitorAlias *alias_monitor = CONTAINER_OF(monitor, struct MonitorAlias, monitor); - if (alias_monitor->ref_data.ref_ticks == ref_ticks) { + if (alias_monitor->ref_ticks == ref_ticks) { return alias_monitor; } } @@ -1271,9 +1284,10 @@ term context_get_monitor_pid(Context *ctx, uint64_t ref_ticks, bool *is_monitori struct Monitor *monitor = GET_LIST_ENTRY(item, struct Monitor, monitor_list_head); switch (monitor->monitor_type) { case CONTEXT_MONITOR_MONITORING_LOCAL: - case CONTEXT_MONITOR_MONITORED_LOCAL: { + case CONTEXT_MONITOR_MONITORED_LOCAL: + case CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS: { struct MonitorLocalMonitor *local_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); - if (local_monitor->ref_data.ref_ticks == ref_ticks) { + if (local_monitor->ref_ticks == ref_ticks) { *is_monitoring = monitor->monitor_type == CONTEXT_MONITOR_MONITORING_LOCAL; return local_monitor->monitor_obj; } @@ -1281,7 +1295,7 @@ term context_get_monitor_pid(Context *ctx, uint64_t ref_ticks, bool *is_monitori } case CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME: { struct MonitorLocalRegisteredNameMonitor *local_registeredname_monitor = CONTAINER_OF(monitor, struct MonitorLocalRegisteredNameMonitor, monitor); - if (local_registeredname_monitor->ref_data.ref_ticks == ref_ticks) { + if (local_registeredname_monitor->ref_ticks == ref_ticks) { *is_monitoring = true; return term_from_local_process_id(local_registeredname_monitor->monitor_process_id); } @@ -1417,21 +1431,22 @@ COLD_FUNC void context_dump(Context *ctx) struct MonitorLocalMonitor *monitoring_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); fprintf(stderr, "monitor to "); term_display(stderr, monitoring_monitor->monitor_obj, ctx); - fprintf(stderr, " ref=%lu", (long unsigned) monitoring_monitor->ref_data.ref_ticks); + fprintf(stderr, " ref=%lu", (long unsigned) monitoring_monitor->ref_ticks); fprintf(stderr, "\n"); break; } case CONTEXT_MONITOR_ALIAS: { struct MonitorAlias *monitor_alias = CONTAINER_OF(monitor, struct MonitorAlias, monitor); - fprintf(stderr, "has alias ref=%lu", (long unsigned) monitor_alias->ref_data.ref_ticks); + fprintf(stderr, "has alias ref=%lu", (long unsigned) monitor_alias->ref_ticks); fprintf(stderr, "\n"); break; } - case CONTEXT_MONITOR_MONITORED_LOCAL: { + case CONTEXT_MONITOR_MONITORED_LOCAL: + case CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS: { struct MonitorLocalMonitor *monitored_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); fprintf(stderr, "monitored by "); term_display(stderr, monitored_monitor->monitor_obj, ctx); - fprintf(stderr, " ref=%lu", (long unsigned) monitored_monitor->ref_data.ref_ticks); + fprintf(stderr, " ref=%lu", (long unsigned) monitored_monitor->ref_ticks); fprintf(stderr, "\n"); break; } @@ -1441,7 +1456,7 @@ COLD_FUNC void context_dump(Context *ctx) term_display(stderr, local_registeredname_monitor->monitor_name, ctx); fprintf(stderr, " ("); term_display(stderr, term_from_local_process_id(local_registeredname_monitor->monitor_process_id), ctx); - fprintf(stderr, ") ref=%lu", (long unsigned) local_registeredname_monitor->ref_data.ref_ticks); + fprintf(stderr, ") ref=%lu", (long unsigned) local_registeredname_monitor->ref_ticks); fprintf(stderr, "\n"); break; } diff --git a/src/libAtomVM/context.h b/src/libAtomVM/context.h index 29a2fc87de..5d8bb2bb62 100644 --- a/src/libAtomVM/context.h +++ b/src/libAtomVM/context.h @@ -181,6 +181,10 @@ enum ContextMonitorType CONTEXT_MONITOR_LINK_REMOTE, CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME, CONTEXT_MONITOR_ALIAS, + // Like CONTEXT_MONITOR_MONITORED_LOCAL, for a monitor created with the {alias, _} option: + // the 'DOWN' reference must be rebuilt as a process reference (the alias the monitoring + // process got), whose pid is monitor_obj, instead of a short reference. + CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS, }; typedef enum @@ -211,14 +215,14 @@ struct LinkLocalMonitor struct MonitorLocalMonitor { struct Monitor monitor; - RefData ref_data; + uint64_t ref_ticks; term monitor_obj; }; struct MonitorLocalRegisteredNameMonitor { struct Monitor monitor; - RefData ref_data; + uint64_t ref_ticks; int32_t monitor_process_id; term monitor_name; }; @@ -226,7 +230,7 @@ struct MonitorLocalRegisteredNameMonitor struct MonitorAlias { struct Monitor monitor; - RefData ref_data; + uint64_t ref_ticks; context_monitor_alias_type_t alias_type; }; @@ -543,31 +547,33 @@ struct Monitor *monitor_link_new(term link_pid); /** * @brief Create a monitor on a process. * - * @param monitor_pid monitored process - * @param ref_data reference of the monitor - * @param is_monitoring if ctx is the monitoring process + * @param monitor_pid monitored process (or monitoring process when ctx is the monitored one) + * @param ref_ticks reference of the monitor + * @param monitor_type \c CONTEXT_MONITOR_MONITORING_LOCAL for the monitoring process's half, + * \c CONTEXT_MONITOR_MONITORED_LOCAL or \c CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS (when the + * monitor was created with the {alias, _} option) for the monitored process's half * @return the allocated monitor or NULL if allocation failed */ -struct Monitor *monitor_new(term monitor_pid, const RefData *ref_data, bool is_monitoring); +struct Monitor *monitor_new(term monitor_pid, uint64_t ref_ticks, enum ContextMonitorType monitor_type); /** * @brief Create a process alias. * - * @param ref_data reference of the alias + * @param ref_ticks reference of the alias * @param alias_type when the alias is deactivated, see the erlang:monitor/3 alias option * @return the allocated monitor or NULL if allocation failed */ -struct Monitor *monitor_alias_new(const RefData *ref_data, context_monitor_alias_type_t alias_type); +struct Monitor *monitor_alias_new(uint64_t ref_ticks, context_monitor_alias_type_t alias_type); /** * @brief Create a monitor on a process by registered name. * * @param monitor_process_id monitored process id * @param monitor_name name of the monitor (atom) - * @param ref_data reference of the monitor + * @param ref_ticks reference of the monitor * @return the allocated monitor or NULL if allocation failed */ -struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, const RefData *ref_data); +struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, uint64_t ref_ticks); /** * @brief Create a resource monitor. diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 272692d0e9..b2995ab802 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1584,7 +1584,7 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free } if (is_alias) { ref_data = (RefData){ .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = ctx->process_id }; - alias_monitor = monitor_alias_new(&ref_data, alias_type); + alias_monitor = monitor_alias_new(ref_data.ref_ticks, alias_type); if (IS_NULL_PTR(alias_monitor)) { monitor_destroy(new_link); monitor_destroy(self_link); @@ -1595,7 +1595,8 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free ref_data = (RefData){ .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = INVALID_PROCESS_ID }; } - new_monitor = monitor_new(term_from_local_process_id(ctx->process_id), &ref_data, false); + new_monitor = monitor_new(term_from_local_process_id(ctx->process_id), ref_data.ref_ticks, + is_alias ? CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS : CONTEXT_MONITOR_MONITORED_LOCAL); if (IS_NULL_PTR(new_monitor)) { monitor_destroy(new_link); monitor_destroy(self_link); @@ -1603,7 +1604,7 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free context_destroy(new_ctx); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - self_monitor = monitor_new(new_pid, &ref_data, true); + self_monitor = monitor_new(new_pid, ref_data.ref_ticks, CONTEXT_MONITOR_MONITORING_LOCAL); if (IS_NULL_PTR(self_monitor)) { monitor_destroy(new_link); monitor_destroy(self_link); @@ -5100,7 +5101,7 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) // stays active (demonitor / reply_demonitor would be deactivated right away, as at a DOWN). struct Monitor *alias_monitor = NULL; if (is_alias && alias_type == ContextMonitorAliasExplicitUnalias) { - alias_monitor = monitor_alias_new(&ref_data, alias_type); + alias_monitor = monitor_alias_new(ref_data.ref_ticks, alias_type); if (IS_NULL_PTR(alias_monitor)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } @@ -5139,7 +5140,7 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) struct Monitor *alias_monitor = NULL; if (is_alias) { ref_data = (RefData){ .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = ctx->process_id }; - alias_monitor = monitor_alias_new(&ref_data, alias_type); + alias_monitor = monitor_alias_new(ref_data.ref_ticks, alias_type); if (IS_NULL_PTR(alias_monitor)) { globalcontext_get_process_unlock(ctx->global, target); RAISE_ERROR(OUT_OF_MEMORY_ATOM); @@ -5149,9 +5150,9 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) } struct Monitor *self_monitor; if (term_is_atom(target_proc)) { - self_monitor = monitor_registeredname_monitor_new(local_process_id, target_proc, &ref_data); + self_monitor = monitor_registeredname_monitor_new(local_process_id, target_proc, ref_data.ref_ticks); } else { - self_monitor = monitor_new(target_pid, &ref_data, true); + self_monitor = monitor_new(target_pid, ref_data.ref_ticks, CONTEXT_MONITOR_MONITORING_LOCAL); } if (IS_NULL_PTR(self_monitor)) { globalcontext_get_process_unlock(ctx->global, target); @@ -5159,7 +5160,8 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) RAISE_ERROR(OUT_OF_MEMORY_ATOM); } term monitoring_pid = term_from_local_process_id(ctx->process_id); - struct Monitor *other_monitor = monitor_new(monitoring_pid, &ref_data, false); + struct Monitor *other_monitor = monitor_new(monitoring_pid, ref_data.ref_ticks, + is_alias ? CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS : CONTEXT_MONITOR_MONITORED_LOCAL); if (IS_NULL_PTR(other_monitor)) { monitor_destroy(alias_monitor); monitor_destroy(self_monitor); @@ -7650,7 +7652,7 @@ static term nif_erlang_alias(Context *ctx, int argc, term argv[]) RefData ref_data = { .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = ctx->process_id }; term process_ref = term_from_ref_data(&ref_data, &ctx->heap); - struct Monitor *monitor = monitor_alias_new(&ref_data, alias_type); + struct Monitor *monitor = monitor_alias_new(ref_data.ref_ticks, alias_type); if (IS_NULL_PTR(monitor)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } From 610a5b17ab280aba0f193e3ff34eece101ed0200 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 10 Jun 2026 14:35:48 +0000 Subject: [PATCH 51/87] Saturate active_alias_count instead of wrapping at 65536 aliases The 16-bit active_alias_count was incremented and decremented unchecked: a process accumulating 65536 simultaneously active aliases would wrap the counter to 0, after which context_find_alias short-circuits and every alias of that process silently stops matching -- messages dropped, unalias/1 returning false, reply_demonitor monitors no longer removed. Silent message loss is the worst failure mode for aliases. Make the counter saturate stickily: at 0xFFFF a new bit, alias_count_saturated, is set and the count is pinned (decrements are skipped). Since the pinned count is never 0, the context_find_alias fast path needs no change -- a saturated process simply always walks its monitor list, degrading the optimization instead of correctness. The bit packs into the existing bitfield word, so sizeof(Context) is unchanged. Not covered by a test: creating 65536 live aliases is infeasible in CI (context_add_monitor's duplicate scan makes it quadratic); the three touched sites (add, unalias, terminate) were reviewed for symmetry instead. Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 19 ++++++++++++++++--- src/libAtomVM/context.h | 5 +++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index 1a6d362ce7..cc7d528f52 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -84,6 +84,7 @@ Context *context_new(GlobalContext *glb) ctx->has_min_heap_size = 0; ctx->has_max_heap_size = 0; ctx->active_alias_count = 0; + ctx->alias_count_saturated = 0; mailbox_init(&ctx->mailbox); @@ -887,7 +888,9 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) } case CONTEXT_MONITOR_ALIAS: { struct MonitorAlias *alias = CONTAINER_OF(monitor, struct MonitorAlias, monitor); - ctx->active_alias_count--; + if (LIKELY(!ctx->alias_count_saturated)) { + ctx->active_alias_count--; + } free(alias); break; } @@ -1090,7 +1093,15 @@ bool context_add_monitor(Context *ctx, struct Monitor *new_monitor) } list_append(&ctx->monitors_head, &new_monitor->monitor_list_head); if (new_monitor->monitor_type == CONTEXT_MONITOR_ALIAS) { - ctx->active_alias_count++; + if (LIKELY(ctx->active_alias_count < 0xFFFF)) { + ctx->active_alias_count++; + } else { + // Saturate instead of wrapping to 0, which would make context_find_alias skip the + // list walk and silently deactivate every alias of this process. Once saturated the + // count is pinned (see alias_count_saturated in context.h) and lookups are never + // skipped again for this process. + ctx->alias_count_saturated = 1; + } } return true; } @@ -1271,7 +1282,9 @@ void context_unalias(Context *ctx, struct MonitorAlias *alias) { TERM_DEBUG_ASSERT(alias != NULL); TERM_DEBUG_ASSERT(ctx->active_alias_count > 0); - ctx->active_alias_count--; + if (LIKELY(!ctx->alias_count_saturated)) { + ctx->active_alias_count--; + } struct Monitor *monitor = &alias->monitor; list_remove(&monitor->monitor_list_head); free(alias); diff --git a/src/libAtomVM/context.h b/src/libAtomVM/context.h index 5d8bb2bb62..6f52ba2fc4 100644 --- a/src/libAtomVM/context.h +++ b/src/libAtomVM/context.h @@ -140,7 +140,12 @@ struct Context unsigned int has_max_heap_size : 1; // Number of active aliases on monitors_head. Owner-written only, like the surrounding // bits; lets the alias lookups short-circuit for the common alias-free process. + // Saturating: when a 65536th alias is added the count sticks at 0xFFFF and + // alias_count_saturated is set, after which the count is never decremented again -- + // lookups are no longer skipped for this process, trading the optimization for + // correctness instead of wrapping to 0 and silently deactivating every alias. unsigned int active_alias_count : 16; + unsigned int alias_count_saturated : 1; bool trap_exit : 1; #ifndef AVM_NO_EMU From 5a1fdf80b661b6fd3e8ab9de48a9e3181184b5c8 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 10 Jun 2026 14:55:18 +0000 Subject: [PATCH 52/87] Deliver incoming distributed alias sends to the local alias Aliases are serialized as 3-word NEWER_REFERENCE_EXT, so a BEAM peer can legitimately hold an AtomVM alias and send to it, emitting DOP_ALIAS_SEND (33) or DOP_ALIAS_SEND_TT (34). Those operations hit the default arm of nif_erlang_dist_ctrl_put_data, which raises badarg in the distribution connection process: a single remote alias send could take the whole connection down instead of, at worst, losing one message. Handle both operations by routing through the existing alias delivery path: decoding our own alias yields a process reference (node and creation match), so the owner pid is in the reference itself and the message is posted with globalcontext_send_message_to_alias, where the owner validates the alias as for a local send. A reference that is not a local process reference (e.g. minted by a previous incarnation of this node) drops the message, exactly like a local send to a non-alias reference. Unlike a pid, the reference is a boxed term, so the control message is kept rooted across the payload decode. The trace token of DOP_ALIAS_SEND_TT is ignored (sequential tracing is not supported). The outbound direction is unchanged: sending to a remote alias from AtomVM is not routed over distribution. test_net_kernel gains test_alias_send_from_beam, which has a real OTP node send through an AtomVM alias (delivery and reference round-trip), then send again after unalias/1 (message dropped, connection survives). The test also passes unchanged on BEAM, pinning the OTP semantics it asserts. Signed-off-by: Davide Bettio --- src/libAtomVM/dist_nifs.c | 25 +++++++++++++++++ tests/libs/estdlib/test_net_kernel.erl | 38 ++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/libAtomVM/dist_nifs.c b/src/libAtomVM/dist_nifs.c index dff76b6b5a..6c4a09ec81 100644 --- a/src/libAtomVM/dist_nifs.c +++ b/src/libAtomVM/dist_nifs.c @@ -560,6 +560,31 @@ static term nif_erlang_dist_ctrl_put_data(Context *ctx, int argc, term argv[]) globalcontext_send_message(ctx->global, target_process_id, payload); break; } + case OPERATION_ALIAS_SEND: + case OPERATION_ALIAS_SEND_TT: { + // {DOP_ALIAS_SEND, FromPid, Alias} or {DOP_ALIAS_SEND_TT, FromPid, Alias, TraceToken}, + // followed by the message payload. The trace token is ignored. + if (UNLIKELY(arity != (term_to_int(operation) == OPERATION_ALIAS_SEND ? 3 : 4))) { + RAISE_ERROR(BADARG_ATOM); + } + term roots[3]; + roots[0] = argv[0]; + roots[1] = argv[1]; // dist handle, ensure it's not garbage collected until we return + // Unlike a pid, the alias reference is a boxed term: keep control rooted across the + // payload decode (which can GC) and re-read the alias from it afterwards. + roots[2] = control; + term payload = external_term_from_binary_with_roots(ctx, 1, 1 + bytes_read, &bytes_read, 3, roots); + control = roots[2]; + term target = term_get_tuple_element(control, 2); + if (LIKELY(term_is_process_reference(target))) { + int32_t target_process_id = term_process_ref_to_process_id(target); + globalcontext_send_message_to_alias(ctx->global, target_process_id, target, payload); + } + // else: not a local alias-shaped reference (e.g. minted by a previous incarnation of + // this node, so its creation did not match and it decoded as an external reference): + // drop the message, as a send to a reference that is not an active alias is dropped. + break; + } case OPERATION_SPAWN_REQUEST: { if (UNLIKELY(arity != 6)) { RAISE_ERROR(BADARG_ATOM); diff --git a/tests/libs/estdlib/test_net_kernel.erl b/tests/libs/estdlib/test_net_kernel.erl index f67f7c5417..560d73bc8b 100644 --- a/tests/libs/estdlib/test_net_kernel.erl +++ b/tests/libs/estdlib/test_net_kernel.erl @@ -44,6 +44,7 @@ test() -> ok = test_link_remote_exit_local(Platform), ok = test_link_local_unlink_remote(Platform), ok = test_link_local_unlink_local(Platform), + ok = test_alias_send_from_beam(Platform), ok = test_is_alive(), ok = test_ping_with_avm_dist_opts(Platform), ok; @@ -498,6 +499,43 @@ test_link_local_unlink_local(Platform) -> ok = stop_apply_loop(BeamMainPid, Pid, MonitorRef), ok. +%% A send to a remote alias emits DOP_ALIAS_SEND, which this node delivers to the +%% local alias. Only the inbound direction is exercised: sending to a remote alias +%% from AtomVM is not supported (the message is silently dropped). +test_alias_send_from_beam(Platform) -> + {BeamMainPid, Pid, MonitorRef} = start_apply_loop(Platform), + Alias = alias(), + %% The payload carries the alias itself, so the receive below also checks the + %% reference round-trips unchanged through the distribution encoding. + {via_alias, Alias} = call_apply_loop( + BeamMainPid, {self(), apply, erlang, send, [Alias, {via_alias, Alias}]} + ), + %% The alias send was emitted before the call_apply_loop reply on the same + %% connection, so it is already in our queue (skipped by the selective receive). + ok = + receive + {via_alias, Alias} -> ok + after 30000 -> alias_message_timeout + end, + true = unalias(Alias), + %% Inactive alias: the send must be silently dropped by this node, without + %% taking the connection down. The call_apply_loop round-trip is the fence: + %% the dropped message preceded the reply, so it can no longer show up. + should_be_dropped = call_apply_loop( + BeamMainPid, {self(), apply, erlang, send, [Alias, should_be_dropped]} + ), + %% Match alias messages only: on BEAM the suite may run in a process that traps + %% exits, so unrelated messages (e.g. stale 'EXIT' from a previous test's linked + %% helper) can sit in the queue. + ok = + receive + should_be_dropped -> unexpected_alias_message; + {via_alias, _} = Unexpected -> {unexpected_alias_message, Unexpected} + after 0 -> ok + end, + ok = stop_apply_loop(BeamMainPid, Pid, MonitorRef), + ok. + test_is_alive() -> false = is_alive(), {ok, _NetKernelPid} = net_kernel:start(atomvm, #{name_domain => shortnames}), From d6769dfb39ad7f2dedb91f98ba180c6af8a09dd6 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 10 Jun 2026 14:57:46 +0000 Subject: [PATCH 53/87] Document alias limitations and the REF_SIZE deprecation Behavior differences go to the differences-with-beam page: sending to a remote alias is not routed over distribution (the inbound direction works), and aliases are rejected as select handles where BEAM accepts them. The unsupported erlang:monitor/3 {tag, Term} option is already stated in the function documentation, and the builtin port-driver request/reply protocols are AtomVM-specific, so neither belongs on the differences page. The alias/1 documentation now states that the OTP 28 priority option is not supported and raises badarg, and the CHANGELOG records the REF_SIZE C macro deprecation with its replacements. Signed-off-by: Davide Bettio --- CHANGELOG.md | 4 ++++ doc/src/differences-with-beam.md | 13 +++++++++++++ libs/estdlib/src/erlang.erl | 1 + 3 files changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52544fff38..153b5f6113 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 longer lines return `{error, {parser, {line_too_long, Prefix}}}` with the first 128 bytes of the offending line. Callers whose upstream servers emit unusually large headers must account for this limit +- Deprecated the C macro `REF_SIZE`: use `TERM_BOXED_REFERENCE_SHORT_SIZE` for references built + from ref ticks, `TERM_BOXED_REFERENCE_PROCESS_SIZE` for process references (aliases), or + `TERM_BOXED_REFERENCE_MAX_SIZE` to fit any reference. `REF_SIZE` still expands to the short + reference size, but now emits a compiler warning ### Removed - Removed `ahttp_client` support for obsolete line folding (RFC 9112 §5.2); folded header and diff --git a/doc/src/differences-with-beam.md b/doc/src/differences-with-beam.md index 687af7ee04..635383fb7a 100644 --- a/doc/src/differences-with-beam.md +++ b/doc/src/differences-with-beam.md @@ -170,6 +170,19 @@ features such as node monitoring are not implemented yet. It is currently possible to connect a BEAM node with an AtomVM node. +### Process aliases + +Process aliases (`alias/0`, `alias/1`, `unalias/1` and the `{alias, Mode}` option of +`erlang:monitor/3`) are supported with OTP ≥ 26 semantics, with two differences: + +- Sending to a remote alias is not supported: a message sent from AtomVM to an alias (a +reference) of another node is silently dropped instead of being routed over distribution. The +other direction works: a message sent from a remote BEAM node to an alias of an AtomVM process is +delivered. +- An alias is not accepted as a select handle: `socket` `nowait` operations and POSIX file selects +raise `badarg` when given an alias reference, where BEAM accepts it and echoes it back in the +select notification. + ## Known limitations of the standard library AtomVM standard library is extremely limited and while programs written for AtomVM can be run diff --git a/libs/estdlib/src/erlang.erl b/libs/estdlib/src/erlang.erl index 09c7c41339..3bdc64c1a6 100644 --- a/libs/estdlib/src/erlang.erl +++ b/libs/estdlib/src/erlang.erl @@ -2205,6 +2205,7 @@ alias() -> %% With `explicit_unalias' (the default, so `alias([])' is `alias/0') %% the alias stays active until `unalias/1'; with `reply' it is %% deactivated when the first message sent via the alias is delivered. +%% The OTP 28 `priority' option is not supported and raises `badarg'. %% @end %%----------------------------------------------------------------------------- -spec alias(Options) -> Alias when Options :: [explicit_unalias | reply], Alias :: reference(). From ba610a2d01db5df4e2f641adab99f94c987d04f5 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Thu, 11 Jun 2026 11:28:01 +0000 Subject: [PATCH 54/87] Process the outer mailbox list in one pass when no alias is active The received-order split added for process aliases reverses the LIFO outer list and then walks it a second time, so that alias side effects run in received order (deactivating the alias, and resolving several sends to the same alias in one batch oldest-first). That second traversal only matters when the owner actually holds an active alias, yet it ran for every signal drain of every process -- including the alias-free processes that are the common case on the embedded targets, and the ctx == NULL scheduler and termination callers that can never own an alias. Gate it on active_alias_count: when it is zero (or there is no owner context), split the outer list into the normal and signal sublists in a single prepend pass, exactly as before aliases existed. active_alias_count is written only by the owner and this runs in the owner's own context, so it is stable for the whole batch and the choice is race-free. An AliasMessageSignal reached on this path necessarily targets an inactive alias: drop it when we own a context (it must never reach the signal loop, which treats it as unreachable), or leave it for the ctx == NULL caller to ignore. The full received-order two-pass is kept unchanged for a process that holds an active alias. No behavior change: the fast path produces the same received-order sublists the two-pass produced for an alias-free batch. Signed-off-by: Davide Bettio --- src/libAtomVM/mailbox.c | 172 ++++++++++++++++++++++++---------------- 1 file changed, 102 insertions(+), 70 deletions(-) diff --git a/src/libAtomVM/mailbox.c b/src/libAtomVM/mailbox.c index 7782fa0f5f..0b87333c3d 100644 --- a/src/libAtomVM/mailbox.c +++ b/src/libAtomVM/mailbox.c @@ -379,89 +379,121 @@ static MailboxMessage *process_outer_list(Context *ctx, Mailbox *mbox) #else mbox->outer_first = NULL; #endif - // The outer list is LIFO (outer_first is the newest message). Reverse it into received order - // (oldest first) before doing anything else: this is pure pointer manipulation, with no side - // effects and no allocation. Alias side effects (which can deactivate the alias, e.g. - // reply_demonitor) must run in received order, so that when several messages target the same - // alias in one batch, the first received one is delivered and the later ones dropped, like OTP. - MailboxMessage *received = NULL; - while (current) { - MailboxMessage *next = current->next; - current->next = received; - received = current; - current = next; - } - - // Walk received order (oldest to newest), appending to the normal and signal sublists so both - // keep received order. + // The outer list is LIFO (outer_first is the newest message); split it into the normal and + // signal sublists, both in received order (oldest first). MailboxMessage *normal_first = NULL; MailboxMessage *normal_last = NULL; MailboxMessage *signal_first = NULL; - MailboxMessage *signal_last = NULL; - current = received; - while (current) { - MailboxMessage *next = current->next; - if (current->type == NormalMessage) { - current->next = NULL; - if (normal_last == NULL) { + + if (ctx == NULL || ctx->active_alias_count == 0) { + // Fast path, and the common case: the owner has no active alias (or there is no owner + // context, as for ports and at termination), so no alias message can be delivered and + // nothing has to run in received order. Reverse the LIFO list into received order in a + // SINGLE pass, by prepending to each sublist while walking newest-to-oldest. + // active_alias_count is written only by the owner and this runs in the owner's own context, + // so it is stable for the whole batch. An AliasMessageSignal here targets an inactive alias: + // drop it when we own a context (it must never reach the signal loop, which treats + // AliasMessageSignal as unreachable); when ctx is NULL leave it in the signal list for the + // scheduler/termination caller to ignore. + while (current) { + MailboxMessage *next = current->next; + if (ctx != NULL && current->type == AliasMessageSignal) { + mailbox_message_dispose(current, &ctx->heap); + } else if (current->type == NormalMessage) { + if (normal_last == NULL) { + normal_last = current; + } + current->next = normal_first; normal_first = current; } else { - normal_last->next = current; + current->next = signal_first; + signal_first = current; } - normal_last = current; - } else if (ctx != NULL && current->type == AliasMessageSignal) { - // Convert an alias message to a normal message, validating the alias in the owner's own - // context. Walking in received order keeps an alias send ordered with plain sends from - // the same sender (alias and pid sends must not be reordered), and resolves repeated - // sends to the same alias oldest-first. - term message = context_process_alias_message_signal(ctx, CONTAINER_OF(current, struct TermSignal, base)); - if (!term_is_invalid_term(message)) { - // Re-type the signal as a normal message in place: struct TermSignal and struct - // Message share an identical layout (the _Static_asserts above) and the message term - // already lives in this signal's own storage, so nothing is copied. Being - // allocation-free, the conversion can no longer drop a message on out of memory -- - // which matters for reply_demonitor, whose alias deactivation and DemonitorSignal - // have already run inside context_process_alias_message_signal. - Message *converted = CONTAINER_OF(current, Message, base); - converted->base.type = NormalMessage; - converted->message = message; - converted->base.next = NULL; + current = next; + } + } else { + // The owner has at least one active alias, so alias side effects (which can deactivate the + // alias, e.g. reply_demonitor) must run in received order, so that when several messages + // target the same alias in one batch, the first received one is delivered and the later ones + // dropped, like OTP. Reverse the LIFO outer list (newest first) into received order before + // doing anything else: this is pure pointer manipulation, with no side effects and no + // allocation. + MailboxMessage *received = NULL; + while (current) { + MailboxMessage *next = current->next; + current->next = received; + received = current; + current = next; + } + + // Walk received order (oldest to newest), appending to the normal and signal sublists so + // both keep received order. + MailboxMessage *signal_last = NULL; + current = received; + while (current) { + MailboxMessage *next = current->next; + if (current->type == NormalMessage) { + current->next = NULL; if (normal_last == NULL) { - normal_first = &converted->base; + normal_first = current; } else { - normal_last->next = &converted->base; + normal_last->next = current; } - normal_last = &converted->base; - } else { - // Inactive alias: drop the message and free the signal. - mailbox_message_dispose(current, &ctx->heap); - } - } else { - // A 'DOWN' that auto-removes a {alias, demonitor} / {alias, reply_demonitor} monitor also - // deactivates its alias (see context_process_monitor_down_signal). Apply just that - // deactivation here, in received order, so an alias send arriving later in this SAME batch - // is dropped like OTP: the alias messages above are converted while the outer list is - // split, before the signal loop ever runs this 'DOWN'. The rest of the 'DOWN' (monitor - // removal, delivering the message) still happens when the signal loop calls - // context_process_monitor_down_signal; context_find_alias returns NULL there by then, so - // that deactivation is an idempotent no-op. - if (ctx != NULL && current->type == MonitorDownSignal) { - struct TermSignal *down_signal = CONTAINER_OF(current, struct TermSignal, base); - uint64_t ref_ticks = term_to_ref_ticks(term_get_tuple_element(down_signal->signal_term, 1)); - struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); - if (alias != NULL && alias->alias_type != ContextMonitorAliasExplicitUnalias) { - context_unalias(ctx, alias); + normal_last = current; + } else if (current->type == AliasMessageSignal) { + // Convert an alias message to a normal message, validating the alias in the owner's + // own context. Walking in received order keeps an alias send ordered with plain + // sends from the same sender (alias and pid sends must not be reordered), and + // resolves repeated sends to the same alias oldest-first. + term message = context_process_alias_message_signal(ctx, CONTAINER_OF(current, struct TermSignal, base)); + if (!term_is_invalid_term(message)) { + // Re-type the signal as a normal message in place: struct TermSignal and struct + // Message share an identical layout (the _Static_asserts above) and the message + // term already lives in this signal's own storage, so nothing is copied. Being + // allocation-free, the conversion can no longer drop a message on out of memory + // -- which matters for reply_demonitor, whose alias deactivation and + // DemonitorSignal have already run inside context_process_alias_message_signal. + Message *converted = CONTAINER_OF(current, Message, base); + converted->base.type = NormalMessage; + converted->message = message; + converted->base.next = NULL; + if (normal_last == NULL) { + normal_first = &converted->base; + } else { + normal_last->next = &converted->base; + } + normal_last = &converted->base; + } else { + // Inactive alias: drop the message and free the signal. + mailbox_message_dispose(current, &ctx->heap); } - } - current->next = NULL; - if (signal_last == NULL) { - signal_first = current; } else { - signal_last->next = current; + // A 'DOWN' that auto-removes a {alias, demonitor} / {alias, reply_demonitor} monitor + // also deactivates its alias (see context_process_monitor_down_signal). Apply just + // that deactivation here, in received order, so an alias send arriving later in this + // SAME batch is dropped like OTP: the alias messages above are converted while the + // outer list is split, before the signal loop ever runs this 'DOWN'. The rest of the + // 'DOWN' (monitor removal, delivering the message) still happens when the signal loop + // calls context_process_monitor_down_signal; context_find_alias returns NULL there by + // then, so that deactivation is an idempotent no-op. + if (current->type == MonitorDownSignal) { + struct TermSignal *down_signal = CONTAINER_OF(current, struct TermSignal, base); + uint64_t ref_ticks = term_to_ref_ticks(term_get_tuple_element(down_signal->signal_term, 1)); + struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); + if (alias != NULL && alias->alias_type != ContextMonitorAliasExplicitUnalias) { + context_unalias(ctx, alias); + } + } + current->next = NULL; + if (signal_last == NULL) { + signal_first = current; + } else { + signal_last->next = current; + } + signal_last = current; } - signal_last = current; + current = next; } - current = next; } // If we enqueued some normal messages, normal_first is the head (oldest received) and From 30509f03e7d5f7349fbcccda8c6ab9208957e289 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Thu, 11 Jun 2026 18:05:37 +0000 Subject: [PATCH 55/87] Reserve the full reply size in the console gen_call error paths The alias support reduced the console handler's heap reservation from a conservative 12 terms to TUPLE_SIZE(3), matching the io_reply 3-tuple once the verbatim ReplyAs echo removed the separate ref allocation. But the gen_call paths build their error reply as an error 2-tuple nested inside the reply 2-tuple (port_create_error_tuple + port_send_reply), i.e. TUPLE_SIZE(2) + TUPLE_SIZE(2) = 6 terms, allocated on ctx's heap by helpers that do not run their own memory_ensure_free. Reserving only 4 terms under-allocates by 2: the writes are masked by heap slack today, but violate the ensure_free contract and corrupt the heap whenever it is tight (e.g. after a MEMORY_CAN_SHRINK shrink to the requested size). Reserve 2 * TUPLE_SIZE(2), the largest reply the handler builds (more than the io_reply 3-tuple and the close 2-tuple). Flagged by CodeQL's allocation-exceeding-ensure_free query. Signed-off-by: Davide Bettio --- src/libAtomVM/nifs.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index b2995ab802..b02822ad56 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1335,7 +1335,11 @@ static NativeHandlerResult process_console_message(Context *ctx, term msg) { // msg is not in the port's heap NativeHandlerResult result = NativeContinue; - if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(3), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + // Reserve for the largest reply this handler builds on ctx's heap. A gen_call error reply nests + // an error 2-tuple inside the reply 2-tuple (TUPLE_SIZE(2) + TUPLE_SIZE(2) = 6 terms), which is + // more than the io_reply 3-tuple or the close 2-tuple. The port reply helpers allocate without + // their own ensure_free, so this single budget has to cover the whole worst-case path. + if (UNLIKELY(memory_ensure_free_opt(ctx, 2 * TUPLE_SIZE(2), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { fprintf(stderr, "Unable to allocate sufficient memory for console driver.\n"); AVM_ABORT(); } From 230fb29db9e3832c94d69a7a491ec7729d8ea1f8 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Fri, 12 Jun 2026 15:17:06 +0000 Subject: [PATCH 56/87] Migrate the USB CDC drivers off the deprecated REF_SIZE The alias work renamed REF_SIZE to TERM_BOXED_REFERENCE_SHORT_SIZE and left REF_SIZE as a deprecation shim that emits a GCC diagnostic on every use. The libAtomVM tree was fully migrated, but the three USB CDC drivers were not: ten uses remained, so every esp32, stm32 and rp2 build gained deprecation warnings, turning into hard errors when AVM_WARNINGS_ARE_ERRORS is enabled (the drivers inherit libAtomVM's compile options). The generic_unix builds never compile these drivers, which is how the leftovers survived every native test pass. All ten uses size reply allocations whose reference is built with term_from_ref_ticks, i.e. a plain short reference, so TERM_BOXED_REFERENCE_SHORT_SIZE is the exact replacement. The shim expands to that same macro, so the generated code is unchanged and the deprecation is left with zero in-tree users. Flagged by the pre-merge review, which confirmed with a standalone probe that the pragma fires in exactly these expression contexts and becomes a hard error under -Werror. Signed-off-by: Davide Bettio --- .../esp32/components/avm_builtins/usb_cdc_driver.c | 8 ++++---- src/platforms/rp2/src/lib/usb_cdc_driver.c | 6 +++--- src/platforms/stm32/src/lib/usb_cdc_driver.c | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/platforms/esp32/components/avm_builtins/usb_cdc_driver.c b/src/platforms/esp32/components/avm_builtins/usb_cdc_driver.c index af5701ad70..365cd6bb14 100644 --- a/src/platforms/esp32/components/avm_builtins/usb_cdc_driver.c +++ b/src/platforms/esp32/components/avm_builtins/usb_cdc_driver.c @@ -169,7 +169,7 @@ EventListener *usb_cdc_interrupt_callback(GlobalContext *glb, EventListener *lis size_t rx_size = 0; esp_err_t ret = tinyusb_cdcacm_read(cdc_data->itf, buf, sizeof(buf), &rx_size); if (ret != ESP_OK || rx_size == 0) { - BEGIN_WITH_STACK_HEAP(REF_SIZE + TUPLE_SIZE(2) * 2, err_heap) + BEGIN_WITH_STACK_HEAP(TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2) * 2, err_heap) term ref = term_from_ref_ticks(reader_ref_ticks, &err_heap); term error_tuple = term_alloc_tuple(2, &err_heap); term_put_tuple_element(error_tuple, 0, ERROR_ATOM); @@ -186,7 +186,7 @@ EventListener *usb_cdc_interrupt_callback(GlobalContext *glb, EventListener *lis int local_pid = term_to_local_process_id(reader_pid); Heap heap; - if (UNLIKELY(memory_init_heap(&heap, bin_size + REF_SIZE + TUPLE_SIZE(2) * 2) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_init_heap(&heap, bin_size + TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2) * 2) != MEMORY_GC_OK)) { fprintf(stderr, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); return listener; @@ -578,7 +578,7 @@ static void usb_cdc_driver_do_close(Context *ctx, GenMessage gen_message) SMP_MUTEX_UNLOCK(cdc_data->reader_lock); if (pending_reader_pid != term_invalid_term()) { - BEGIN_WITH_STACK_HEAP(REF_SIZE + TUPLE_SIZE(2) * 2, heap) + BEGIN_WITH_STACK_HEAP(TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2) * 2, heap) term error_tuple = term_alloc_tuple(2, &heap); term_put_tuple_element(error_tuple, 0, ERROR_ATOM); term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(glb, ATOM_STR("\x6", "closed"))); @@ -623,7 +623,7 @@ static NativeHandlerResult usb_cdc_driver_consume_mailbox(Context *ctx) int local_pid = term_to_local_process_id(gen_message.pid); if (is_closed) { - if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2) * 2 + REF_SIZE) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2) * 2 + TERM_BOXED_REFERENCE_SHORT_SIZE) != MEMORY_GC_OK)) { ESP_LOGE(TAG, "Failed to allocate space for error tuple"); globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); } else { diff --git a/src/platforms/rp2/src/lib/usb_cdc_driver.c b/src/platforms/rp2/src/lib/usb_cdc_driver.c index 89a11a2bee..2569dda785 100644 --- a/src/platforms/rp2/src/lib/usb_cdc_driver.c +++ b/src/platforms/rp2/src/lib/usb_cdc_driver.c @@ -150,7 +150,7 @@ static EventListener *usb_cdc_listener_handler(GlobalContext *glb, EventListener int bin_size = term_binary_heap_size(rx_size); Heap heap; - if (UNLIKELY(memory_init_heap(&heap, bin_size + REF_SIZE + TUPLE_SIZE(2) * 2) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_init_heap(&heap, bin_size + TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2) * 2) != MEMORY_GC_OK)) { fprintf(stderr, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); AVM_ABORT(); } @@ -464,7 +464,7 @@ static void usb_cdc_driver_do_close(Context *ctx, GenMessage gen_message) SMP_MUTEX_UNLOCK(cdc_data->reader_lock); if (pending_reader_pid != term_invalid_term()) { - BEGIN_WITH_STACK_HEAP(REF_SIZE + TUPLE_SIZE(2) * 2, heap) + BEGIN_WITH_STACK_HEAP(TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2) * 2, heap) term error_tuple = term_alloc_tuple(2, &heap); term_put_tuple_element(error_tuple, 0, ERROR_ATOM); term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(glb, ATOM_STR("\x6", "closed"))); @@ -508,7 +508,7 @@ static NativeHandlerResult usb_cdc_driver_consume_mailbox(Context *ctx) int local_pid = term_to_local_process_id(gen_message.pid); if (is_closed) { - if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2) * 2 + REF_SIZE) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2) * 2 + TERM_BOXED_REFERENCE_SHORT_SIZE) != MEMORY_GC_OK)) { fprintf(stderr, "usb_cdc: Failed to allocate error tuple\n"); globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); } else { diff --git a/src/platforms/stm32/src/lib/usb_cdc_driver.c b/src/platforms/stm32/src/lib/usb_cdc_driver.c index fe53f87431..a5d9af0672 100644 --- a/src/platforms/stm32/src/lib/usb_cdc_driver.c +++ b/src/platforms/stm32/src/lib/usb_cdc_driver.c @@ -119,7 +119,7 @@ static void usb_cdc_check_rx(struct USBCDCData *cdc_data) int bin_size = term_binary_heap_size(rx_size); Heap heap; - if (UNLIKELY(memory_init_heap(&heap, bin_size + REF_SIZE + TUPLE_SIZE(2) * 2) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_init_heap(&heap, bin_size + TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2) * 2) != MEMORY_GC_OK)) { fprintf(stderr, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); AVM_ABORT(); } @@ -367,7 +367,7 @@ static void usb_cdc_driver_do_close(Context *ctx, GenMessage gen_message) cdc_data->reader_process_pid = term_invalid_term(); if (pending_reader_pid != term_invalid_term()) { - BEGIN_WITH_STACK_HEAP(REF_SIZE + TUPLE_SIZE(2) * 2, heap) + BEGIN_WITH_STACK_HEAP(TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2) * 2, heap) term error_tuple = term_alloc_tuple(2, &heap); term_put_tuple_element(error_tuple, 0, ERROR_ATOM); term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(glb, ATOM_STR("\x6", "closed"))); @@ -436,7 +436,7 @@ static NativeHandlerResult usb_cdc_driver_consume_mailbox(Context *ctx) if (is_closed) { GlobalContext *glb = ctx->global; - if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2) * 2 + REF_SIZE) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2) * 2 + TERM_BOXED_REFERENCE_SHORT_SIZE) != MEMORY_GC_OK)) { fprintf(stderr, "usb_cdc: Failed to allocate error tuple\n"); globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); } else { From 533ca189e01aa4f4e0df9030ada1716a7e6f557d Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Fri, 12 Jun 2026 15:24:46 +0000 Subject: [PATCH 57/87] Move the remote alias limitation to the distributed Erlang page The differences-with-beam page documents core divergences from the BEAM, and the process alias section was not one: its remaining substance is a distribution limitation. Move it to a new Known Issues & Limitations section of the distributed Erlang page, which now carries the only user-facing note: an outbound send to a remote alias is dropped, while inbound delivery from a BEAM peer works. The page-closing invitation to file issues or pull requests moves below the new section, so it keeps closing the page and now reads on both the partial feature list and the known issues. Dropped in the move: the select-handle rejection (an exotic corner that is not worth user-facing documentation -- it remains commented at both validation sites and covered by tests) and the OTP >= 26 semantics claim. The differences-with-beam page is back to its release-0.7 content. Signed-off-by: Davide Bettio --- doc/src/differences-with-beam.md | 13 ------------- doc/src/distributed-erlang.md | 7 +++++++ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/doc/src/differences-with-beam.md b/doc/src/differences-with-beam.md index 635383fb7a..687af7ee04 100644 --- a/doc/src/differences-with-beam.md +++ b/doc/src/differences-with-beam.md @@ -170,19 +170,6 @@ features such as node monitoring are not implemented yet. It is currently possible to connect a BEAM node with an AtomVM node. -### Process aliases - -Process aliases (`alias/0`, `alias/1`, `unalias/1` and the `{alias, Mode}` option of -`erlang:monitor/3`) are supported with OTP ≥ 26 semantics, with two differences: - -- Sending to a remote alias is not supported: a message sent from AtomVM to an alias (a -reference) of another node is silently dropped instead of being routed over distribution. The -other direction works: a message sent from a remote BEAM node to an alias of an AtomVM process is -delivered. -- An alias is not accepted as a select handle: `socket` `nowait` operations and POSIX file selects -raise `badarg` when given an alias reference, where BEAM accepts it and echoes it back in the -select notification. - ## Known limitations of the standard library AtomVM standard library is extremely limited and while programs written for AtomVM can be run diff --git a/doc/src/distributed-erlang.md b/doc/src/distributed-erlang.md index 7d7c152365..05d2a9b87b 100644 --- a/doc/src/distributed-erlang.md +++ b/doc/src/distributed-erlang.md @@ -325,4 +325,11 @@ RPC (remote procedure call) from Erlang/OTP to AtomVM is also supported. Shell requires several OTP standard library modules. See [the example project](https://github.com/pguyot/atomvm_shell). +## Known Issues & Limitations + +- Sending to a remote process alias is not supported: a message sent from AtomVM to an alias + (a reference) of another node is silently dropped instead of being routed over distribution. + The other direction works: a message sent from a remote BEAM node to an alias of an AtomVM + process is delivered. + Please do not hesitate to file issues or pull requests for additional features. From 514d1d59b4bcbe78e1ff7d40bbf7b6609c044a97 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Fri, 12 Jun 2026 16:07:42 +0000 Subject: [PATCH 58/87] Add alias coverage for dead owners, sender races and decode bounds Five tests closing the review's coverage gaps (AR-10): - test_alias_send_after_owner_died: a send through the alias of a dead process is dropped, still returns the message, and surfaces nowhere; the spawn churn probes would report a misdelivery. AtomVM assigns process ids monotonically so the dead owner's id is not reused, and globally-unique ref ticks are the invariant that keeps a stale alias unmatchable even by an id reuser. - test_alias_multi_sender_unalias: four senders hammer one alias while the owner unaliases mid-stream. All synchronization is per-sender send order and explicit acks -- no sleeps and no timing windows -- so the test cannot flake on slow or loaded hosts. Phase 1 pins the exact delivered count behind per-sender fences while the alias is active; phase 2 races unalias/1 against the senders and pins no crash, a bounded count and an observably dead alias afterwards. - test_alias_duplicate_options: alias/1 duplicate options are last-wins in both orders, like OTP 29 (the monitor/3 duplicate was already covered). - test_unalias_non_reference_badarg: unalias(42) raises badarg, like OTP. - test_binary_to_term_invalid_process_ref: a wire-format alias with its owner pid word patched to 0 or above the 28-bit maximum must fail to decode -- the untrusted-input validation this branch added to the decoder, previously untested. The BEAM does not read reference words as a pid and decodes plain references there; the test asserts that instead. All five pass on both flavors and on the BEAM (OTP 29) via -b; the module was looped 30x on each flavor (0 failures) and runs under valgrind with 0 errors. Signed-off-by: Davide Bettio --- tests/erlang_tests/test_monitor.erl | 170 ++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index bd2dfbfcf9..12c9f600ca 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -69,6 +69,11 @@ start() -> ok = test_monitor_alias_registered_self_installs_nothing(), ok = test_alias_1(), ok = test_alias_reply_mode(), + ok = test_alias_send_after_owner_died(), + ok = test_alias_multi_sender_unalias(), + ok = test_alias_duplicate_options(), + ok = test_unalias_non_reference_badarg(), + ok = test_binary_to_term_invalid_process_ref(), 0. test_monitor_normal() -> @@ -685,6 +690,171 @@ test_alias_reply_mode() -> false = unalias(A), ok. +%% A send to the alias of a process that no longer exists is dropped and still returns the +%% message, like a send to any non-alias reference. AtomVM assigns process ids monotonically, +%% so the dead owner's id is not reused, and the globally-unique ref ticks would make the +%% stale alias unmatchable even by a process reusing the id: the churn probes pin that no +%% message surfaces anywhere and nothing crashes. +test_alias_send_after_owner_died() -> + Parent = self(), + {P, Fence} = spawn_opt(fun() -> Parent ! {alias, erlang:alias()} end, [monitor]), + {alias, A} = recv_one(), + {'DOWN', Fence, process, P, normal} = recv_one(), + hello = (A ! hello), + ok = churn_and_send_stale(A, 20), + ok = assert_no_message(), + ok. + +churn_and_send_stale(_A, 0) -> + ok; +churn_and_send_stale(A, N) -> + Parent = self(), + {Q, Mon} = spawn_opt(fun() -> stale_alias_probe(Parent) end, [monitor]), + drop = (A ! drop), + Q ! quit, + {'DOWN', Mon, process, Q, normal} = recv_one(), + churn_and_send_stale(A, N - 1). + +%% Reports any message other than the expected quit: a stale-alias message must never surface +%% here as a plain message (its signal is dropped against this process's empty alias list). +stale_alias_probe(Parent) -> + receive + quit -> + ok; + Other -> + Parent ! {misdelivered, Other}, + stale_alias_probe(Parent) + end. + +%% Several senders hammer one alias while the owner unaliases mid-stream. All synchronization +%% is per-sender send order and explicit acks -- no sleeps and no timing windows, so the test +%% cannot flake on slow or loaded hosts. Phase 1 pins exact delivery while the alias is +%% active: a sender's alias messages all precede its sent_all fence in the owner's queue, so +%% once the last fence is consumed every alias message has been counted. Phase 2 races +%% unalias/1 against the senders and pins what survives the race: no crash, a bounded +%% delivery count, and an observably dead alias afterwards. +test_alias_multi_sender_unalias() -> + NSenders = 4, + NMsgs = 25, + A = erlang:alias(), + Senders = spawn_alias_senders(self(), A, NMsgs, NSenders), + ok = send_to_each(Senders, go), + Count1 = drain_alias_msgs(NSenders, sent_all, 0), + Count1 = NSenders * NMsgs, + ok = send_to_each(Senders, go2), + true = erlang:unalias(A), + Count2 = drain_alias_msgs(NSenders, sent_all2, 0), + true = Count2 =< NSenders * NMsgs, + false = erlang:unalias(A), + dead = (A ! dead), + ok = assert_no_message(), + ok. + +spawn_alias_senders(_Owner, _A, _NMsgs, 0) -> + []; +spawn_alias_senders(Owner, A, NMsgs, K) -> + Pid = spawn_opt(fun() -> alias_sender(Owner, A, NMsgs) end, []), + [Pid | spawn_alias_senders(Owner, A, NMsgs, K - 1)]. + +alias_sender(Owner, A, NMsgs) -> + receive + go -> ok + end, + ok = alias_blast(A, NMsgs), + Owner ! sent_all, + receive + go2 -> ok + end, + ok = alias_blast(A, NMsgs), + Owner ! sent_all2, + ok. + +alias_blast(_A, 0) -> + ok; +alias_blast(A, N) -> + {am, N} = (A ! {am, N}), + alias_blast(A, N - 1). + +send_to_each([], _Msg) -> + ok; +send_to_each([Pid | Rest], Msg) -> + Pid ! Msg, + send_to_each(Rest, Msg). + +drain_alias_msgs(0, _FenceMsg, Count) -> + Count; +drain_alias_msgs(FencesLeft, FenceMsg, Count) -> + case recv_one() of + {am, _} -> drain_alias_msgs(FencesLeft, FenceMsg, Count + 1); + FenceMsg -> drain_alias_msgs(FencesLeft - 1, FenceMsg, Count); + Other -> {unexpected, Other} + end. + +%% Duplicate alias/1 options: the last one wins, like OTP 29 (probed in both orders) and like +%% the monitor/3 duplicate {alias, _} options. +test_alias_duplicate_options() -> + A1 = alias([explicit_unalias, reply]), + A1 ! r1, + A1 ! r2, + r1 = recv_one(), + ok = assert_no_message(), + false = unalias(A1), + A2 = alias([reply, explicit_unalias]), + A2 ! e1, + A2 ! e2, + e1 = recv_one(), + e2 = recv_one(), + true = unalias(A2), + ok. + +%% unalias/1 raises badarg on a non-reference argument, like OTP. +test_unalias_non_reference_badarg() -> + ok = + try unalias(42) of + R -> {unexpected, R} + catch + error:badarg -> ok + end, + ok. + +%% The owner pid word of a wire-format alias (len-3 NEWER_REFERENCE_EXT) is untrusted input: +%% decoding must reject pid 0 (INVALID_PROCESS_ID, the short-ref sentinel) and pids above the +%% 28-bit maximum. Not a BEAM divergence any real term can show: term_to_binary can never +%% produce these bytes, and the BEAM treats reference words as opaque payload while AtomVM +%% gives the third word pid semantics -- so only AtomVM has anything to validate. On the BEAM +%% the patched binaries decode as plain references, which the test asserts there. +test_binary_to_term_invalid_process_ref() -> + A = erlang:alias(), + B = term_to_binary(A), + A = binary_to_term(B), + PrefixSize = byte_size(B) - 4, + <> = B, + TooBigPid = 1 bsl 28, + BadZero = <>, + BadBig = <>, + case erlang:system_info(machine) of + "BEAM" -> + true = is_reference(binary_to_term(BadZero)), + true = is_reference(binary_to_term(BadBig)); + _ -> + %% On AtomVM the alias serializes as a len-3 reference whose last word is the pid. + <<131, 90, 3:16, _/binary>> = B, + ok = + try binary_to_term(BadZero) of + R1 -> {unexpected, R1} + catch + error:badarg -> ok + end, + ok = + try binary_to_term(BadBig) of + R2 -> {unexpected, R2} + catch + error:badarg -> ok + end + end, + true = erlang:unalias(A), + ok. + spawn_opt_monitor(LoopFun, Opts) -> spawn_opt(LoopFun, [{monitor, Opts}]). From 2178a9de7ca3527757c32af5927476cad0585031 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Fri, 12 Jun 2026 16:25:01 +0000 Subject: [PATCH 59/87] Return int32_t from the process reference pid accessor Process ids are int32_t throughout (Context.process_id, RefData.process_id, and every assignment of this accessor's result), but term_process_ref_to_process_id returned uint32_t. Return int32_t, making the six int32_t call sites exact; the remaining sites store the always-positive, 28-bit-bounded value into uint32_t wire words, where the conversion is well-defined and unchanged. Also from the review's style pass: drop the stray blank line in the REFERENCE_PROCESS_PID_OFFSET conditional (the neighboring blocks are compact), fix "an unique" in the term_make_process_reference doc, and state the load-bearing layout invariant there: the ticks occupy the same words as in a short reference, so term_to_ref_ticks works on both shapes. The remaining nits from the same review pass were deliberately not taken: explicit casts in term_make_process_reference would decorate conversions that are well-defined and always in range; initializing the parse_monitor_opts alias_type out-param adds a line for a latent-only risk; the context_dump %lu ticks truncation matches the surrounding pre-existing lines and is best fixed together with them. Signed-off-by: Davide Bettio --- src/libAtomVM/term.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libAtomVM/term.h b/src/libAtomVM/term.h index f5da9a1fb7..60325ebbf7 100644 --- a/src/libAtomVM/term.h +++ b/src/libAtomVM/term.h @@ -195,7 +195,6 @@ _Static_assert(TERM_BOXED_REFERENCE_RESOURCE_SIZE <= TERM_BOXED_REFERENCE_MAX_SI #if TERM_BYTES == 4 #define REFERENCE_PROCESS_PID_OFFSET 3 - #elif TERM_BYTES == 8 #define REFERENCE_PROCESS_PID_OFFSET 2 #endif @@ -2263,10 +2262,11 @@ static inline uint64_t term_to_ref_ticks(term rt) /** * @brief Creates a process reference * @details Process reference contains ref_ticks and process_id of a process. - * They are used by process aliases and monitors. + * They are used by process aliases and monitors. The ticks occupy the same + * words as in a short reference, so term_to_ref_ticks works on both shapes. * * @param process_id process_id of a process that the reference will identify. - * @param ref_ticks an unique uint64 value that will be used to create ref term. + * @param ref_ticks a unique uint64 value that will be used to create ref term. * @param heap the heap to allocate memory in * @return a ref term created using given ref ticks. */ @@ -2302,11 +2302,11 @@ static inline term term_make_process_reference(int32_t process_id, uint64_t ref_ * @param rt the process reference term * @return the process id of the process the reference identifies */ -static inline uint32_t term_process_ref_to_process_id(term rt) +static inline int32_t term_process_ref_to_process_id(term rt) { TERM_DEBUG_ASSERT(term_is_process_reference(rt)); const term *boxed_value = term_to_const_term_ptr(rt); - return (uint32_t) boxed_value[REFERENCE_PROCESS_PID_OFFSET]; + return (int32_t) boxed_value[REFERENCE_PROCESS_PID_OFFSET]; } /** From e328f0bb780c009de7757862d1cf10a984a2dcbe Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Fri, 12 Jun 2026 16:40:33 +0000 Subject: [PATCH 60/87] Free dropped alias messages instead of deferring them to GC A message sent to an inactive alias is rejected at the owner's mailbox drain, but both drop sites disposed it with mailbox_message_dispose, which appends the signal storage to the receiver's heap as a fragment: the dead {Ref, Message} tree -- including any refc binaries it pins -- stayed on the victim's heap until its next GC. OTP drops such messages with no receiver heap impact, so a flood of sends to a deactivated alias produced AtomVM-only GC pressure, with the receiver paying for messages it never accepted. The deferred reclamation is only needed for delivered messages, whose terms may still be referenced after a receive. A dropped alias message was never delivered, so nothing can reference its term: free it immediately with mailbox_message_dispose_unsent (which sweeps the mso_list, releasing refc binaries), exactly the treatment unsent messages already get. TermSignal and Message share the layout already asserted for the in-place conversion, and both drop sites run in the owner's own context (from_task = false). The reply-mode and duplicate-option tests now also push a >64-byte binary through each drop path, so the refc sweep is exercised under valgrind and ASAN. Flagged by the pre-merge review (round 2, issue 4). Signed-off-by: Davide Bettio --- src/libAtomVM/mailbox.c | 12 ++++++++---- tests/erlang_tests/test_monitor.erl | 4 ++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/libAtomVM/mailbox.c b/src/libAtomVM/mailbox.c index 0b87333c3d..9316c89d9f 100644 --- a/src/libAtomVM/mailbox.c +++ b/src/libAtomVM/mailbox.c @@ -394,11 +394,13 @@ static MailboxMessage *process_outer_list(Context *ctx, Mailbox *mbox) // so it is stable for the whole batch. An AliasMessageSignal here targets an inactive alias: // drop it when we own a context (it must never reach the signal loop, which treats // AliasMessageSignal as unreachable); when ctx is NULL leave it in the signal list for the - // scheduler/termination caller to ignore. + // scheduler/termination caller to ignore. A dropped alias message was never delivered, so + // nothing references its term: free it immediately, sweeping its refc binaries, instead of + // parking it on the receiver's heap until the next GC (OTP drops with no heap impact). while (current) { MailboxMessage *next = current->next; if (ctx != NULL && current->type == AliasMessageSignal) { - mailbox_message_dispose(current, &ctx->heap); + mailbox_message_dispose_unsent(CONTAINER_OF(current, Message, base), ctx->global, false); } else if (current->type == NormalMessage) { if (normal_last == NULL) { normal_last = current; @@ -464,8 +466,10 @@ static MailboxMessage *process_outer_list(Context *ctx, Mailbox *mbox) } normal_last = &converted->base; } else { - // Inactive alias: drop the message and free the signal. - mailbox_message_dispose(current, &ctx->heap); + // Inactive alias: the message was never delivered, so nothing references its + // term -- free it immediately (sweeping refc binaries) instead of deferring + // reclamation to the receiver's next GC. + mailbox_message_dispose_unsent(CONTAINER_OF(current, Message, base), ctx->global, false); } } else { // A 'DOWN' that auto-removes a {alias, demonitor} / {alias, reply_demonitor} monitor diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 12c9f600ca..8fa3841ebc 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -686,6 +686,8 @@ test_alias_reply_mode() -> m1 = recv_one(), ok = assert_no_message(), A ! m3, + %% A dropped refc binary exercises the mso sweep of the immediately-freed signal. + A ! <<0:1600>>, ok = assert_no_message(), false = unalias(A), ok. @@ -796,6 +798,8 @@ test_alias_duplicate_options() -> A1 = alias([explicit_unalias, reply]), A1 ! r1, A1 ! r2, + %% A dropped refc binary exercises the mso sweep on the received-order drop path too. + A1 ! <<0:1600>>, r1 = recv_one(), ok = assert_no_message(), false = unalias(A1), From c63f98030a09fe7622f0ccf8269b9f1e874456f8 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Fri, 12 Jun 2026 16:47:51 +0000 Subject: [PATCH 61/87] Document the drain-time processes_table lock invariant The reply_demonitor branch of context_process_alias_message_signal takes the processes_table read lock from inside the mailbox drain, which is why callers of the with-aliases drain must not hold that lock and why context_destroy stays on the alias-blind variant. The constraint was documented at the mailbox.h API; nothing at the acquisition site itself said so, and the pre-merge review (round 2, issue 5) asked for a guard against a future caller draining with the lock held. A guard already exists, stronger than the suggested debug assertion: on generic_unix smp_rwlock_rdlock aborts on any pthread error, and glibc returns EDEADLK when the calling thread already owns the write lock (probe-verified), so the misuse dies fast at the exact lock call in every build type instead of deadlocking silently. A literal current-thread ownership assertion would need thread-local tracking maintained at all 22 processes_table lock sites across the four platform smp implementations -- out of proportion for a fragility note. State the invariant and its enforcement in a comment at the acquisition site, so the coupling is visible where a future reader would break it. Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index cc7d528f52..f1f7e35d95 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -516,6 +516,11 @@ term context_process_alias_message_signal(Context *ctx, struct TermSignal *signa term monitor_pid = context_get_monitor_pid(ctx, ref_ticks, &is_monitoring); context_demonitor(ctx, ref_ticks); if (!term_is_invalid_term(monitor_pid) && is_monitoring) { + // This takes the processes_table read lock from inside the mailbox drain, so a caller + // of the with-aliases drain must not hold that lock (mailbox.h documents the + // constraint; context_destroy deliberately stays on the alias-blind variant). Misuse + // fails fast instead of deadlocking: glibc pthread returns EDEADLK to the write-lock + // owner and smp_rwlock_rdlock aborts on any error. int32_t monitored_process_id = term_to_local_process_id(monitor_pid); Context *target = globalcontext_get_process_lock(ctx->global, monitored_process_id); if (target) { From e406a4e2c98963ef2f7b3efe43be9fa201771bfd Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Fri, 12 Jun 2026 17:07:54 +0000 Subject: [PATCH 62/87] Raise the same-batch relay wait bound for valgrind schedulers The owner in test_monitor_alias_down_before_send_same_batch busy-waits on whereis by design: receiving would drain its mailbox and defeat the same-batch construction. Under valgrind all threads are serialized with an unfair scheduler, so the spinning owner can starve the relay; the 5M-iteration bound expired once in a full-suite valgrind run (the test passes standalone, on the other flavor, and on rerun, with zero memory errors every time). Raise the bound to 50M -- ten times the scheduling opportunities for the relay -- and say why it is large. The fast path is unchanged: the loop exits as soon as the relay registers. Signed-off-by: Davide Bettio --- tests/erlang_tests/test_monitor.erl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 8fa3841ebc..369c45f400 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -540,7 +540,9 @@ test_monitor_alias_down_before_send_same_batch() -> [] ), P ! quit, - ok = wait_registered(down_batch_relay, 5000000), + %% The huge bound absorbs valgrind's unfair thread scheduling, which can starve the relay + %% while this process spins (it must busy-wait: receiving would drain its mailbox). + ok = wait_registered(down_batch_relay, 50000000), {'DOWN', Mon, process, P, normal} = recv_one(), ok = assert_no_message(), Relay ! release, From 32f36790d9e63d005e3c563d300930ac12d7e55d Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Mon, 15 Jun 2026 08:50:29 +0000 Subject: [PATCH 63/87] Make the unsupported-option doc notes stand out The monitor/3 and alias/1 @doc comments each closed their option-list paragraph with a one-line caveat about an OTP option AtomVM does not support: {tag, Term} for monitor/3, the OTP 28 priority option for alias/1. Tucked onto the end of the bullet list, both were easy to miss. Lift each caveat into its own paragraph, led by a bold "Note:" and reworded to open with "Unlike Erlang/OTP", so the divergence stands out when scanning the generated documentation. Comment-only change; the compiled module is unaffected. Signed-off-by: Davide Bettio --- libs/estdlib/src/erlang.erl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/libs/estdlib/src/erlang.erl b/libs/estdlib/src/erlang.erl index 3bdc64c1a6..0de24c3b98 100644 --- a/libs/estdlib/src/erlang.erl +++ b/libs/estdlib/src/erlang.erl @@ -1335,7 +1335,9 @@ monitor(_Type, _PidOrPort) -> %% - reply_demonitor - additionally, the alias is deactivated and the %% monitor removed (as by `demonitor/1') when the %% first message sent via the alias is delivered. -%% The OTP `{tag, Term}' option is not supported and raises `unsupported'. +%% +%% Note: Unlike Erlang/OTP, the `{tag, Term}' option is not +%% supported and raises `unsupported'. %% @end %%----------------------------------------------------------------------------- -spec monitor @@ -2205,7 +2207,9 @@ alias() -> %% With `explicit_unalias' (the default, so `alias([])' is `alias/0') %% the alias stays active until `unalias/1'; with `reply' it is %% deactivated when the first message sent via the alias is delivered. -%% The OTP 28 `priority' option is not supported and raises `badarg'. +%% +%% Note: Unlike Erlang/OTP, the `priority' option (OTP 28) is +%% not supported and raises `badarg'. %% @end %%----------------------------------------------------------------------------- -spec alias(Options) -> Alias when Options :: [explicit_unalias | reply], Alias :: reference(). From 762311976e887086033dde380744512934d97eec Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Mon, 15 Jun 2026 08:55:16 +0000 Subject: [PATCH 64/87] Drop the narration above the decoded process-id check The process-reference decode path rejects a process id that is INVALID_PROCESS_ID or above TERM_MAX_LOCAL_PROCESS_ID. The three-line comment above the check only restated it -- the constant names already say what is rejected and why. Remove it. Signed-off-by: Davide Bettio --- src/libAtomVM/external_term.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/libAtomVM/external_term.c b/src/libAtomVM/external_term.c index 2457461849..038af64379 100644 --- a/src/libAtomVM/external_term.c +++ b/src/libAtomVM/external_term.c @@ -1013,9 +1013,6 @@ static term parse_external_terms(const uint8_t *external_term_buf, size_t *eterm } else if (len == 3 && node == this_node && creation == this_creation) { uint64_t ticks = ((uint64_t) data[0]) << 32 | data[1]; uint32_t process_id = data[2]; - // Reject a malformed process id (the value comes from untrusted input). pid 0 is - // INVALID_PROCESS_ID, the short-ref sentinel, so it never names a real owner: a - // genuine short ref is encoded as a 2-word reference, not a process reference. if (process_id == INVALID_PROCESS_ID || process_id > TERM_MAX_LOCAL_PROCESS_ID) { return term_invalid_term(); } From 9c193d8f9e1bcf4503cba09e2863cc383280402f Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Mon, 15 Jun 2026 12:14:22 +0000 Subject: [PATCH 65/87] Test the term ordering of alias references The ordering of alias references was exercised only indirectly, by test_refs_ordering, whose aliases all share one owner. Add a focused test_monitor case pinning the portable, OTP-conformant properties: a plain reference sorts before an alias whichever was created first; aliases of one owner order by creation; the owner pid is encoded, so a process reference round-trips through term_to_binary equal; and two owners' aliases are distinct and strictly ordered. The cross-owner direction follows the internal pid, not the pid term order (checked on OTP 29), so it is asserted only as a strict total order rather than pinned. The module also runs on the BEAM; the test passes there and on the interpreter and JIT flavors. Signed-off-by: Davide Bettio --- tests/erlang_tests/test_monitor.erl | 39 +++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 369c45f400..04ea9b9d3d 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -74,8 +74,47 @@ start() -> ok = test_alias_duplicate_options(), ok = test_unalias_non_reference_badarg(), ok = test_binary_to_term_invalid_process_ref(), + ok = test_alias_ref_ordering(), 0. +%% Reference term order around aliases, matching OTP (probed on OTP 29; this +%% module also runs on the BEAM). An alias is a process reference: it carries the +%% owner pid, so it sorts after every plain reference whichever was created first, +%% aliases of one owner order by creation, and the owner pid takes part in the +%% order so two owners' aliases are distinct and strictly ordered. That last +%% direction follows the internal pid (not the pid term order) and is +%% implementation defined, so it is not pinned here. +test_alias_ref_ordering() -> + %% A plain reference sorts before an alias, whichever was created first. + R0 = make_ref(), + A0 = erlang:alias(), + true = R0 < A0, + A1 = erlang:alias(), + R1 = make_ref(), + true = R1 < A1, + %% Same owner: a later alias is greater than an earlier one (ticks break the tie). + Ea = erlang:alias(), + Eb = erlang:alias(), + true = Ea < Eb, + %% The pid word is encoded and decoded: a process reference round-trips equal. + true = Eb =:= binary_to_term(term_to_binary(Eb)), + %% Two owners' aliases are distinct and strictly ordered (the pid word participates). + Parent = self(), + Child = spawn_opt(fun() -> + receive {get, P} -> P ! {child_alias, erlang:alias()} end + end, []), + Child ! {get, Parent}, + ChildAlias = + receive + {child_alias, Ca} -> Ca + after 5000 -> error(child_alias_timeout) + end, + SelfAlias = erlang:alias(), + true = ChildAlias =/= SelfAlias, + true = (ChildAlias < SelfAlias) xor (SelfAlias < ChildAlias), + _ = [erlang:unalias(R) || R <- [A0, A1, Ea, Eb, SelfAlias]], + ok. + test_monitor_normal() -> Pid = spawn_opt(fun() -> normal_loop() end, []), Ref = monitor(process, Pid), From 6794cb78fc523a9650fe73d7a69640f5bcdb6b00 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Mon, 15 Jun 2026 12:14:30 +0000 Subject: [PATCH 66/87] Drop the wire-order narration in the reference comparison The two len == 3 branches in term_compare each carried a multi-line comment explaining that a local process reference is laid out [ticks_hi, ticks_lo, process_id] to match the wire/external word order. The layout is plain in the three assignments that follow, and each branch is already labelled by its else if (len == 3), like the commentless len == 2 sibling. Remove the narration. Signed-off-by: Davide Bettio --- src/libAtomVM/term.c | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/libAtomVM/term.c b/src/libAtomVM/term.c index caef26985d..03a1cadf47 100644 --- a/src/libAtomVM/term.c +++ b/src/libAtomVM/term.c @@ -705,10 +705,6 @@ TermCompareResult term_compare(term t, term other, TermCompareOpts opts, GlobalC other_data[0] = other_ticks >> 32; other_data[1] = (uint32_t) other_ticks; } else if (len == 3) { - // Process references: compare in the same word order term_to_binary - // writes on the wire ([ticks_hi, ticks_lo, process_id]), so a local - // process reference orders consistently with its serialized form and - // with the external-reference path below. int64_t t_ticks = term_to_ref_ticks(t); data[0] = t_ticks >> 32; data[1] = (uint32_t) t_ticks; @@ -795,11 +791,6 @@ TermCompareResult term_compare(term t, term other, TermCompareOpts opts, GlobalC other_data = local_data; } } else if (len == 3) { - // len == 3 (process references). Lay a local process - // reference out in wire order ([ticks_hi, ticks_lo, - // process_id]) to match the external reference words, so a - // mixed local/external comparison is consistent (and agrees - // with the local-vs-local and external-vs-external paths). uint32_t local_data[3]; if (term_is_external(t)) { data = term_get_external_reference_words(t); From c539f6f81d41e5c0abc33431c7c415ba260df3a7 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Mon, 15 Jun 2026 12:34:11 +0000 Subject: [PATCH 67/87] Check the process reference wire bytes and block round-trip folding test_encode_process_ref only asserted that binary_to_term(term_to_binary(R)) returns R, which a compiler is free to fold to R = R, testing nothing. Route the binary through ?MODULE:id/1 (already used elsewhere in this module) so the round-trip is opaque to the compiler and actually runs. Also match the serialized form against its known bytes: the version byte, the NEWER_REFERENCE_EXT tag, the nonode@nohost node atom and the zero creation. The id-word count differs between implementations (AtomVM 3 words, the BEAM 5), so the length word and the id bytes are left unmatched -- the pattern holds on both, where this module also runs. Signed-off-by: Davide Bettio --- tests/erlang_tests/test_binary_to_term.erl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/erlang_tests/test_binary_to_term.erl b/tests/erlang_tests/test_binary_to_term.erl index 4ae419a086..66c0c77811 100644 --- a/tests/erlang_tests/test_binary_to_term.erl +++ b/tests/erlang_tests/test_binary_to_term.erl @@ -998,7 +998,14 @@ test_encode_resource(OTPVersion) -> test_encode_process_ref() -> ProcessRef = erlang:alias(), - ProcessRef = binary_to_term(term_to_binary(ProcessRef)), + Bin = term_to_binary(ProcessRef), + %% A process reference serializes as NEWER_REFERENCE_EXT (90) for the + %% nonode@nohost node. The id-word count differs (AtomVM 3 words, the BEAM 5), + %% so the length word and the id bytes that follow it are left unmatched. + <<131, 90, _Len:16, 119, 13, "nonode@nohost", 0:32, _/binary>> = Bin, + %% ?MODULE:id/1 keeps the compiler from folding the round-trip + %% binary_to_term(term_to_binary(X)) back to X. + ProcessRef = binary_to_term(?MODULE:id(Bin)), ok. % Verify term_to_binary(binary_to_term(Bin)) is idempotent. From c4ffdcbc58539f24fa3bb69e0d899c0e1e802d68 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Mon, 15 Jun 2026 12:34:21 +0000 Subject: [PATCH 68/87] Remove the OTP version note from the process aliases changelog entry Drop the "(OTP >= 26 semantics)" parenthetical; the entry now reads "Added support for process aliases: ...". Signed-off-by: Davide Bettio --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 153b5f6113..4e15f31a40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `"USB_SERIAL_JTAG"` peripheral to the ESP32 `uart` module on chips with a built-in USB-Serial-JTAG controller (C3/C5/C6/C61/H2/H21/H4/P4/S3) - Added support for the `safe` option in `erlang:binary_to_term/2` -- Added support for process aliases (OTP ≥ 26 semantics): `erlang:alias/0,1`, `erlang:unalias/1`, +- Added support for process aliases: `erlang:alias/0,1`, `erlang:unalias/1`, `erlang:monitor/3` with the `{alias, Mode}` option, `spawn_opt` `{monitor, MonitorOpts}` and sending to an alias reference - Added xtensa JIT backend for esp32 platform From 78b010e301f45702705699b610a5154ef9ff6607 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Mon, 15 Jun 2026 13:36:05 +0000 Subject: [PATCH 69/87] Check for a reference before the alias branch in the send paths erlang:send/2, the send opcode and the JIT send dispatched on term_is_process_reference before the term_is_reference guard that rejects a non-reference recipient with badarg. Put the guard first, then the process-reference (active alias) branch, then the silent drop for any other reference. A process reference is always a reference (TERM_BOXED_REFERENCE_PROCESS_HEADER carries TERM_BOXED_REF in its low bits), so the three outcomes -- badarg, alias send, drop -- are unchanged. Also turn the note about the unsupported outbound distributed alias into a TODO at each of the three drop sites. Signed-off-by: Davide Bettio --- src/libAtomVM/jit.c | 11 ++++++----- src/libAtomVM/nifs.c | 11 ++++++----- src/libAtomVM/opcodesswitch.h | 7 ++++--- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/libAtomVM/jit.c b/src/libAtomVM/jit.c index 6858a76a1f..6686edd770 100644 --- a/src/libAtomVM/jit.c +++ b/src/libAtomVM/jit.c @@ -859,18 +859,19 @@ static bool jit_send(Context *ctx, JITState *jit_state) } globalcontext_send_message(ctx->global, local_process_id, ctx->x[1]); ctx->x[0] = ctx->x[1]; + } else if (!term_is_reference(recipient_term)) { + set_error(ctx, jit_state, 0, BADARG_ATOM); + return false; } else if (term_is_process_reference(recipient_term)) { int32_t process_id = term_process_ref_to_process_id(recipient_term); globalcontext_send_message_to_alias(ctx->global, process_id, recipient_term, ctx->x[1]); ctx->x[0] = ctx->x[1]; - } else if (!term_is_reference(recipient_term)) { - set_error(ctx, jit_state, 0, BADARG_ATOM); - return false; } else { // A reference that is not a local process reference (short/resource/external ref): the // message is silently dropped, as OTP drops a send to a non-active-alias reference, but - // send still returns the message. Distributed aliases (external references) are - // unsupported, so they are lost. + // send still returns the message. + // TODO: support distributed aliases (external references) by routing the send over + // distribution; for now a message addressed to a remote alias is lost. ctx->x[0] = ctx->x[1]; } diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index b02822ad56..7354339721 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1859,9 +1859,6 @@ static term nif_erlang_send_2(Context *ctx, int argc, term argv[]) globalcontext_send_message(glb, local_process_id, argv[1]); - } else if (term_is_process_reference(target)) { - int32_t process_id = term_process_ref_to_process_id(target); - globalcontext_send_message_to_alias(glb, process_id, target, argv[1]); } else if (term_is_atom(target)) { // We need to hold a lock on the processes_table until the message is sent to avoid a race condition, // otherwise the receiving process could be killed at any point between checking it is registered, @@ -1888,11 +1885,15 @@ static term nif_erlang_send_2(Context *ctx, int argc, term argv[]) synclist_unlock(&glb->processes_table); } else if (!term_is_reference(target)) { RAISE_ERROR(BADARG_ATOM); + } else if (term_is_process_reference(target)) { + int32_t process_id = term_process_ref_to_process_id(target); + globalcontext_send_message_to_alias(glb, process_id, target, argv[1]); } // else: target is a reference but not a local process reference (a plain/short ref, a resource ref, // or an external reference). It is silently dropped, as OTP drops a send to a reference that is not - // an active local alias. Distributed aliases (external references) are not supported, so a message - // addressed to a remote alias is lost rather than routed over distribution. + // an active local alias. + // TODO: support distributed aliases (external references) by routing the send over distribution; + // for now a message addressed to a remote alias is lost rather than routed. return argv[1]; } diff --git a/src/libAtomVM/opcodesswitch.h b/src/libAtomVM/opcodesswitch.h index 3f52293588..125b297bbc 100644 --- a/src/libAtomVM/opcodesswitch.h +++ b/src/libAtomVM/opcodesswitch.h @@ -2378,17 +2378,18 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) TRACE("send/0 target_pid=%i\n", local_process_id); TRACE_SEND(ctx, x_regs[0], x_regs[1]); globalcontext_send_message(ctx->global, local_process_id, x_regs[1]); + } else if (!term_is_reference(recipient_term)) { + RAISE_ERROR(BADARG_ATOM); } else if (term_is_process_reference(recipient_term)) { int32_t target_process_id = term_process_ref_to_process_id(recipient_term); TRACE("send/0 target_pid=%i\n", target_process_id); TRACE_SEND(ctx, x_regs[0], x_regs[1]); globalcontext_send_message_to_alias(ctx->global, target_process_id, recipient_term, x_regs[1]); - } else if (!term_is_reference(recipient_term)) { - RAISE_ERROR(BADARG_ATOM); } // else: a reference that is not a local process reference (short/resource/external // ref) is silently dropped, as OTP drops a send to a non-active-alias reference. - // Distributed aliases (external references) are unsupported, so they are lost. + // TODO: support distributed aliases (external references) by routing the send over + // distribution; for now a message addressed to a remote alias is lost. x_regs[0] = x_regs[1]; } break; From b95b07dc4c1c9723293acc707efbbe64edde7727 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Mon, 15 Jun 2026 13:51:41 +0000 Subject: [PATCH 70/87] Saturate the active alias count in 8 bits without a separate flag active_alias_count was a 16-bit field plus a 1-bit alias_count_saturated flag (17 bits) used only to skip the monitor-list walk for alias-free processes. That skip only distinguishes count == 0 from count > 0, so saturation needs no distinct value: shrink the count to 8 bits and let 0xFF double as the saturated sentinel. It pins at 0xFF once 255 aliases coexist and is never decremented again, so lookups keep walking the list rather than wrapping to 0 and silently deactivating every alias -- the same guarantee as before, in one fewer field and a little less code. The read gates (context_find_alias and the mailbox fast path) are unchanged; the increment loses its saturation branch and the two decrements guard on the 0xFF sentinel instead of the removed flag. Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 17 +++++++---------- src/libAtomVM/context.h | 8 +++----- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index f1f7e35d95..e0bf2d634d 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -84,7 +84,6 @@ Context *context_new(GlobalContext *glb) ctx->has_min_heap_size = 0; ctx->has_max_heap_size = 0; ctx->active_alias_count = 0; - ctx->alias_count_saturated = 0; mailbox_init(&ctx->mailbox); @@ -893,7 +892,7 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) } case CONTEXT_MONITOR_ALIAS: { struct MonitorAlias *alias = CONTAINER_OF(monitor, struct MonitorAlias, monitor); - if (LIKELY(!ctx->alias_count_saturated)) { + if (LIKELY(ctx->active_alias_count != 0xFF)) { ctx->active_alias_count--; } free(alias); @@ -1098,15 +1097,13 @@ bool context_add_monitor(Context *ctx, struct Monitor *new_monitor) } list_append(&ctx->monitors_head, &new_monitor->monitor_list_head); if (new_monitor->monitor_type == CONTEXT_MONITOR_ALIAS) { - if (LIKELY(ctx->active_alias_count < 0xFFFF)) { + if (LIKELY(ctx->active_alias_count < 0xFF)) { ctx->active_alias_count++; - } else { - // Saturate instead of wrapping to 0, which would make context_find_alias skip the - // list walk and silently deactivate every alias of this process. Once saturated the - // count is pinned (see alias_count_saturated in context.h) and lookups are never - // skipped again for this process. - ctx->alias_count_saturated = 1; } + // else: the count is pinned at 0xFF instead of wrapping to 0, which would make + // context_find_alias skip the list walk and silently deactivate every alias of this + // process. Once saturated it is never decremented again and lookups are never skipped + // again for this process. } return true; } @@ -1287,7 +1284,7 @@ void context_unalias(Context *ctx, struct MonitorAlias *alias) { TERM_DEBUG_ASSERT(alias != NULL); TERM_DEBUG_ASSERT(ctx->active_alias_count > 0); - if (LIKELY(!ctx->alias_count_saturated)) { + if (LIKELY(ctx->active_alias_count != 0xFF)) { ctx->active_alias_count--; } struct Monitor *monitor = &alias->monitor; diff --git a/src/libAtomVM/context.h b/src/libAtomVM/context.h index 6f52ba2fc4..c2079d6ed5 100644 --- a/src/libAtomVM/context.h +++ b/src/libAtomVM/context.h @@ -140,12 +140,10 @@ struct Context unsigned int has_max_heap_size : 1; // Number of active aliases on monitors_head. Owner-written only, like the surrounding // bits; lets the alias lookups short-circuit for the common alias-free process. - // Saturating: when a 65536th alias is added the count sticks at 0xFFFF and - // alias_count_saturated is set, after which the count is never decremented again -- - // lookups are no longer skipped for this process, trading the optimization for + // Saturating: it sticks at 0xFF once 255 aliases coexist and is never decremented again, + // so lookups are no longer skipped for this process -- trading the optimization for // correctness instead of wrapping to 0 and silently deactivating every alias. - unsigned int active_alias_count : 16; - unsigned int alias_count_saturated : 1; + unsigned int active_alias_count : 8; bool trap_exit : 1; #ifndef AVM_NO_EMU From a75c8ead223acbc070e978b8bc858e486ba44a47 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Mon, 15 Jun 2026 14:05:49 +0000 Subject: [PATCH 71/87] Reserve the exact reference size for the terminate-path DOWN message context_monitors_handle_terminate reserved TERM_BOXED_REFERENCE_PROCESS_SIZE for every monitor's DOWN, but only a CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS monitor builds a process reference; a plain monitor builds a short reference, one word smaller on 64-bit and two on 32-bit. Plain monitors are the common case, so reserve by the flavor instead of always taking the larger size. Hoist the monitor_type test into a single is_monitored_alias bool that drives both the reservation size and the ref build, so the reserved size cannot drift from what is allocated -- the reservation now exactly matches the allocation on each branch. Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index e0bf2d634d..4a591cf804 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -858,18 +858,22 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) Context *target = globalcontext_get_process_nolock(glb, local_process_id); // Target cannot be NULL as we processed Demonitor signals assert(target != NULL); - int required_terms = TERM_BOXED_REFERENCE_PROCESS_SIZE + TUPLE_SIZE(5); + // The ref carried by the DOWN depends on the monitor flavor (built below): an + // {alias, _} monitor needs a process reference, a plain monitor a short one. + // Reserve exactly the size that build will use. + bool is_monitored_alias = monitor->monitor_type == CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS; + int ref_size = is_monitored_alias ? TERM_BOXED_REFERENCE_PROCESS_SIZE : TERM_BOXED_REFERENCE_SHORT_SIZE; + int required_terms = ref_size + TUPLE_SIZE(5); if (UNLIKELY(memory_ensure_free(ctx, required_terms) != MEMORY_GC_OK)) { // TODO: handle out of memory here fprintf(stderr, "Cannot handle out of memory.\n"); globalcontext_get_process_unlock(glb, target); AVM_ABORT(); } - // Prepare the message on ctx's heap which will be freed afterwards. A monitor - // created with the {alias, _} option must carry the same alias-shaped process - // reference the monitoring process got, whose pid is the monitoring process. + // Prepare the message on ctx's heap, which is freed afterwards. For an {alias, _} + // monitor the process reference's pid is the monitoring process (local_process_id). term ref; - if (monitor->monitor_type == CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS) { + if (is_monitored_alias) { ref = term_make_process_reference(local_process_id, monitored_monitor->ref_ticks, &ctx->heap); } else { ref = term_from_ref_ticks(monitored_monitor->ref_ticks, &ctx->heap); From 3e0b8288051316720ef7bec465b5bd747eb17021 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Mon, 15 Jun 2026 14:15:55 +0000 Subject: [PATCH 72/87] Drop the redundant pid/port check on a registered send target in jit_send In jit_send's atom branch the registered-name lookup result was re-tested with term_is_local_pid_or_port, raising badarg in the else. That else is unreachable: nif_erlang_register_2 is the only writer of the registry and validates term_is_local_pid_or_port before storing, so globalcontext_get_registered_process returns either a local pid/port or UNDEFINED_ATOM, and UNDEFINED_ATOM is already handled just above. Resolve straight to the process id and send, matching jit_send's own direct-pid branch and nif_erlang_send_2. Signed-off-by: Davide Bettio --- src/libAtomVM/jit.c | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/libAtomVM/jit.c b/src/libAtomVM/jit.c index 6686edd770..9f15225e4e 100644 --- a/src/libAtomVM/jit.c +++ b/src/libAtomVM/jit.c @@ -850,13 +850,9 @@ static bool jit_send(Context *ctx, JITState *jit_state) set_error(ctx, jit_state, 0, BADARG_ATOM); return false; } - int local_process_id; - if (term_is_local_pid_or_port(recipient_term)) { - local_process_id = term_to_local_process_id(recipient_term); - } else { - set_error(ctx, jit_state, 0, BADARG_ATOM); - return false; - } + // A registered name can only resolve to a local pid or port (register/2 validates this), + // so the lookup result is necessarily one here. + int local_process_id = term_to_local_process_id(recipient_term); globalcontext_send_message(ctx->global, local_process_id, ctx->x[1]); ctx->x[0] = ctx->x[1]; } else if (!term_is_reference(recipient_term)) { From 251c7b2086eadd578aa444373605988ea0aa14dc Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 16 Jun 2026 13:11:45 +0000 Subject: [PATCH 73/87] Raise unsupported for the alias/1 priority option The priority option (OTP 28) was the only recognized-but-unimplemented OTP option in the alias/monitor option parsing that raised badarg; the monitor/3 {tag, _} option raises unsupported, the release-0.7 convention for options AtomVM recognizes but does not implement. Match it: recognize priority explicitly and raise unsupported, so both report the same error. Genuinely unknown options still fall through to badarg. Add the priority atom and update the alias/1 @doc note accordingly. Signed-off-by: Davide Bettio --- libs/estdlib/src/erlang.erl | 2 +- src/libAtomVM/defaultatoms.def | 1 + src/libAtomVM/nifs.c | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/libs/estdlib/src/erlang.erl b/libs/estdlib/src/erlang.erl index 0de24c3b98..80234dd206 100644 --- a/libs/estdlib/src/erlang.erl +++ b/libs/estdlib/src/erlang.erl @@ -2209,7 +2209,7 @@ alias() -> %% deactivated when the first message sent via the alias is delivered. %% %% Note: Unlike Erlang/OTP, the `priority' option (OTP 28) is -%% not supported and raises `badarg'. +%% not supported and raises `unsupported'. %% @end %%----------------------------------------------------------------------------- -spec alias(Options) -> Alias when Options :: [explicit_unalias | reply], Alias :: reference(). diff --git a/src/libAtomVM/defaultatoms.def b/src/libAtomVM/defaultatoms.def index 25580005f5..8911bc90a0 100644 --- a/src/libAtomVM/defaultatoms.def +++ b/src/libAtomVM/defaultatoms.def @@ -229,3 +229,4 @@ X(EXPLICIT_UNALIAS_ATOM, "\x10", "explicit_unalias") X(REPLY_DEMONITOR_ATOM, "\xF", "reply_demonitor") X(REPLY_ATOM, "\x5", "reply") X(TAG_ATOM, "\x3", "tag") +X(PRIORITY_ATOM, "\x8", "priority") diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 7354339721..3d4882e862 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -7641,6 +7641,8 @@ static term nif_erlang_alias(Context *ctx, int argc, term argv[]) alias_type = ContextMonitorAliasExplicitUnalias; } else if (option == REPLY_ATOM) { alias_type = ContextMonitorAliasReplyDemonitor; + } else if (UNLIKELY(option == PRIORITY_ATOM)) { + RAISE_ERROR(UNSUPPORTED_ATOM); } else { RAISE_ERROR(BADARG_ATOM); } From ca610b09a6ab3258d71f80d56e0f73023b811d02 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 16 Jun 2026 13:35:47 +0000 Subject: [PATCH 74/87] Mark the process-reference error paths as unlikely The alias/process-reference code added by this branch left a few error and not-taken branches without the UNLIKELY / IS_NULL_PTR hint that the surrounding code already applies to the same paths: the send paths guard a registered-name miss with UNLIKELY but not the adjacent non-reference badarg, and nif_erlang_unalias hints the alias lookup with IS_NULL_PTR but not the external-reference rejection just above it. Hint them the same way: the non-reference send target (the send/2 NIF, the send opcode and jit_send) and the external-reference unalias/1 with UNLIKELY before their badarg/false, the out-of-range owner pid in the process-reference decode with UNLIKELY, and the drop of a message to an inactive alias with IS_NULL_PTR. Branch-prediction hints only; no behavior change. Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 2 +- src/libAtomVM/external_term.c | 2 +- src/libAtomVM/jit.c | 2 +- src/libAtomVM/nifs.c | 4 ++-- src/libAtomVM/opcodesswitch.h | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index 4a591cf804..6461e5100e 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -502,7 +502,7 @@ term context_process_alias_message_signal(Context *ctx, struct TermSignal *signa term message = term_get_tuple_element(signal->signal_term, 1); struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); - if (alias == NULL) { + if (IS_NULL_PTR(alias)) { // Alias is not (or no longer) active: drop the message, matching OTP. return term_invalid_term(); } diff --git a/src/libAtomVM/external_term.c b/src/libAtomVM/external_term.c index 038af64379..f306e2e810 100644 --- a/src/libAtomVM/external_term.c +++ b/src/libAtomVM/external_term.c @@ -1013,7 +1013,7 @@ static term parse_external_terms(const uint8_t *external_term_buf, size_t *eterm } else if (len == 3 && node == this_node && creation == this_creation) { uint64_t ticks = ((uint64_t) data[0]) << 32 | data[1]; uint32_t process_id = data[2]; - if (process_id == INVALID_PROCESS_ID || process_id > TERM_MAX_LOCAL_PROCESS_ID) { + if (UNLIKELY(process_id == INVALID_PROCESS_ID || process_id > TERM_MAX_LOCAL_PROCESS_ID)) { return term_invalid_term(); } return term_make_process_reference(process_id, ticks, heap); diff --git a/src/libAtomVM/jit.c b/src/libAtomVM/jit.c index 9f15225e4e..a5b95fd3a5 100644 --- a/src/libAtomVM/jit.c +++ b/src/libAtomVM/jit.c @@ -855,7 +855,7 @@ static bool jit_send(Context *ctx, JITState *jit_state) int local_process_id = term_to_local_process_id(recipient_term); globalcontext_send_message(ctx->global, local_process_id, ctx->x[1]); ctx->x[0] = ctx->x[1]; - } else if (!term_is_reference(recipient_term)) { + } else if (UNLIKELY(!term_is_reference(recipient_term))) { set_error(ctx, jit_state, 0, BADARG_ATOM); return false; } else if (term_is_process_reference(recipient_term)) { diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 3d4882e862..130d9a72bf 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1883,7 +1883,7 @@ static term nif_erlang_send_2(Context *ctx, int argc, term argv[]) globalcontext_send_message_nolock(glb, local_process_id, argv[1]); synclist_unlock(&glb->processes_table); - } else if (!term_is_reference(target)) { + } else if (UNLIKELY(!term_is_reference(target))) { RAISE_ERROR(BADARG_ATOM); } else if (term_is_process_reference(target)) { int32_t process_id = term_process_ref_to_process_id(target); @@ -7673,7 +7673,7 @@ static term nif_erlang_unalias(Context *ctx, int argc, term argv[]) term process_ref = argv[0]; VALIDATE_VALUE(process_ref, term_is_reference); - if (!term_is_local_reference(process_ref)) { + if (UNLIKELY(!term_is_local_reference(process_ref))) { // An external reference cannot be an alias of the calling process: return false like // OTP, instead of raising badarg. return FALSE_ATOM; diff --git a/src/libAtomVM/opcodesswitch.h b/src/libAtomVM/opcodesswitch.h index 125b297bbc..e867342505 100644 --- a/src/libAtomVM/opcodesswitch.h +++ b/src/libAtomVM/opcodesswitch.h @@ -2378,7 +2378,7 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) TRACE("send/0 target_pid=%i\n", local_process_id); TRACE_SEND(ctx, x_regs[0], x_regs[1]); globalcontext_send_message(ctx->global, local_process_id, x_regs[1]); - } else if (!term_is_reference(recipient_term)) { + } else if (UNLIKELY(!term_is_reference(recipient_term))) { RAISE_ERROR(BADARG_ATOM); } else if (term_is_process_reference(recipient_term)) { int32_t target_process_id = term_process_ref_to_process_id(recipient_term); From f4cf48b26b2a57e8dbbd4b84e72ca845b0231b11 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 16 Jun 2026 14:22:32 +0000 Subject: [PATCH 75/87] Trim redundant and obvious comments added by the alias work The process-aliases work added a number of comments that restate what the code or the header doxygen already says, repeat the same fact across files, or narrate platform internals. The per-function contracts are documented in context.h, globalcontext.h and mailbox.h, so the inline copies in context.c and globalcontext.c added nothing but a second place to drift out of sync. Drop the inline blocks that duplicate the header docs (the alias-signal contract, the send-to-alias rationale), the comment that restated the reference-size ternary right below it, the active_alias_count saturation note already carried by the field declaration, and the glibc/pthread EDEADLK narration on the processes-table lock. Tighten the still-useful comments in the mailbox drain, do_spawn, monitor/3 and the three send sites, keeping the non-obvious reasoning (received order, in-place re-typing, OOM atomicity) but not the restatement. Also remove the duplicated "reference types are distinguished by size" clause from the 32-bit process-reference size comment. Fix a misplaced comment in nif_erlang_dist_ctrl_put_data along the way: the "dist handle" label sat on roots[1] (argv[1], the decoded binary) instead of roots[0] (argv[0], the connection resource), where the other operations in the same function correctly put it. The code itself is unchanged. In the tests, drop narration that the test function name already conveys, and extract the eavmlib file select-handle alias-rejection check out of test_fifo_select into its own test_alias_select_handle_rejected/1. Comment and test-refactor only; no behavior change. Both flavors build and the alias modules plus the eavmlib and estdlib test packs pass. Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 27 ++------- src/libAtomVM/context.h | 12 ++-- src/libAtomVM/dist_nifs.c | 11 ++-- src/libAtomVM/globalcontext.c | 3 - src/libAtomVM/jit.c | 10 +--- src/libAtomVM/mailbox.c | 61 +++++++-------------- src/libAtomVM/nifs.c | 64 +++++++++------------- src/libAtomVM/opcodesswitch.h | 6 +- src/libAtomVM/otp_socket.c | 6 +- src/libAtomVM/resources.c | 6 +- src/libAtomVM/term.h | 7 +-- tests/erlang_tests/test_binary_to_term.erl | 5 -- tests/libs/eavmlib/test_file.erl | 30 ++++++---- tests/libs/estdlib/test_udp_socket.erl | 4 -- 14 files changed, 93 insertions(+), 159 deletions(-) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index 6461e5100e..c6ee820ddc 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -492,11 +492,6 @@ void context_process_monitor_down_signal(Context *ctx, struct TermSignal *signal term context_process_alias_message_signal(Context *ctx, struct TermSignal *signal) { - // signal->signal_term is a 2-tuple {Ref, Message}. The alias lookup runs here, in the owner's own - // context, against the owner's own monitor list -- the same single-owner discipline that alias/0, - // unalias/1 and demonitor rely on. That is what makes it race-free, unlike checking from the sender. - // Returns the message to deliver as a normal message, or an invalid term if the alias is inactive. - // The caller delivers it in send order, so an alias send is not reordered against a plain send. term ref = term_get_tuple_element(signal->signal_term, 0); uint64_t ref_ticks = term_to_ref_ticks(ref); term message = term_get_tuple_element(signal->signal_term, 1); @@ -508,18 +503,14 @@ term context_process_alias_message_signal(Context *ctx, struct TermSignal *signa } if (alias->alias_type == ContextMonitorAliasReplyDemonitor) { - // reply_demonitor: deactivate the alias and remove the local monitor, then tell the monitored - // process to drop its side of the monitor (as erlang:demonitor/1 does). Capture the monitored - // pid before context_demonitor removes the local monitoring entry. + // Deactivate the alias and remove the monitor, like erlang:demonitor/1. Capture the + // monitored pid first, before context_demonitor removes the local monitoring entry. bool is_monitoring = false; term monitor_pid = context_get_monitor_pid(ctx, ref_ticks, &is_monitoring); context_demonitor(ctx, ref_ticks); if (!term_is_invalid_term(monitor_pid) && is_monitoring) { - // This takes the processes_table read lock from inside the mailbox drain, so a caller - // of the with-aliases drain must not hold that lock (mailbox.h documents the - // constraint; context_destroy deliberately stays on the alias-blind variant). Misuse - // fails fast instead of deadlocking: glibc pthread returns EDEADLK to the write-lock - // owner and smp_rwlock_rdlock aborts on any error. + // Takes the processes_table read lock: callers of the with-aliases drain must not + // already hold it (see mailbox_process_outer_list_with_aliases). int32_t monitored_process_id = term_to_local_process_id(monitor_pid); Context *target = globalcontext_get_process_lock(ctx->global, monitored_process_id); if (target) { @@ -858,9 +849,6 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) Context *target = globalcontext_get_process_nolock(glb, local_process_id); // Target cannot be NULL as we processed Demonitor signals assert(target != NULL); - // The ref carried by the DOWN depends on the monitor flavor (built below): an - // {alias, _} monitor needs a process reference, a plain monitor a short one. - // Reserve exactly the size that build will use. bool is_monitored_alias = monitor->monitor_type == CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS; int ref_size = is_monitored_alias ? TERM_BOXED_REFERENCE_PROCESS_SIZE : TERM_BOXED_REFERENCE_SHORT_SIZE; int required_terms = ref_size + TUPLE_SIZE(5); @@ -870,8 +858,7 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) globalcontext_get_process_unlock(glb, target); AVM_ABORT(); } - // Prepare the message on ctx's heap, which is freed afterwards. For an {alias, _} - // monitor the process reference's pid is the monitoring process (local_process_id). + // Prepare the message on ctx's heap, which is freed afterwards. term ref; if (is_monitored_alias) { ref = term_make_process_reference(local_process_id, monitored_monitor->ref_ticks, &ctx->heap); @@ -1104,10 +1091,6 @@ bool context_add_monitor(Context *ctx, struct Monitor *new_monitor) if (LIKELY(ctx->active_alias_count < 0xFF)) { ctx->active_alias_count++; } - // else: the count is pinned at 0xFF instead of wrapping to 0, which would make - // context_find_alias skip the list walk and silently deactivate every alias of this - // process. Once saturated it is never decremented again and lookups are never skipped - // again for this process. } return true; } diff --git a/src/libAtomVM/context.h b/src/libAtomVM/context.h index c2079d6ed5..c54abbe4a9 100644 --- a/src/libAtomVM/context.h +++ b/src/libAtomVM/context.h @@ -138,11 +138,9 @@ struct Context unsigned int leader : 1; unsigned int has_min_heap_size : 1; unsigned int has_max_heap_size : 1; - // Number of active aliases on monitors_head. Owner-written only, like the surrounding - // bits; lets the alias lookups short-circuit for the common alias-free process. - // Saturating: it sticks at 0xFF once 255 aliases coexist and is never decremented again, - // so lookups are no longer skipped for this process -- trading the optimization for - // correctness instead of wrapping to 0 and silently deactivating every alias. + // Number of active aliases on monitors_head; lets alias lookups short-circuit for the + // common alias-free process. Owner-written only. Saturates at 0xFF rather than wrapping to + // 0, which would skip the lookups and silently deactivate every alias of the process. unsigned int active_alias_count : 8; bool trap_exit : 1; @@ -184,9 +182,7 @@ enum ContextMonitorType CONTEXT_MONITOR_LINK_REMOTE, CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME, CONTEXT_MONITOR_ALIAS, - // Like CONTEXT_MONITOR_MONITORED_LOCAL, for a monitor created with the {alias, _} option: - // the 'DOWN' reference must be rebuilt as a process reference (the alias the monitoring - // process got), whose pid is monitor_obj, instead of a short reference. + // Like CONTEXT_MONITOR_MONITORED_LOCAL, but the 'DOWN' carries a process reference (an alias). CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS, }; diff --git a/src/libAtomVM/dist_nifs.c b/src/libAtomVM/dist_nifs.c index 6c4a09ec81..9c03b6d468 100644 --- a/src/libAtomVM/dist_nifs.c +++ b/src/libAtomVM/dist_nifs.c @@ -568,10 +568,8 @@ static term nif_erlang_dist_ctrl_put_data(Context *ctx, int argc, term argv[]) RAISE_ERROR(BADARG_ATOM); } term roots[3]; - roots[0] = argv[0]; - roots[1] = argv[1]; // dist handle, ensure it's not garbage collected until we return - // Unlike a pid, the alias reference is a boxed term: keep control rooted across the - // payload decode (which can GC) and re-read the alias from it afterwards. + roots[0] = argv[0]; // dist handle + roots[1] = argv[1]; roots[2] = control; term payload = external_term_from_binary_with_roots(ctx, 1, 1 + bytes_read, &bytes_read, 3, roots); control = roots[2]; @@ -580,9 +578,8 @@ static term nif_erlang_dist_ctrl_put_data(Context *ctx, int argc, term argv[]) int32_t target_process_id = term_process_ref_to_process_id(target); globalcontext_send_message_to_alias(ctx->global, target_process_id, target, payload); } - // else: not a local alias-shaped reference (e.g. minted by a previous incarnation of - // this node, so its creation did not match and it decoded as an external reference): - // drop the message, as a send to a reference that is not an active alias is dropped. + // else: not a local process reference (e.g. an alias minted by a previous incarnation + // of this node), so not an active alias -- drop the message. break; } case OPERATION_SPAWN_REQUEST: { diff --git a/src/libAtomVM/globalcontext.c b/src/libAtomVM/globalcontext.c index 43bdb59590..cc85e4620b 100644 --- a/src/libAtomVM/globalcontext.c +++ b/src/libAtomVM/globalcontext.c @@ -418,9 +418,6 @@ void globalcontext_send_message_to_alias(GlobalContext *glb, int32_t process_id, { Context *p = globalcontext_get_process_lock(glb, process_id); if (p) { - // Post the alias message as a signal carrying {Ref, Message}. The owner validates the alias - // against its own monitor list (in its own context, when draining signals) and either delivers - // the message or drops it. The sender must never touch the target's monitor list directly. BEGIN_WITH_STACK_HEAP(TUPLE_SIZE(2), temp_heap) term tuple = term_alloc_tuple(2, &temp_heap); term_put_tuple_element(tuple, 0, ref); diff --git a/src/libAtomVM/jit.c b/src/libAtomVM/jit.c index a5b95fd3a5..a6afc988be 100644 --- a/src/libAtomVM/jit.c +++ b/src/libAtomVM/jit.c @@ -850,8 +850,7 @@ static bool jit_send(Context *ctx, JITState *jit_state) set_error(ctx, jit_state, 0, BADARG_ATOM); return false; } - // A registered name can only resolve to a local pid or port (register/2 validates this), - // so the lookup result is necessarily one here. + // A registered name always resolves to a local pid or port (register/2 validates this). int local_process_id = term_to_local_process_id(recipient_term); globalcontext_send_message(ctx->global, local_process_id, ctx->x[1]); ctx->x[0] = ctx->x[1]; @@ -863,11 +862,8 @@ static bool jit_send(Context *ctx, JITState *jit_state) globalcontext_send_message_to_alias(ctx->global, process_id, recipient_term, ctx->x[1]); ctx->x[0] = ctx->x[1]; } else { - // A reference that is not a local process reference (short/resource/external ref): the - // message is silently dropped, as OTP drops a send to a non-active-alias reference, but - // send still returns the message. - // TODO: support distributed aliases (external references) by routing the send over - // distribution; for now a message addressed to a remote alias is lost. + // Not a local process reference: drop the message but still return it in x0, as OTP drops + // a send to a non-active-alias reference. Outbound distributed aliases are unsupported. ctx->x[0] = ctx->x[1]; } diff --git a/src/libAtomVM/mailbox.c b/src/libAtomVM/mailbox.c index 9316c89d9f..f9b1be00e1 100644 --- a/src/libAtomVM/mailbox.c +++ b/src/libAtomVM/mailbox.c @@ -386,17 +386,11 @@ static MailboxMessage *process_outer_list(Context *ctx, Mailbox *mbox) MailboxMessage *signal_first = NULL; if (ctx == NULL || ctx->active_alias_count == 0) { - // Fast path, and the common case: the owner has no active alias (or there is no owner - // context, as for ports and at termination), so no alias message can be delivered and - // nothing has to run in received order. Reverse the LIFO list into received order in a - // SINGLE pass, by prepending to each sublist while walking newest-to-oldest. - // active_alias_count is written only by the owner and this runs in the owner's own context, - // so it is stable for the whole batch. An AliasMessageSignal here targets an inactive alias: - // drop it when we own a context (it must never reach the signal loop, which treats - // AliasMessageSignal as unreachable); when ctx is NULL leave it in the signal list for the - // scheduler/termination caller to ignore. A dropped alias message was never delivered, so - // nothing references its term: free it immediately, sweeping its refc binaries, instead of - // parking it on the receiver's heap until the next GC (OTP drops with no heap impact). + // Fast path (the common case): no active alias, or no owner context (ports, termination), + // so no alias message can be delivered. Reverse the LIFO list into received order in a + // single pass, prepending to each sublist. An AliasMessageSignal here targets an inactive + // alias: free it now if we own a context (it must not reach the signal loop, which treats + // it as unreachable), else leave it for the alias-blind caller to drop. while (current) { MailboxMessage *next = current->next; if (ctx != NULL && current->type == AliasMessageSignal) { @@ -414,12 +408,9 @@ static MailboxMessage *process_outer_list(Context *ctx, Mailbox *mbox) current = next; } } else { - // The owner has at least one active alias, so alias side effects (which can deactivate the - // alias, e.g. reply_demonitor) must run in received order, so that when several messages - // target the same alias in one batch, the first received one is delivered and the later ones - // dropped, like OTP. Reverse the LIFO outer list (newest first) into received order before - // doing anything else: this is pure pointer manipulation, with no side effects and no - // allocation. + // At least one active alias: alias side effects can deactivate the alias (e.g. + // reply_demonitor), so they must run in received order -- of several same-batch sends to one + // alias, only the first is delivered, like OTP. Reverse the LIFO list into received order. MailboxMessage *received = NULL; while (current) { MailboxMessage *next = current->next; @@ -428,8 +419,7 @@ static MailboxMessage *process_outer_list(Context *ctx, Mailbox *mbox) current = next; } - // Walk received order (oldest to newest), appending to the normal and signal sublists so - // both keep received order. + // Walk oldest to newest, appending so both sublists keep received order. MailboxMessage *signal_last = NULL; current = received; while (current) { @@ -443,18 +433,13 @@ static MailboxMessage *process_outer_list(Context *ctx, Mailbox *mbox) } normal_last = current; } else if (current->type == AliasMessageSignal) { - // Convert an alias message to a normal message, validating the alias in the owner's - // own context. Walking in received order keeps an alias send ordered with plain - // sends from the same sender (alias and pid sends must not be reordered), and - // resolves repeated sends to the same alias oldest-first. + // Validate the alias (in the owner's own context) and convert to a normal message. term message = context_process_alias_message_signal(ctx, CONTAINER_OF(current, struct TermSignal, base)); if (!term_is_invalid_term(message)) { - // Re-type the signal as a normal message in place: struct TermSignal and struct - // Message share an identical layout (the _Static_asserts above) and the message - // term already lives in this signal's own storage, so nothing is copied. Being - // allocation-free, the conversion can no longer drop a message on out of memory - // -- which matters for reply_demonitor, whose alias deactivation and - // DemonitorSignal have already run inside context_process_alias_message_signal. + // Re-type in place: struct TermSignal and struct Message share a layout + // (static-asserted above) and the message term already lives in this signal's + // storage, so nothing is copied and the conversion cannot fail on OOM -- which + // matters because reply_demonitor's side effects already ran above. Message *converted = CONTAINER_OF(current, Message, base); converted->base.type = NormalMessage; converted->message = message; @@ -466,20 +451,16 @@ static MailboxMessage *process_outer_list(Context *ctx, Mailbox *mbox) } normal_last = &converted->base; } else { - // Inactive alias: the message was never delivered, so nothing references its - // term -- free it immediately (sweeping refc binaries) instead of deferring - // reclamation to the receiver's next GC. + // Inactive alias: never delivered, so nothing references the term -- free it + // now (sweeping refc binaries) instead of leaving it on the heap until GC. mailbox_message_dispose_unsent(CONTAINER_OF(current, Message, base), ctx->global, false); } } else { - // A 'DOWN' that auto-removes a {alias, demonitor} / {alias, reply_demonitor} monitor - // also deactivates its alias (see context_process_monitor_down_signal). Apply just - // that deactivation here, in received order, so an alias send arriving later in this - // SAME batch is dropped like OTP: the alias messages above are converted while the - // outer list is split, before the signal loop ever runs this 'DOWN'. The rest of the - // 'DOWN' (monitor removal, delivering the message) still happens when the signal loop - // calls context_process_monitor_down_signal; context_find_alias returns NULL there by - // then, so that deactivation is an idempotent no-op. + // A 'DOWN' auto-removing a {alias, _} monitor also deactivates its alias. Do that + // here, in received order, so a later same-batch alias send is dropped like OTP + // (alias messages are converted during this split, before the signal loop runs the + // 'DOWN'). The signal loop's own deactivation in context_process_monitor_down_signal + // is then an idempotent no-op. if (current->type == MonitorDownSignal) { struct TermSignal *down_signal = CONTAINER_OF(current, struct TermSignal, base); uint64_t ref_ticks = term_to_ref_ticks(term_get_tuple_element(down_signal->signal_term, 1)); diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 130d9a72bf..c2b43df449 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1335,10 +1335,9 @@ static NativeHandlerResult process_console_message(Context *ctx, term msg) { // msg is not in the port's heap NativeHandlerResult result = NativeContinue; - // Reserve for the largest reply this handler builds on ctx's heap. A gen_call error reply nests - // an error 2-tuple inside the reply 2-tuple (TUPLE_SIZE(2) + TUPLE_SIZE(2) = 6 terms), which is - // more than the io_reply 3-tuple or the close 2-tuple. The port reply helpers allocate without - // their own ensure_free, so this single budget has to cover the whole worst-case path. + // Reserve the worst-case reply on ctx's heap: the gen_call error path nests an error 2-tuple in + // the reply 2-tuple (2 * TUPLE_SIZE(2)), larger than the io_reply or close reply. The port reply + // helpers don't ensure_free, so this one budget must cover every path. if (UNLIKELY(memory_ensure_free_opt(ctx, 2 * TUPLE_SIZE(2), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { fprintf(stderr, "Unable to allocate sufficient memory for console driver.\n"); AVM_ABORT(); @@ -1368,10 +1367,9 @@ static NativeHandlerResult process_console_message(Context *ctx, term msg) term reply = term_alloc_tuple(3, &ctx->heap); term_put_tuple_element(reply, 0, IO_REPLY_ATOM); - // Echo ReplyAs back verbatim (it still lives in msg's storage until this handler - // returns, and port_send_message copies it). Rebuilding it from its ref ticks would - // turn an alias (process reference) into a short reference that the requester's - // receive would not match -- and ReplyAs does not have to be a reference at all. + // Echo ReplyAs (ref) back verbatim: rebuilding it from ref ticks would turn an + // alias into a plain reference the requester's receive would not match, and ReplyAs + // need not be a reference at all. term_put_tuple_element(reply, 1, ref); term_put_tuple_element(reply, 2, OK_ATOM); @@ -1551,10 +1549,9 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free context_monitor_alias_type_t alias_type; term new_pid = term_from_local_process_id(new_ctx->process_id); - // Run every fallible step (option parsing, allocations, result-space reservation) before - // publishing any side effect, so a late failure cannot leave the caller with a - // half-installed link or monitor: destroying the never-published new_ctx would otherwise - // send the caller a spurious {'EXIT', Pid, normal} for a spawn that raised. + // Run every fallible step (option parsing, allocations, result reservation) before publishing + // any side effect, so a late failure leaves no half-installed link or monitor -- destroying the + // never-published new_ctx would otherwise send the caller a spurious {'EXIT', Pid, normal}. struct Monitor *new_link = NULL; struct Monitor *self_link = NULL; struct Monitor *alias_monitor = NULL; @@ -1618,9 +1615,8 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - // Reserve the result tuple/ref space before publishing the monitors, so an OOM cannot leave - // ctx with monitors (and new_ctx queued to run) while the caller gets an exception and never - // receives {Pid, Ref}. GC here is safe: new_pid and ref_data are immediates. + // Reserve the result space before publishing the monitors (see above). GC here is safe: + // new_pid and ref_data are immediates. int res_size = TERM_BOXED_REFERENCE_PROCESS_SIZE + TUPLE_SIZE(2); if (UNLIKELY(memory_ensure_free_opt(ctx, res_size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { monitor_destroy(new_link); @@ -1632,10 +1628,9 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free RAISE_ERROR(OUT_OF_MEMORY_ATOM); } } else if (UNLIKELY(!term_is_invalid_term(monitor_term))) { - // {monitor, BadTerm} where BadTerm is neither a list nor 'true': raise badarg like OTP, - // instead of silently spawning an unmonitored process. No link or monitor has been published - // yet, so free the (possibly allocated) link halves and destroy the not-yet-scheduled - // context; monitor_destroy(NULL) is a no-op. + // {monitor, BadTerm} where BadTerm is neither a list nor 'true': raise badarg like OTP + // instead of spawning an unmonitored process. Nothing is published yet; monitor_destroy(NULL) + // is a no-op. monitor_destroy(new_link); monitor_destroy(self_link); context_destroy(new_ctx); @@ -1889,11 +1884,10 @@ static term nif_erlang_send_2(Context *ctx, int argc, term argv[]) int32_t process_id = term_process_ref_to_process_id(target); globalcontext_send_message_to_alias(glb, process_id, target, argv[1]); } - // else: target is a reference but not a local process reference (a plain/short ref, a resource ref, - // or an external reference). It is silently dropped, as OTP drops a send to a reference that is not - // an active local alias. - // TODO: support distributed aliases (external references) by routing the send over distribution; - // for now a message addressed to a remote alias is lost rather than routed. + // else: a reference that is not a local process reference is silently dropped, as OTP drops a + // send to a non-active-alias reference. + // TODO: route sends to external references over distribution (outbound distributed aliases are + // unsupported; the message is currently lost). return argv[1]; } @@ -5082,16 +5076,14 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) target = NULL; } else { local_process_id = term_to_local_process_id(target_pid); - // Monitoring self installs nothing, like OTP: no monitor (self never sends a DOWN) and, - // with {alias, _}, no alias either -- sends to the returned reference are dropped, - // unalias/1 and demonitor(Ref, [info]) return false. + // Monitoring self installs nothing, like OTP: no monitor and (with {alias, _}) no alias, so + // sends to the returned ref are dropped and unalias/1, demonitor(Ref, [info]) return false. if (UNLIKELY(local_process_id == ctx->process_id)) { if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_SHORT_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - // No alias is installed, so this is always a short ref. term_from_ref_ticks allocates - // exactly TERM_BOXED_REFERENCE_SHORT_SIZE, matching the reservation; term_from_ref_data - // sizes by pid, which the allocation checker can't prove stays short here. + // No alias here, so the result is always a short ref; term_from_ref_ticks matches the + // SHORT_SIZE reservation above. uint64_t ref_ticks = globalcontext_get_ref_ticks(ctx->global); return term_from_ref_ticks(ref_ticks, &ctx->heap); } @@ -5174,10 +5166,9 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - // Reserve the result reference space *before* publishing any monitor/alias state, so an - // out-of-memory here cannot leave the target with a queued MonitorSignal (or ctx with monitors) - // while the caller gets an exception and never receives the reference. GC here is safe: the - // monitor structs hold only an immediate pid and a plain RefData, none reachable from the heap. + // Reserve the result reference *before* publishing any monitor/alias state, so an OOM here + // cannot leave the target with a queued MonitorSignal while the caller gets an exception and + // never receives the reference. GC here is safe: the monitor structs hold only immediates. if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_PROCESS_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { monitor_destroy(alias_monitor); monitor_destroy(self_monitor); @@ -7628,9 +7619,8 @@ static term nif_erlang_crc32_combine_3(Context *ctx, int argc, term argv[]) static term nif_erlang_alias(Context *ctx, int argc, term argv[]) { - // alias/0 behaves as alias([]); the default mode is explicit_unalias. The reply option - // maps onto the reply_demonitor machinery: with no monitor to remove, the alias is just - // deactivated when the first message sent via it is delivered, which is the OTP behavior. + // Default mode is explicit_unalias. The reply option reuses the reply_demonitor machinery: + // with no monitor to remove, the alias is deactivated when the first message via it is delivered. context_monitor_alias_type_t alias_type = ContextMonitorAliasExplicitUnalias; if (argc == 1) { term opts = argv[0]; diff --git a/src/libAtomVM/opcodesswitch.h b/src/libAtomVM/opcodesswitch.h index e867342505..8f871c7ca5 100644 --- a/src/libAtomVM/opcodesswitch.h +++ b/src/libAtomVM/opcodesswitch.h @@ -2386,10 +2386,8 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) TRACE_SEND(ctx, x_regs[0], x_regs[1]); globalcontext_send_message_to_alias(ctx->global, target_process_id, recipient_term, x_regs[1]); } - // else: a reference that is not a local process reference (short/resource/external - // ref) is silently dropped, as OTP drops a send to a non-active-alias reference. - // TODO: support distributed aliases (external references) by routing the send over - // distribution; for now a message addressed to a remote alias is lost. + // else: not a local process reference -- silently dropped, as OTP drops a send + // to a non-active-alias reference. Outbound distributed aliases are unsupported. x_regs[0] = x_regs[1]; } break; diff --git a/src/libAtomVM/otp_socket.c b/src/libAtomVM/otp_socket.c index ffe67fcebf..0daf99e237 100644 --- a/src/libAtomVM/otp_socket.c +++ b/src/libAtomVM/otp_socket.c @@ -1032,10 +1032,8 @@ static term nif_socket_select_read(Context *ctx, int argc, term argv[]) term select_ref_term = argv[1]; if (select_ref_term != UNDEFINED_ATOM) { VALIDATE_VALUE(select_ref_term, term_is_local_reference); - // On the BEAM a process alias is also accepted as a select handle and is echoed back - // verbatim in the select notification. AtomVM stores only the reference ticks, which - // would rebuild the handle as a plain reference that the caller's alias would not - // match, so reject aliases here instead of supporting that corner case. + // A process alias is a valid select handle on the BEAM (echoed back verbatim), but AtomVM + // stores only ref ticks and would echo a plain reference the alias would not match -- reject it. if (UNLIKELY(term_is_process_reference(select_ref_term))) { RAISE_ERROR(BADARG_ATOM); } diff --git a/src/libAtomVM/resources.c b/src/libAtomVM/resources.c index 2645cc458c..c81bae50e0 100644 --- a/src/libAtomVM/resources.c +++ b/src/libAtomVM/resources.c @@ -238,10 +238,8 @@ int enif_select(ErlNifEnv *env, ErlNifEvent event, enum ErlNifSelectFlags mode, if (UNLIKELY(mode & (ERL_NIF_SELECT_READ | ERL_NIF_SELECT_WRITE) && !term_is_local_reference(ref) && ref != UNDEFINED_ATOM)) { return ERL_NIF_SELECT_BADARG; } - // On the BEAM a process alias is also a valid enif_select ref and comes back verbatim in - // the select notification. AtomVM stores only the reference ticks, which would rebuild the - // ref as a plain reference that an alias would not match, so reject aliases instead of - // supporting that corner case. + // A process alias is a valid enif_select ref on the BEAM (echoed back verbatim), but AtomVM + // stores only ref ticks and would echo a plain reference the alias would not match -- reject it. if (UNLIKELY(term_is_process_reference(ref))) { return ERL_NIF_SELECT_BADARG; } diff --git a/src/libAtomVM/term.h b/src/libAtomVM/term.h index 60325ebbf7..7a5c98df5b 100644 --- a/src/libAtomVM/term.h +++ b/src/libAtomVM/term.h @@ -159,10 +159,9 @@ extern "C" { #if TERM_BYTES == 8 #define TERM_BOXED_REFERENCE_PROCESS_SIZE (TERM_BOXED_REFERENCE_SHORT_SIZE + 1) #else -// Enough size would be 3 + 1, but that is the resource reference size on 32-bit, and -// reference types are distinguished by size. Pad the process reference instead of the -// resource reference: process references only exist when aliases are used, while -// resource references are everywhere on the embedded targets. +// Enough size would be 3 + 1, but that is the resource reference size on 32-bit. Pad the +// process reference instead of the resource reference: process references only exist when +// aliases are used, while resource references are everywhere on the embedded targets. #define TERM_BOXED_REFERENCE_PROCESS_SIZE (TERM_BOXED_REFERENCE_SHORT_SIZE + 2) #endif #define TERM_BOXED_REFERENCE_PROCESS_HEADER (((TERM_BOXED_REFERENCE_PROCESS_SIZE - 1) << 6) | TERM_BOXED_REF) diff --git a/tests/erlang_tests/test_binary_to_term.erl b/tests/erlang_tests/test_binary_to_term.erl index 66c0c77811..3a5a8007f1 100644 --- a/tests/erlang_tests/test_binary_to_term.erl +++ b/tests/erlang_tests/test_binary_to_term.erl @@ -999,12 +999,7 @@ test_encode_resource(OTPVersion) -> test_encode_process_ref() -> ProcessRef = erlang:alias(), Bin = term_to_binary(ProcessRef), - %% A process reference serializes as NEWER_REFERENCE_EXT (90) for the - %% nonode@nohost node. The id-word count differs (AtomVM 3 words, the BEAM 5), - %% so the length word and the id bytes that follow it are left unmatched. <<131, 90, _Len:16, 119, 13, "nonode@nohost", 0:32, _/binary>> = Bin, - %% ?MODULE:id/1 keeps the compiler from folding the round-trip - %% binary_to_term(term_to_binary(X)) back to X. ProcessRef = binary_to_term(?MODULE:id(Bin)), ok. diff --git a/tests/libs/eavmlib/test_file.erl b/tests/libs/eavmlib/test_file.erl index f0aff3d21f..480e8f3db2 100644 --- a/tests/libs/eavmlib/test_file.erl +++ b/tests/libs/eavmlib/test_file.erl @@ -29,6 +29,7 @@ test() -> HasExecve = atomvm:platform() =/= emscripten, ok = test_basic_file(), ok = test_fifo_select(HasSelect), + ok = test_alias_select_handle_rejected(HasSelect), ok = test_gc(HasSelect), ok = test_crash_no_leak(HasSelect), ok = test_select_with_gone_process(HasSelect), @@ -66,16 +67,6 @@ test_fifo_select(_HasSelect) -> ok = atomvm:posix_mkfifo(Path, 8#644), {ok, RdFd} = atomvm:posix_open(Path, [o_rdonly]), {ok, WrFd} = atomvm:posix_open(Path, [o_wronly]), - %% A process alias is rejected as a select ref: AtomVM stores only the reference ticks, - %% so the notification could not carry the alias back (the BEAM's enif_select accepts it). - Alias = alias(), - ok = - try atomvm:posix_select_write(WrFd, self(), Alias) of - R -> {unexpected, R} - catch - error:badarg -> ok - end, - true = unalias(Alias), SelectWriteRef = make_ref(), ok = atomvm:posix_select_write(WrFd, self(), SelectWriteRef), ok = @@ -136,6 +127,25 @@ test_fifo_select(_HasSelect) -> ok = atomvm:posix_close(WrFd), ok = atomvm:posix_unlink(Path). +test_alias_select_handle_rejected(false) -> + ok; +test_alias_select_handle_rejected(_HasSelect) -> + Path = "/tmp/atomvm.tmp." ++ integer_to_list(erlang:system_time(millisecond)), + ok = atomvm:posix_mkfifo(Path, 8#644), + {ok, RdFd} = atomvm:posix_open(Path, [o_rdonly]), + {ok, WrFd} = atomvm:posix_open(Path, [o_wronly]), + Alias = alias(), + ok = + try atomvm:posix_select_write(WrFd, self(), Alias) of + R -> {unexpected, R} + catch + error:badarg -> ok + end, + true = unalias(Alias), + ok = atomvm:posix_close(RdFd), + ok = atomvm:posix_close(WrFd), + ok = atomvm:posix_unlink(Path). + % Test is based on the fact that `erlang:memory(binary)` count resources. test_gc(HasSelect) -> Path = "/tmp/atomvm.tmp." ++ integer_to_list(erlang:system_time(millisecond)), diff --git a/tests/libs/estdlib/test_udp_socket.erl b/tests/libs/estdlib/test_udp_socket.erl index 231ada6298..ff57169669 100644 --- a/tests/libs/estdlib/test_udp_socket.erl +++ b/tests/libs/estdlib/test_udp_socket.erl @@ -268,10 +268,6 @@ receive_loop_nowait_ref(Socket, Packet) -> Error end. -%% On the BEAM a process alias is a valid select handle (select_handle() is any reference()) -%% and the select notification carries it back verbatim. AtomVM stores only the reference -%% ticks, which would rebuild the handle as a plain reference the alias would not match, so -%% it rejects aliases as select handles with badarg instead of supporting that corner case. test_alias_select_handle_rejected() -> case erlang:system_info(machine) of "BEAM" -> From 233e4d6d903e4d2411fe98248b8bd71581f47aee Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 16 Jun 2026 14:38:57 +0000 Subject: [PATCH 76/87] Drop em-dashes from the alias comments for a plainer style The comments added by the alias work leaned on `--` em-dashes and a few long sentences joined by `;`, which reads as machine-generated and is out of step with the rest of the codebase. Rewrite them as plain, short sentences: the parenthetical and trailing `--` become periods, commas or parentheses, and the long `;` sentences are split in two. Touches the alias comments in context.c, dist_nifs.c, mailbox.c, nifs.c, opcodesswitch.h, otp_socket.c and resources.c, plus the test_monitor.erl docstrings. The pre-existing base `--` uses (the sub-binary doxygen in term.h, the timeout fall-through comments in opcodesswitch.h) are left alone. Comment-only; no behavior change. Both flavors build and the alias test modules pass. Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 6 +++--- src/libAtomVM/dist_nifs.c | 2 +- src/libAtomVM/mailbox.c | 10 +++++----- src/libAtomVM/nifs.c | 8 ++++---- src/libAtomVM/opcodesswitch.h | 2 +- src/libAtomVM/otp_socket.c | 2 +- src/libAtomVM/resources.c | 2 +- tests/erlang_tests/test_monitor.erl | 20 ++++++++++---------- 8 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index c6ee820ddc..22236e1a36 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -141,9 +141,9 @@ void context_destroy(Context *ctx) scheduler_cancel_timeout(ctx); // Remove the process from the scheduler queue. A process terminated by the scheduler was - // already dequeued (and its queue item was reset, making this a no-op), but a process that - // is destroyed before it was ever scheduled -- e.g. on a spawn_opt option error -- is still - // on the waiting queue, and destroying it without dequeuing would leave a dangling entry. + // already dequeued and its queue item reset, so this is a no-op. A process destroyed before + // it was ever scheduled (e.g. a spawn_opt option error) is still on the waiting queue. + // Dequeuing it here avoids leaving a dangling entry. SMP_SPINLOCK_LOCK(&ctx->global->processes_spinlock); list_remove(&ctx->processes_list_head); SMP_SPINLOCK_UNLOCK(&ctx->global->processes_spinlock); diff --git a/src/libAtomVM/dist_nifs.c b/src/libAtomVM/dist_nifs.c index 9c03b6d468..0be28795b6 100644 --- a/src/libAtomVM/dist_nifs.c +++ b/src/libAtomVM/dist_nifs.c @@ -579,7 +579,7 @@ static term nif_erlang_dist_ctrl_put_data(Context *ctx, int argc, term argv[]) globalcontext_send_message_to_alias(ctx->global, target_process_id, target, payload); } // else: not a local process reference (e.g. an alias minted by a previous incarnation - // of this node), so not an active alias -- drop the message. + // of this node), so not an active alias. Drop the message. break; } case OPERATION_SPAWN_REQUEST: { diff --git a/src/libAtomVM/mailbox.c b/src/libAtomVM/mailbox.c index f9b1be00e1..fd01d423bf 100644 --- a/src/libAtomVM/mailbox.c +++ b/src/libAtomVM/mailbox.c @@ -340,7 +340,7 @@ void mailbox_send_monitor_signal(Context *c, enum MessageType type, struct Monit struct MonitorPointerSignal *monitor_signal = malloc(sizeof(struct MonitorPointerSignal)); if (IS_NULL_PTR(monitor_signal)) { // FIXME (pre-existing): this function returns void, so an out-of-memory here is silently - // dropped -- the monitor is leaked and the caller believes it was installed. It should return + // dropped. The monitor is leaked and the caller believes it was installed. It should return // bool so the caller can free the monitor and raise out_of_memory. fprintf(stderr, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); return; @@ -379,7 +379,7 @@ static MailboxMessage *process_outer_list(Context *ctx, Mailbox *mbox) #else mbox->outer_first = NULL; #endif - // The outer list is LIFO (outer_first is the newest message); split it into the normal and + // The outer list is LIFO (outer_first is the newest message). Split it into the normal and // signal sublists, both in received order (oldest first). MailboxMessage *normal_first = NULL; MailboxMessage *normal_last = NULL; @@ -409,7 +409,7 @@ static MailboxMessage *process_outer_list(Context *ctx, Mailbox *mbox) } } else { // At least one active alias: alias side effects can deactivate the alias (e.g. - // reply_demonitor), so they must run in received order -- of several same-batch sends to one + // reply_demonitor), so they must run in received order. Of several same-batch sends to one // alias, only the first is delivered, like OTP. Reverse the LIFO list into received order. MailboxMessage *received = NULL; while (current) { @@ -438,7 +438,7 @@ static MailboxMessage *process_outer_list(Context *ctx, Mailbox *mbox) if (!term_is_invalid_term(message)) { // Re-type in place: struct TermSignal and struct Message share a layout // (static-asserted above) and the message term already lives in this signal's - // storage, so nothing is copied and the conversion cannot fail on OOM -- which + // storage, so nothing is copied. The conversion cannot fail on OOM, which // matters because reply_demonitor's side effects already ran above. Message *converted = CONTAINER_OF(current, Message, base); converted->base.type = NormalMessage; @@ -451,7 +451,7 @@ static MailboxMessage *process_outer_list(Context *ctx, Mailbox *mbox) } normal_last = &converted->base; } else { - // Inactive alias: never delivered, so nothing references the term -- free it + // Inactive alias: never delivered, so nothing references the term. Free it // now (sweeping refc binaries) instead of leaving it on the heap until GC. mailbox_message_dispose_unsent(CONTAINER_OF(current, Message, base), ctx->global, false); } diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index c2b43df449..98cae414cb 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1550,8 +1550,8 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free term new_pid = term_from_local_process_id(new_ctx->process_id); // Run every fallible step (option parsing, allocations, result reservation) before publishing - // any side effect, so a late failure leaves no half-installed link or monitor -- destroying the - // never-published new_ctx would otherwise send the caller a spurious {'EXIT', Pid, normal}. + // any side effect, so a late failure leaves no half-installed link or monitor. Otherwise, + // destroying the never-published new_ctx would send the caller a spurious {'EXIT', Pid, normal}. struct Monitor *new_link = NULL; struct Monitor *self_link = NULL; struct Monitor *alias_monitor = NULL; @@ -1629,7 +1629,7 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free } } else if (UNLIKELY(!term_is_invalid_term(monitor_term))) { // {monitor, BadTerm} where BadTerm is neither a list nor 'true': raise badarg like OTP - // instead of spawning an unmonitored process. Nothing is published yet; monitor_destroy(NULL) + // instead of spawning an unmonitored process. Nothing is published yet. monitor_destroy(NULL) // is a no-op. monitor_destroy(new_link); monitor_destroy(self_link); @@ -5082,7 +5082,7 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_SHORT_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - // No alias here, so the result is always a short ref; term_from_ref_ticks matches the + // No alias here, so the result is always a short ref. term_from_ref_ticks matches the // SHORT_SIZE reservation above. uint64_t ref_ticks = globalcontext_get_ref_ticks(ctx->global); return term_from_ref_ticks(ref_ticks, &ctx->heap); diff --git a/src/libAtomVM/opcodesswitch.h b/src/libAtomVM/opcodesswitch.h index 8f871c7ca5..fe2d5b5473 100644 --- a/src/libAtomVM/opcodesswitch.h +++ b/src/libAtomVM/opcodesswitch.h @@ -2386,7 +2386,7 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) TRACE_SEND(ctx, x_regs[0], x_regs[1]); globalcontext_send_message_to_alias(ctx->global, target_process_id, recipient_term, x_regs[1]); } - // else: not a local process reference -- silently dropped, as OTP drops a send + // else: not a local process reference. Silently dropped, as OTP drops a send // to a non-active-alias reference. Outbound distributed aliases are unsupported. x_regs[0] = x_regs[1]; } diff --git a/src/libAtomVM/otp_socket.c b/src/libAtomVM/otp_socket.c index 0daf99e237..0de8a61237 100644 --- a/src/libAtomVM/otp_socket.c +++ b/src/libAtomVM/otp_socket.c @@ -1033,7 +1033,7 @@ static term nif_socket_select_read(Context *ctx, int argc, term argv[]) if (select_ref_term != UNDEFINED_ATOM) { VALIDATE_VALUE(select_ref_term, term_is_local_reference); // A process alias is a valid select handle on the BEAM (echoed back verbatim), but AtomVM - // stores only ref ticks and would echo a plain reference the alias would not match -- reject it. + // stores only ref ticks and would echo a plain reference the alias would not match. Reject it. if (UNLIKELY(term_is_process_reference(select_ref_term))) { RAISE_ERROR(BADARG_ATOM); } diff --git a/src/libAtomVM/resources.c b/src/libAtomVM/resources.c index c81bae50e0..47086d2322 100644 --- a/src/libAtomVM/resources.c +++ b/src/libAtomVM/resources.c @@ -239,7 +239,7 @@ int enif_select(ErlNifEnv *env, ErlNifEvent event, enum ErlNifSelectFlags mode, return ERL_NIF_SELECT_BADARG; } // A process alias is a valid enif_select ref on the BEAM (echoed back verbatim), but AtomVM - // stores only ref ticks and would echo a plain reference the alias would not match -- reject it. + // stores only ref ticks and would echo a plain reference the alias would not match. Reject it. if (UNLIKELY(term_is_process_reference(ref))) { return ERL_NIF_SELECT_BADARG; } diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 04ea9b9d3d..0e5b657c50 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -472,7 +472,7 @@ test_monitor_alias_noproc_returns_alias() -> ok. %% monitor(process, self(), [{alias, _}]) installs nothing, like OTP: no monitor and no active -%% alias -- sends to the returned reference are dropped, unalias/1 and demonitor(Ref, [info]) +%% alias. Sends to the returned reference are dropped, unalias/1 and demonitor(Ref, [info]) %% return false. (Verified against OTP 29.) test_monitor_alias_self_installs_nothing() -> Mon = erlang:monitor(process, self(), [{alias, explicit_unalias}]), @@ -484,7 +484,7 @@ test_monitor_alias_self_installs_nothing() -> %% spawn_opt(F, [link, {monitor, [Bad]}]) must raise badarg atomically, like OTP: no process is %% spawned and no link is left behind. The link is installed before the monitor options are -%% parsed, so it must not survive the error -- or the caller would keep a link to a destroyed +%% parsed, so it must not survive the error. Otherwise the caller would keep a link to a destroyed %% pid and receive a spurious {'EXIT', Pid, normal} for a spawn that raised. test_spawn_opt_link_monitor_badarg_is_atomic() -> %% On the BEAM the test process is linked to init, so compare against the initial links @@ -535,7 +535,7 @@ test_monitor_alias_dead_process() -> ok. %% spawn_opt(F, [{monitor, BadTerm}]) where BadTerm is neither a list nor 'true' must raise badarg, -%% like OTP -- not silently spawn an unmonitored process. Distinct from +%% like OTP, not silently spawn an unmonitored process. Distinct from %% {monitor, [BadOption]}, which fails inside the monitor-option parser. test_spawn_opt_monitor_non_list_badarg() -> ok = @@ -555,10 +555,10 @@ test_spawn_opt_monitor_non_list_badarg() -> %% A 'DOWN' that deactivates a {alias, demonitor} alias must drop an alias send that lands in the %% SAME mailbox drain, even though alias messages are converted before the 'DOWN' signal is applied: %% the deactivation now runs in received order while the outer list is split. A relay forces both -%% into one batch -- the owner busy-waits on whereis/1 (which does not drain its mailbox), so its +%% into one batch. The owner busy-waits on whereis/1 (which does not drain its mailbox), so its %% first receive drains the 'DOWN' (received first) together with the later alias send. -%% On a single-scheduler build the owner may drain the 'DOWN' alone before the relay runs; -%% the test then still passes through the cross-batch deactivation path -- the same-batch +%% On a single-scheduler build the owner may drain the 'DOWN' alone before the relay runs. +%% The test then still passes through the cross-batch deactivation path. The same-batch %% path is reliably exercised on SMP builds (a schedule-in drain is a VM property no Erlang %% code can pin down). test_monitor_alias_down_before_send_same_batch() -> @@ -609,7 +609,7 @@ test_unalias_and_send_non_local_refs() -> ok. %% The io protocol echoes ReplyAs back verbatim: an alias passed as ReplyAs must come back as -%% the very same reference -- not as a short reference rebuilt from its ticks, which would not +%% the very same reference, not as a short reference rebuilt from its ticks, which would not %% match it. (Verified against OTP 29.) test_io_request_alias_reply() -> %% On the BEAM the group leader is a full io server; on AtomVM the test process has no @@ -770,7 +770,7 @@ stale_alias_probe(Parent) -> end. %% Several senders hammer one alias while the owner unaliases mid-stream. All synchronization -%% is per-sender send order and explicit acks -- no sleeps and no timing windows, so the test +%% is per-sender send order and explicit acks. No sleeps and no timing windows, so the test %% cannot flake on slow or loaded hosts. Phase 1 pins exact delivery while the alias is %% active: a sender's alias messages all precede its sent_all fence in the owner's queue, so %% once the last fence is consumed every alias message has been counted. Phase 2 races @@ -866,7 +866,7 @@ test_unalias_non_reference_badarg() -> %% decoding must reject pid 0 (INVALID_PROCESS_ID, the short-ref sentinel) and pids above the %% 28-bit maximum. Not a BEAM divergence any real term can show: term_to_binary can never %% produce these bytes, and the BEAM treats reference words as opaque payload while AtomVM -%% gives the third word pid semantics -- so only AtomVM has anything to validate. On the BEAM +%% gives the third word pid semantics, so only AtomVM has anything to validate. On the BEAM %% the patched binaries decode as plain references, which the test asserts there. test_binary_to_term_invalid_process_ref() -> A = erlang:alias(), @@ -929,7 +929,7 @@ recv_one() -> end. %% Assert that no message arrives. Only call this when the would-be message is already -%% settled (delivered or dropped) -- behind a fence reply or after a same-process send -- +%% settled (delivered or dropped), behind a fence reply or after a same-process send, %% so the short window is not a race. assert_no_message() -> receive From bbb154755e3d358fdfeed731fbe905e246e30c2b Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 16 Jun 2026 14:50:31 +0000 Subject: [PATCH 77/87] Name the active_alias_count saturation sentinel The saturation value 0xFF appeared as a bare literal in the three active_alias_count guards, which is easy to misread. Give it a name, ACTIVE_ALIAS_COUNT_SATURATED, defined in context.c next to the other file-local constants, and carry the saturation rationale (why saturate instead of wrapping to 0) in the comment on the define. The field comment in context.h shrinks to the essentials: it is an optimization and it saturates at a sticky sentinel, with a pointer to the define. No behavior change. Both flavors build and test_monitor passes. Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 11 ++++++++--- src/libAtomVM/context.h | 5 ++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index 22236e1a36..ce5f3377be 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -53,6 +53,11 @@ #define DEFAULT_STACK_SIZE 8 #define BYTES_PER_TERM (TERM_BITS / 8) +// active_alias_count saturates at this value instead of wrapping to 0. A wrap would make +// context_find_alias skip the list walk and silently drop every alias of the process. Once +// saturated, the count is never incremented or decremented again. +#define ACTIVE_ALIAS_COUNT_SATURATED 0xFF + static struct Monitor *context_monitors_handle_terminate(Context *ctx); static void context_distribution_handle_terminate(Context *ctx); static void destroy_extended_registers(Context *ctx, unsigned int live); @@ -883,7 +888,7 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) } case CONTEXT_MONITOR_ALIAS: { struct MonitorAlias *alias = CONTAINER_OF(monitor, struct MonitorAlias, monitor); - if (LIKELY(ctx->active_alias_count != 0xFF)) { + if (LIKELY(ctx->active_alias_count != ACTIVE_ALIAS_COUNT_SATURATED)) { ctx->active_alias_count--; } free(alias); @@ -1088,7 +1093,7 @@ bool context_add_monitor(Context *ctx, struct Monitor *new_monitor) } list_append(&ctx->monitors_head, &new_monitor->monitor_list_head); if (new_monitor->monitor_type == CONTEXT_MONITOR_ALIAS) { - if (LIKELY(ctx->active_alias_count < 0xFF)) { + if (LIKELY(ctx->active_alias_count < ACTIVE_ALIAS_COUNT_SATURATED)) { ctx->active_alias_count++; } } @@ -1271,7 +1276,7 @@ void context_unalias(Context *ctx, struct MonitorAlias *alias) { TERM_DEBUG_ASSERT(alias != NULL); TERM_DEBUG_ASSERT(ctx->active_alias_count > 0); - if (LIKELY(ctx->active_alias_count != 0xFF)) { + if (LIKELY(ctx->active_alias_count != ACTIVE_ALIAS_COUNT_SATURATED)) { ctx->active_alias_count--; } struct Monitor *monitor = &alias->monitor; diff --git a/src/libAtomVM/context.h b/src/libAtomVM/context.h index c54abbe4a9..65dac010f0 100644 --- a/src/libAtomVM/context.h +++ b/src/libAtomVM/context.h @@ -138,9 +138,8 @@ struct Context unsigned int leader : 1; unsigned int has_min_heap_size : 1; unsigned int has_max_heap_size : 1; - // Number of active aliases on monitors_head; lets alias lookups short-circuit for the - // common alias-free process. Owner-written only. Saturates at 0xFF rather than wrapping to - // 0, which would skip the lookups and silently deactivate every alias of the process. + // Optimization: count of active aliases, so alias lookups can short-circuit for the common + // alias-free process. Saturates at a sticky sentinel (ACTIVE_ALIAS_COUNT_SATURATED, context.c). unsigned int active_alias_count : 8; bool trap_exit : 1; From 91cb08dd141907db8cb957d15fc7a0137463189e Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 17 Jun 2026 08:58:19 +0000 Subject: [PATCH 78/87] Name the expected control arity in the alias-send dist handler The arity check for DOP_ALIAS_SEND / DOP_ALIAS_SEND_TT inlined the expected value as a ternary inside the condition, on a long hard-to-scan line. Pull it out into a named expected_arity (size_t, matching arity so the comparison stays sign-clean) so the check reads plainly: DOP_ALIAS_SEND carries 3 control elements, DOP_ALIAS_SEND_TT 4 (the extra trace token). No behavior change. Both flavors build and test_net_kernel (the inbound DOP_ALIAS_SEND test) passes. Signed-off-by: Davide Bettio --- src/libAtomVM/dist_nifs.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libAtomVM/dist_nifs.c b/src/libAtomVM/dist_nifs.c index 0be28795b6..b5dc23d227 100644 --- a/src/libAtomVM/dist_nifs.c +++ b/src/libAtomVM/dist_nifs.c @@ -564,7 +564,8 @@ static term nif_erlang_dist_ctrl_put_data(Context *ctx, int argc, term argv[]) case OPERATION_ALIAS_SEND_TT: { // {DOP_ALIAS_SEND, FromPid, Alias} or {DOP_ALIAS_SEND_TT, FromPid, Alias, TraceToken}, // followed by the message payload. The trace token is ignored. - if (UNLIKELY(arity != (term_to_int(operation) == OPERATION_ALIAS_SEND ? 3 : 4))) { + size_t expected_arity = (term_to_int(operation) == OPERATION_ALIAS_SEND) ? 3 : 4; + if (UNLIKELY(arity != expected_arity)) { RAISE_ERROR(BADARG_ATOM); } term roots[3]; From bc870f92bacee25e14a9cf08905b64f22a266f0b Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 17 Jun 2026 09:04:42 +0000 Subject: [PATCH 79/87] Shorten the mailbox_send_monitor_signal allocation-failure FIXME The FIXME spelled out the leak and the suggested fix over three lines and used the "pre-existing" review tag, which does not belong in a code comment. Reduce it to a one-line note of the actual defect: the void return hides the failure from the caller. Comment-only; no behavior change. Signed-off-by: Davide Bettio --- src/libAtomVM/mailbox.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libAtomVM/mailbox.c b/src/libAtomVM/mailbox.c index fd01d423bf..fd7cba944b 100644 --- a/src/libAtomVM/mailbox.c +++ b/src/libAtomVM/mailbox.c @@ -339,9 +339,7 @@ void mailbox_send_monitor_signal(Context *c, enum MessageType type, struct Monit { struct MonitorPointerSignal *monitor_signal = malloc(sizeof(struct MonitorPointerSignal)); if (IS_NULL_PTR(monitor_signal)) { - // FIXME (pre-existing): this function returns void, so an out-of-memory here is silently - // dropped. The monitor is leaked and the caller believes it was installed. It should return - // bool so the caller can free the monitor and raise out_of_memory. + // FIXME this function returns void, so the caller is not told the allocation failed fprintf(stderr, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); return; } From cd94f03587b64068160d8ae2bd493dc6205d4e0b Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 17 Jun 2026 10:02:08 +0000 Subject: [PATCH 80/87] Decouple the alias-blind mailbox drain from Context process_outer_list took both a Context and a Mailbox, with ctx == NULL used as an alias-blind mode flag. The two arguments had to agree: a caller could pass process_outer_list(ctx1, &ctx2->mailbox) and nothing would catch it. The fast path used only active_alias_count and global, never the whole Context. Split the worker into its two real operations. mailbox_process_outer_list now takes a Mailbox again and is free of any Context dependency: it is the alias-blind drain used by ports, teardown and the crashdump, which leave any alias signal in the signal list for the caller to drop. mailbox_process_outer_list_with_aliases takes the Context and derives its own mailbox, so the mismatched-pair case is gone. The shared mechanics, the CAS detach and the inner-list splice, move into two static inline helpers. The alias-active slow path still needs the whole Context because it mutates the alias table. The hot path stays a single pass and the helpers are inlined, so the generated code is equivalent. No behavior change; both JIT flavors pass the full suite and the full valgrind run is clean. Signed-off-by: Davide Bettio --- src/libAtomVM/mailbox.c | 124 +++++++++++++++++++++++++--------------- 1 file changed, 77 insertions(+), 47 deletions(-) diff --git a/src/libAtomVM/mailbox.c b/src/libAtomVM/mailbox.c index fd7cba944b..f3afb98275 100644 --- a/src/libAtomVM/mailbox.c +++ b/src/libAtomVM/mailbox.c @@ -367,9 +367,10 @@ void mailbox_reset(Mailbox *mbox) mbox->receive_pointer_prev = NULL; } -static MailboxMessage *process_outer_list(Context *ctx, Mailbox *mbox) +// CAS-empty the outer list and return its raw head. The outer list is LIFO, so the head is the +// newest message and each message is older than its predecessor. +static inline MailboxMessage *detach_outer_list(Mailbox *mbox) { - // Empty outer list using CAS MailboxMessage *current = mbox->outer_first; #if !defined(AVM_NO_SMP) || defined(AVM_TASK_DRIVER_ENABLED) while (!ATOMIC_COMPARE_EXCHANGE_WEAK_PTR(&mbox->outer_first, ¤t, NULL)) { @@ -377,21 +378,85 @@ static MailboxMessage *process_outer_list(Context *ctx, Mailbox *mbox) #else mbox->outer_first = NULL; #endif - // The outer list is LIFO (outer_first is the newest message). Split it into the normal and - // signal sublists, both in received order (oldest first). + return current; +} + +// Append a received-order run of normal messages, from first (oldest) to last (newest), at the end +// of the inner list, restoring the receive pointer when the inner list had been fully consumed. +// first and last are NULL together when no normal message was collected, making this a no-op. +static inline void append_normal_messages(Mailbox *mbox, MailboxMessage *first, MailboxMessage *last) +{ + if (last == NULL) { + return; + } + + // With no receive_pointer, it becomes the new list head. + if (mbox->receive_pointer == NULL) { + mbox->receive_pointer = first; + // If we had a prev, set the prev's next to the new current. + if (mbox->receive_pointer_prev) { + mbox->receive_pointer_prev->next = first; + } else if (mbox->inner_first == NULL) { + // If we had no first, this is the first message. + mbox->inner_first = first; + } + } + + // Append the new items at the end of the inner list. mbox->inner_last may be + // mbox->receive_pointer_prev, which is then updated a second time here. + if (mbox->inner_last) { + mbox->inner_last->next = first; + } + mbox->inner_last = last; +} + +MailboxMessage *mailbox_process_outer_list(Mailbox *mbox) +{ + MailboxMessage *current = detach_outer_list(mbox); + + // Reverse the LIFO list into received order (oldest first), splitting it into a normal sublist + // and a signal sublist in one pass. This entry point is alias-blind: an AliasMessageSignal is + // kept in the signal list for the caller to drop. Its callers (ports, teardown and crashdump) + // never own an active alias. MailboxMessage *normal_first = NULL; MailboxMessage *normal_last = NULL; MailboxMessage *signal_first = NULL; + while (current) { + MailboxMessage *next = current->next; + if (current->type == NormalMessage) { + if (normal_last == NULL) { + normal_last = current; + } + current->next = normal_first; + normal_first = current; + } else { + current->next = signal_first; + signal_first = current; + } + current = next; + } + + append_normal_messages(mbox, normal_first, normal_last); + return signal_first; +} - if (ctx == NULL || ctx->active_alias_count == 0) { - // Fast path (the common case): no active alias, or no owner context (ports, termination), - // so no alias message can be delivered. Reverse the LIFO list into received order in a - // single pass, prepending to each sublist. An AliasMessageSignal here targets an inactive - // alias: free it now if we own a context (it must not reach the signal loop, which treats - // it as unreachable), else leave it for the alias-blind caller to drop. +MailboxMessage *mailbox_process_outer_list_with_aliases(Context *ctx) +{ + Mailbox *mbox = &ctx->mailbox; + MailboxMessage *current = detach_outer_list(mbox); + + MailboxMessage *normal_first = NULL; + MailboxMessage *normal_last = NULL; + MailboxMessage *signal_first = NULL; + + if (ctx->active_alias_count == 0) { + // Fast path (the common case): no active alias, so no alias message can be delivered. Same + // single-pass split as mailbox_process_outer_list, except a stale AliasMessageSignal (its + // alias is inactive) is freed now so it does not reach the signal loop, which would treat + // it as unreachable. while (current) { MailboxMessage *next = current->next; - if (ctx != NULL && current->type == AliasMessageSignal) { + if (current->type == AliasMessageSignal) { mailbox_message_dispose_unsent(CONTAINER_OF(current, Message, base), ctx->global, false); } else if (current->type == NormalMessage) { if (normal_last == NULL) { @@ -479,45 +544,10 @@ static MailboxMessage *process_outer_list(Context *ctx, Mailbox *mbox) } } - // If we enqueued some normal messages, normal_first is the head (oldest received) and - // normal_last is the tail (newest received). Splice them at the end of the inner list. - if (normal_last) { - // normal_first is the new list head. - // If we had no receive_pointer, it should be this list head. - if (mbox->receive_pointer == NULL) { - mbox->receive_pointer = normal_first; - // If we had a prev, set the prev's next to the new current. - if (mbox->receive_pointer_prev) { - mbox->receive_pointer_prev->next = normal_first; - } else if (mbox->inner_first == NULL) { - // If we had no first, this is the first message. - mbox->inner_first = normal_first; - } - } - - // Update last and previous last's next. - // Append these new items at the end of inner list. - if (mbox->inner_last) { - // This may be mbox->receive_pointer_prev which we - // are updating a second time here. - mbox->inner_last->next = normal_first; - } - mbox->inner_last = normal_last; - } - + append_normal_messages(mbox, normal_first, normal_last); return signal_first; } -MailboxMessage *mailbox_process_outer_list(Mailbox *mbox) -{ - return process_outer_list(NULL, mbox); -} - -MailboxMessage *mailbox_process_outer_list_with_aliases(Context *ctx) -{ - return process_outer_list(ctx, &ctx->mailbox); -} - void mailbox_next(Mailbox *mbox) { // This is called from OP_LOOP_REC_END opcode, so we cannot make any From 9a72db5c38383cac783cf4319680f106d2d8c8bd Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 17 Jun 2026 10:28:36 +0000 Subject: [PATCH 81/87] Trim comments that restate a contract or nearby comment A few comments left by the alias work restated something the reader already has: a function's own doxygen, a neighbouring comment, or the size macro a few lines up. Drop the redundant part and keep whatever is genuinely non-obvious. - mailbox.h: mailbox_process_outer_list already returns "the signal messages", so it need not add that alias messages come back as signals. - mailbox.c: the alias-blind drain need not restate where an alias signal goes, only why the simple handling is safe (its callers never own an active alias). - nifs.c: the self-monitor branch already says no alias is installed, and a SHORT_SIZE reservation followed by term_from_ref_ticks is the usual pattern. - term.h: the process-reference pad-init comment keeps only the GC reason. The size rationale already lives on TERM_BOXED_REFERENCE_PROCESS_SIZE. - erlang.erl: send/2 already lists an alias as a Target, so the doc keeps only the non-obvious rule that a send to an inactive alias is dropped. - test_monitor.erl: the ordering test's header already lists every property, so two inline comments that repeated it are gone (those adding the tie-break and round-trip detail stay). Comment and documentation only; no behavior change. Signed-off-by: Davide Bettio --- libs/estdlib/src/erlang.erl | 5 ++--- src/libAtomVM/mailbox.c | 5 ++--- src/libAtomVM/mailbox.h | 2 +- src/libAtomVM/nifs.c | 2 -- src/libAtomVM/term.h | 5 ++--- tests/erlang_tests/test_monitor.erl | 2 -- 6 files changed, 7 insertions(+), 14 deletions(-) diff --git a/libs/estdlib/src/erlang.erl b/libs/estdlib/src/erlang.erl index 80234dd206..99872a94a0 100644 --- a/libs/estdlib/src/erlang.erl +++ b/libs/estdlib/src/erlang.erl @@ -1292,9 +1292,8 @@ make_ref() -> %% @param Target process, registered name or alias to send the message to %% @param Message message to send %% @returns the sent message -%% @doc Send a message to a given process. The target may also be an alias -%% reference created with `alias/0' or `monitor/3'; a message sent to -%% a reference that is not an active alias is silently dropped. +%% @doc Send a message to a given process. A message sent to a reference +%% that is not an active alias is silently dropped. %% @end %%----------------------------------------------------------------------------- -spec send(Target :: send_destination(), Message :: Message) -> Message. diff --git a/src/libAtomVM/mailbox.c b/src/libAtomVM/mailbox.c index f3afb98275..4fe636b17e 100644 --- a/src/libAtomVM/mailbox.c +++ b/src/libAtomVM/mailbox.c @@ -415,9 +415,8 @@ MailboxMessage *mailbox_process_outer_list(Mailbox *mbox) MailboxMessage *current = detach_outer_list(mbox); // Reverse the LIFO list into received order (oldest first), splitting it into a normal sublist - // and a signal sublist in one pass. This entry point is alias-blind: an AliasMessageSignal is - // kept in the signal list for the caller to drop. Its callers (ports, teardown and crashdump) - // never own an active alias. + // and a signal sublist in one pass. This entry is alias-blind: its callers (ports, teardown and + // crashdump) never own an active alias. MailboxMessage *normal_first = NULL; MailboxMessage *normal_last = NULL; MailboxMessage *signal_first = NULL; diff --git a/src/libAtomVM/mailbox.h b/src/libAtomVM/mailbox.h index 77cbad7d54..45b9932829 100644 --- a/src/libAtomVM/mailbox.h +++ b/src/libAtomVM/mailbox.h @@ -214,7 +214,7 @@ size_t mailbox_size(Mailbox *mbox); /** * @brief Process the outer list of messages. * - * @details To be called from the process only. Alias messages (if any) are returned as signals. + * @details To be called from the process only. * @param mbox the mailbox to work with * @return the signal messages in received order. */ diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 98cae414cb..1f6e57778b 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -5082,8 +5082,6 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_SHORT_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - // No alias here, so the result is always a short ref. term_from_ref_ticks matches the - // SHORT_SIZE reservation above. uint64_t ref_ticks = globalcontext_get_ref_ticks(ctx->global); return term_from_ref_ticks(ref_ticks, &ctx->heap); } diff --git a/src/libAtomVM/term.h b/src/libAtomVM/term.h index 7a5c98df5b..1175d7f9dc 100644 --- a/src/libAtomVM/term.h +++ b/src/libAtomVM/term.h @@ -2286,9 +2286,8 @@ static inline term term_make_process_reference(int32_t process_id, uint64_t ref_ #endif boxed_value[REFERENCE_PROCESS_PID_OFFSET] = process_id; #if TERM_BYTES == 4 - // On 32-bit the process reference is one word larger than header + ticks + pid (so reference - // shapes stay size-distinguishable); initialize the trailing padding word so GC copies a - // defined value instead of uninitialized memory. + // Initialize the trailing padding word so GC copies a defined value instead of uninitialized + // memory. boxed_value[REFERENCE_PROCESS_PID_OFFSET + 1] = term_nil(); #endif diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 0e5b657c50..766289eb2a 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -85,7 +85,6 @@ start() -> %% direction follows the internal pid (not the pid term order) and is %% implementation defined, so it is not pinned here. test_alias_ref_ordering() -> - %% A plain reference sorts before an alias, whichever was created first. R0 = make_ref(), A0 = erlang:alias(), true = R0 < A0, @@ -98,7 +97,6 @@ test_alias_ref_ordering() -> true = Ea < Eb, %% The pid word is encoded and decoded: a process reference round-trips equal. true = Eb =:= binary_to_term(term_to_binary(Eb)), - %% Two owners' aliases are distinct and strictly ordered (the pid word participates). Parent = self(), Child = spawn_opt(fun() -> receive {get, P} -> P ! {child_alias, erlang:alias()} end From b54621ff9f14104943bd5530ee2c6e3c9722a9ac Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 17 Jun 2026 10:57:24 +0000 Subject: [PATCH 82/87] Cut over-narrating comments in the alias tests The alias tests carried many comments that narrate what the next lines or the test function name already say (e.g. a header restating the function name, or a note describing the very send/receive below it). Keep comments minimal: only facts a reader cannot infer from the code stay. Kept across the suite: the timing and ordering rationales (why a fence makes an `after 0` race-free, why a selective receive leaves a 'DOWN' queued), the BEAM-vs-AtomVM divergences (stale 'EXIT' under trap_exit, the eval process linked to init, the group-leader setup, opaque reference words), the magic-value choices (the large spin bound, the refc-binary size that forces an mso sweep), and the monotonic-pid plus unique-ticks invariant the stale-alias test relies on. - test_net_kernel.erl: four narration blocks reduced to the one fence + stale 'EXIT' note. - test_monitor.erl: 17 comment blocks dropped, 15 tightened. Comment only; no behavior change. Signed-off-by: Davide Bettio --- tests/erlang_tests/test_monitor.erl | 143 +++++++++---------------- tests/libs/estdlib/test_net_kernel.erl | 15 +-- 2 files changed, 50 insertions(+), 108 deletions(-) diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 766289eb2a..686e4b8463 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -77,13 +77,10 @@ start() -> ok = test_alias_ref_ordering(), 0. -%% Reference term order around aliases, matching OTP (probed on OTP 29; this -%% module also runs on the BEAM). An alias is a process reference: it carries the -%% owner pid, so it sorts after every plain reference whichever was created first, -%% aliases of one owner order by creation, and the owner pid takes part in the -%% order so two owners' aliases are distinct and strictly ordered. That last -%% direction follows the internal pid (not the pid term order) and is -%% implementation defined, so it is not pinned here. +%% An alias sorts after every plain reference whichever was created first. Two +%% owners' aliases are distinct and strictly ordered, but that direction follows +%% the internal pid (not pid term order) and is implementation defined, so it is +%% not pinned here. test_alias_ref_ordering() -> R0 = make_ref(), A0 = erlang:alias(), @@ -91,11 +88,9 @@ test_alias_ref_ordering() -> A1 = erlang:alias(), R1 = make_ref(), true = R1 < A1, - %% Same owner: a later alias is greater than an earlier one (ticks break the tie). Ea = erlang:alias(), Eb = erlang:alias(), true = Ea < Eb, - %% The pid word is encoded and decoded: a process reference round-trips equal. true = Eb =:= binary_to_term(term_to_binary(Eb)), Parent = self(), Child = spawn_opt(fun() -> @@ -375,26 +370,21 @@ test_monitor_alias_reply_demonitor(SpawnFun) -> P ! quit, ok. -%% reply_demonitor must, on the first reply through the alias, also remove the underlying monitor -%% (like demonitor/1), so no 'DOWN' is delivered when the monitored process later exits. test_reply_demonitor_removes_monitor(SpawnFun) -> {P, Mon} = SpawnFun(fun echo_loop/0, [{alias, reply_demonitor}]), Ref = make_ref(), P ! {{reply, Ref}, Mon}, {reply, Ref} = recv_one(), - %% Fence: monitors fire in installation order, so if the reply had not removed the - %% monitor, its 'DOWN' would be enqueued before the fence's and recv_one would return - %% it first. (On the BEAM the buggy 'DOWN' does not exist, so order is irrelevant.) + %% Monitors fire in installation order, so a stale 'DOWN' from a not-removed monitor + %% would arrive before this fence's. On the BEAM no such 'DOWN' exists. Fence = monitor(process, P), P ! quit, {'DOWN', Fence, process, P, normal} = recv_one(), ok = assert_no_message(), ok. -%% reply_demonitor must resolve two sends to the same alias in one batch in RECEIVED order: -%% deliver the FIRST and drop the SECOND (the first delivery deactivates the alias), like OTP. -%% Self-sending to our own reply_demonitor alias and only then receiving guarantees both alias -%% signals are drained in a single outer-list batch, so this is deterministic on SMP and non-SMP. +%% Self-sending to our own alias and only then receiving guarantees both alias signals +%% drain in a single outer-list batch, so this is deterministic on SMP and non-SMP. test_reply_demonitor_same_batch_order() -> P = spawn_opt(fun echo_loop/0, []), Mon = erlang:monitor(process, P, [{alias, reply_demonitor}]), @@ -415,24 +405,20 @@ test_monitor_down_alias(SpawnFun) -> {'DOWN', Mon, process, P, normal} = recv_one(), ok. -%% {alias, demonitor}: when the monitor is auto-removed on 'DOWN' delivery, the alias must be -%% deactivated too, so a third party can no longer reach us through it. test_monitor_alias_demonitor_deactivates_on_down(SpawnFun) -> {P, Mon} = SpawnFun(fun echo_loop/0, [{alias, demonitor}]), P ! quit, {'DOWN', Mon, process, P, normal} = recv_one(), Echo = spawn_opt(fun echo_loop/0, []), Echo ! {should_drop, Mon}, - %% Fence through the same echo: sends from one process keep their order, so once the - %% fence reply arrives the dropped alias message can no longer show up afterwards. + %% Sends from one process keep their order, so once the fence reply arrives a dropped + %% alias message can no longer show up afterwards. Echo ! {fence, self()}, fence = recv_one(), ok = assert_no_message(), Echo ! quit, ok. -%% Messages from one sender to one receiver must keep send order, even when one goes via an alias -%% (signal path) and the next via the pid (normal message). test_alias_pid_send_order() -> Parent = self(), P = spawn_opt( @@ -451,8 +437,6 @@ test_alias_pid_send_order() -> {got, m2} = recv_one(), ok. -%% monitor(process, DeadPid, [{alias, explicit_unalias}]) must still return a usable alias (and an -%% immediate noproc DOWN), like OTP, not a plain reference. test_monitor_alias_noproc_returns_alias() -> {P, _} = spawn_opt(fun() -> ok end, [monitor]), ok = @@ -469,9 +453,6 @@ test_monitor_alias_noproc_returns_alias() -> Echo ! quit, ok. -%% monitor(process, self(), [{alias, _}]) installs nothing, like OTP: no monitor and no active -%% alias. Sends to the returned reference are dropped, unalias/1 and demonitor(Ref, [info]) -%% return false. (Verified against OTP 29.) test_monitor_alias_self_installs_nothing() -> Mon = erlang:monitor(process, self(), [{alias, explicit_unalias}]), Mon ! hello, @@ -480,10 +461,8 @@ test_monitor_alias_self_installs_nothing() -> false = erlang:demonitor(Mon, [info]), ok. -%% spawn_opt(F, [link, {monitor, [Bad]}]) must raise badarg atomically, like OTP: no process is -%% spawned and no link is left behind. The link is installed before the monitor options are -%% parsed, so it must not survive the error. Otherwise the caller would keep a link to a destroyed -%% pid and receive a spurious {'EXIT', Pid, normal} for a spawn that raised. +%% The link is installed before the monitor options are parsed, so the badarg must +%% still unwind it: a surviving link would later deliver a spurious {'EXIT', Pid, normal}. test_spawn_opt_link_monitor_badarg_is_atomic() -> %% On the BEAM the test process is linked to init, so compare against the initial links %% instead of []. @@ -532,9 +511,8 @@ test_monitor_alias_dead_process() -> {'DOWN', Mon3, process, P, noproc} = recv_one(), ok. -%% spawn_opt(F, [{monitor, BadTerm}]) where BadTerm is neither a list nor 'true' must raise badarg, -%% like OTP, not silently spawn an unmonitored process. Distinct from -%% {monitor, [BadOption]}, which fails inside the monitor-option parser. +%% A non-list, non-'true' monitor value fails before the monitor-option parser, unlike +%% {monitor, [BadOption]}, so it is exercised separately here. test_spawn_opt_monitor_non_list_badarg() -> ok = try spawn_opt(fun() -> ok end, [{monitor, foo}]) of @@ -550,19 +528,16 @@ test_spawn_opt_monitor_non_list_badarg() -> end, ok. -%% A 'DOWN' that deactivates a {alias, demonitor} alias must drop an alias send that lands in the -%% SAME mailbox drain, even though alias messages are converted before the 'DOWN' signal is applied: -%% the deactivation now runs in received order while the outer list is split. A relay forces both -%% into one batch. The owner busy-waits on whereis/1 (which does not drain its mailbox), so its -%% first receive drains the 'DOWN' (received first) together with the later alias send. -%% On a single-scheduler build the owner may drain the 'DOWN' alone before the relay runs. -%% The test then still passes through the cross-batch deactivation path. The same-batch -%% path is reliably exercised on SMP builds (a schedule-in drain is a VM property no Erlang -%% code can pin down). +%% A 'DOWN' that deactivates a {alias, demonitor} alias must drop an alias send that lands +%% in the SAME mailbox drain. The relay sends the alias message only after seeing the owner's +%% 'DOWN', so both reach the owner in one batch. The owner busy-waits on whereis/1 rather than +%% receiving, because a receive would drain its mailbox before the batch is assembled. +%% On a single scheduler the owner may drain the 'DOWN' alone first. The test then passes +%% through the cross-batch deactivation path instead. The same-batch path is reliably +%% exercised only on SMP builds. test_monitor_alias_down_before_send_same_batch() -> P = spawn_opt(fun() -> receive quit -> ok end end, []), - %% Monitor P before spawning the relay, so P holds the owner's monitor ahead of the relay's and - %% posts the owner's 'DOWN' first when it exits. + %% Monitor P before the relay does, so the owner's 'DOWN' is posted before the relay's. Mon = erlang:monitor(process, P, [{alias, demonitor}]), Relay = spawn_opt( fun() -> @@ -577,17 +552,14 @@ test_monitor_alias_down_before_send_same_batch() -> [] ), P ! quit, - %% The huge bound absorbs valgrind's unfair thread scheduling, which can starve the relay - %% while this process spins (it must busy-wait: receiving would drain its mailbox). + %% The huge spin bound absorbs valgrind's unfair scheduling, which can starve the relay + %% while this process spins. It must busy-wait: receiving would drain its mailbox. ok = wait_registered(down_batch_relay, 50000000), {'DOWN', Mon, process, P, normal} = recv_one(), ok = assert_no_message(), Relay ! release, ok. -%% A reference from another node cannot be an alias of this process: unalias/1 returns false -%% (it does not raise), and a send to it is silently dropped, exactly like a send to a plain -%% local reference that never was an alias. (Verified against OTP 29.) test_unalias_and_send_non_local_refs() -> %% NEWER_REFERENCE_EXT (90): Len:16, Node atom, Creation:32, Len x 4-byte words. ExtRef = binary_to_term( @@ -606,11 +578,10 @@ test_unalias_and_send_non_local_refs() -> end, ok. -%% The io protocol echoes ReplyAs back verbatim: an alias passed as ReplyAs must come back as -%% the very same reference, not as a short reference rebuilt from its ticks, which would not -%% match it. (Verified against OTP 29.) +%% An alias passed as ReplyAs must come back verbatim. A short reference rebuilt from its +%% ticks would not match it, so the receive below would not fire. test_io_request_alias_reply() -> - %% On the BEAM the group leader is a full io server; on AtomVM the test process has no + %% On the BEAM the group leader is a full io server. On AtomVM the test process has no %% group leader, so talk to the console port driver directly. IoServer = case erlang:system_info(machine) of @@ -627,8 +598,6 @@ test_io_request_alias_reply() -> true = erlang:unalias(Alias), ok. -%% An alias is an ordinary reference for term handling: usable as a map and ets key, -%% compared by value and distinct from any plain reference. test_alias_as_key() -> Alias = erlang:alias(), Plain = make_ref(), @@ -646,14 +615,13 @@ test_alias_as_key() -> true = erlang:unalias(Alias), ok. -%% demonitor(Mon, [flush]) on an {alias, demonitor} monitor: the alias died with the monitor -%% at 'DOWN' delivery, and flush removes the already-delivered 'DOWN' from the queue. test_monitor_alias_demonitor_flush() -> P = spawn_opt(fun() -> receive quit -> ok end end, []), Mon = erlang:monitor(process, P, [{alias, demonitor}]), Fence = monitor(process, P), P ! quit, - %% Selective receive: Mon's 'DOWN' was enqueued first (installation order) and stays queued. + %% Mon's 'DOWN' was enqueued first (installation order); this selective receive leaves it + %% queued for the flush below to remove. ok = receive {'DOWN', Fence, process, P, normal} -> ok @@ -669,18 +637,15 @@ test_monitor_alias_demonitor_flush() -> Echo ! quit, ok. -%% Duplicate {alias, _} options: the last one wins, like OTP 29 (probed in both orders). +%% With duplicate {alias, _} options the last one wins, like OTP 29. test_monitor_alias_duplicate_option() -> P = spawn_opt(fun echo_loop/0, []), Mon = erlang:monitor(process, P, [{alias, demonitor}, {alias, explicit_unalias}]), true = demonitor(Mon), - %% explicit_unalias won: the alias survives the demonitor. do_test_alias(P, Mon), P ! quit, ok. -%% monitor(process, NameOfSelf, [{alias, _}]) resolves the name first and installs nothing, -%% exactly like monitoring self() directly. (Verified against OTP 29.) test_monitor_alias_registered_self_installs_nothing() -> true = register(alias_self_name, self()), Mon = erlang:monitor(process, alias_self_name, [{alias, explicit_unalias}]), @@ -691,8 +656,6 @@ test_monitor_alias_registered_self_installs_nothing() -> true = unregister(alias_self_name), ok. -%% alias/1: alias([]) and alias([explicit_unalias]) behave like alias/0; bad options and a -%% non-list argument raise badarg. (Verified against OTP 29.) test_alias_1() -> A1 = alias([]), A1 ! x1, @@ -716,8 +679,8 @@ test_alias_1() -> end, ok. -%% alias([reply]): the alias is deactivated when the first message sent via it is delivered, -%% so a second message in the same batch is dropped and later sends are dropped too, like OTP. +%% A reply alias is deactivated when its first message is delivered, so a second message in +%% the same batch is dropped too, not just delayed. test_alias_reply_mode() -> A = alias([reply]), A ! m1, @@ -731,11 +694,9 @@ test_alias_reply_mode() -> false = unalias(A), ok. -%% A send to the alias of a process that no longer exists is dropped and still returns the -%% message, like a send to any non-alias reference. AtomVM assigns process ids monotonically, -%% so the dead owner's id is not reused, and the globally-unique ref ticks would make the -%% stale alias unmatchable even by a process reusing the id: the churn probes pin that no -%% message surfaces anywhere and nothing crashes. +%% AtomVM assigns process ids monotonically and ref ticks are globally unique, so a dead +%% owner's stale alias stays unmatchable even by a later process. The churn loop spawns such +%% processes to confirm a stale-alias send never surfaces anywhere. test_alias_send_after_owner_died() -> Parent = self(), {P, Fence} = spawn_opt(fun() -> Parent ! {alias, erlang:alias()} end, [monitor]), @@ -756,8 +717,8 @@ churn_and_send_stale(A, N) -> {'DOWN', Mon, process, Q, normal} = recv_one(), churn_and_send_stale(A, N - 1). -%% Reports any message other than the expected quit: a stale-alias message must never surface -%% here as a plain message (its signal is dropped against this process's empty alias list). +%% A stale-alias signal is dropped against this process's empty alias list, so it must never +%% surface here as a plain message. Any non-quit message is reported as a misdelivery. stale_alias_probe(Parent) -> receive quit -> @@ -767,13 +728,10 @@ stale_alias_probe(Parent) -> stale_alias_probe(Parent) end. -%% Several senders hammer one alias while the owner unaliases mid-stream. All synchronization -%% is per-sender send order and explicit acks. No sleeps and no timing windows, so the test -%% cannot flake on slow or loaded hosts. Phase 1 pins exact delivery while the alias is -%% active: a sender's alias messages all precede its sent_all fence in the owner's queue, so -%% once the last fence is consumed every alias message has been counted. Phase 2 races -%% unalias/1 against the senders and pins what survives the race: no crash, a bounded -%% delivery count, and an observably dead alias afterwards. +%% Several senders hammer one alias while the owner unaliases mid-stream. Synchronization is +%% per-sender send order plus explicit acks, with no sleeps or timing windows, so the test +%% cannot flake on slow hosts. A sender's alias messages all precede its sent_all fence in the +%% owner's queue, so once the last phase-1 fence is consumed Count1 has counted them all. test_alias_multi_sender_unalias() -> NSenders = 4, NMsgs = 25, @@ -831,8 +789,7 @@ drain_alias_msgs(FencesLeft, FenceMsg, Count) -> Other -> {unexpected, Other} end. -%% Duplicate alias/1 options: the last one wins, like OTP 29 (probed in both orders) and like -%% the monitor/3 duplicate {alias, _} options. +%% With duplicate alias/1 options the last one wins, like OTP 29. Both orders are checked. test_alias_duplicate_options() -> A1 = alias([explicit_unalias, reply]), A1 ! r1, @@ -850,7 +807,6 @@ test_alias_duplicate_options() -> true = unalias(A2), ok. -%% unalias/1 raises badarg on a non-reference argument, like OTP. test_unalias_non_reference_badarg() -> ok = try unalias(42) of @@ -860,12 +816,10 @@ test_unalias_non_reference_badarg() -> end, ok. -%% The owner pid word of a wire-format alias (len-3 NEWER_REFERENCE_EXT) is untrusted input: -%% decoding must reject pid 0 (INVALID_PROCESS_ID, the short-ref sentinel) and pids above the -%% 28-bit maximum. Not a BEAM divergence any real term can show: term_to_binary can never -%% produce these bytes, and the BEAM treats reference words as opaque payload while AtomVM -%% gives the third word pid semantics, so only AtomVM has anything to validate. On the BEAM -%% the patched binaries decode as plain references, which the test asserts there. +%% The owner pid word of a wire-format alias is untrusted input: decoding must reject pid 0 +%% (the short-ref sentinel) and pids above the 28-bit maximum. The BEAM treats reference words +%% as opaque payload, so it decodes the patched binaries as plain references instead of +%% rejecting them. Only AtomVM gives the third word pid semantics, so the test forks on machine. test_binary_to_term_invalid_process_ref() -> A = erlang:alias(), B = term_to_binary(A), @@ -926,9 +880,8 @@ recv_one() -> after 5000 -> timeout end. -%% Assert that no message arrives. Only call this when the would-be message is already -%% settled (delivered or dropped), behind a fence reply or after a same-process send, -%% so the short window is not a race. +%% Only call this once the would-be message is already settled, behind a fence reply or after +%% a same-process send, so the short timeout window is not a race. assert_no_message() -> receive Msg -> {unexpected_message, Msg} diff --git a/tests/libs/estdlib/test_net_kernel.erl b/tests/libs/estdlib/test_net_kernel.erl index 560d73bc8b..209ac58f28 100644 --- a/tests/libs/estdlib/test_net_kernel.erl +++ b/tests/libs/estdlib/test_net_kernel.erl @@ -499,34 +499,23 @@ test_link_local_unlink_local(Platform) -> ok = stop_apply_loop(BeamMainPid, Pid, MonitorRef), ok. -%% A send to a remote alias emits DOP_ALIAS_SEND, which this node delivers to the -%% local alias. Only the inbound direction is exercised: sending to a remote alias -%% from AtomVM is not supported (the message is silently dropped). test_alias_send_from_beam(Platform) -> {BeamMainPid, Pid, MonitorRef} = start_apply_loop(Platform), Alias = alias(), - %% The payload carries the alias itself, so the receive below also checks the - %% reference round-trips unchanged through the distribution encoding. {via_alias, Alias} = call_apply_loop( BeamMainPid, {self(), apply, erlang, send, [Alias, {via_alias, Alias}]} ), - %% The alias send was emitted before the call_apply_loop reply on the same - %% connection, so it is already in our queue (skipped by the selective receive). ok = receive {via_alias, Alias} -> ok after 30000 -> alias_message_timeout end, true = unalias(Alias), - %% Inactive alias: the send must be silently dropped by this node, without - %% taking the connection down. The call_apply_loop round-trip is the fence: - %% the dropped message preceded the reply, so it can no longer show up. should_be_dropped = call_apply_loop( BeamMainPid, {self(), apply, erlang, send, [Alias, should_be_dropped]} ), - %% Match alias messages only: on BEAM the suite may run in a process that traps - %% exits, so unrelated messages (e.g. stale 'EXIT' from a previous test's linked - %% helper) can sit in the queue. + %% The dropped send precedes this reply on the same connection, so `after 0` is + %% race-free. Match only alias messages: a stale 'EXIT' may be queued on BEAM. ok = receive should_be_dropped -> unexpected_alias_message; From f7eccdcc003bba4cf1e3362ba9ba121a78d16ee3 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 17 Jun 2026 10:59:47 +0000 Subject: [PATCH 83/87] Trim the select-handle alias-rejection comments Both select validation sites explained at length why a process alias is rejected as a select handle. That the alias is rejected, and that the select machinery only carries ref ticks, is clear from the surrounding code. Keep only the part a reader cannot infer here: that the BEAM accepts an alias where AtomVM does not. Comment only; no behavior change. Signed-off-by: Davide Bettio --- src/libAtomVM/otp_socket.c | 3 +-- src/libAtomVM/resources.c | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/libAtomVM/otp_socket.c b/src/libAtomVM/otp_socket.c index 0de8a61237..a561e0392d 100644 --- a/src/libAtomVM/otp_socket.c +++ b/src/libAtomVM/otp_socket.c @@ -1032,8 +1032,7 @@ static term nif_socket_select_read(Context *ctx, int argc, term argv[]) term select_ref_term = argv[1]; if (select_ref_term != UNDEFINED_ATOM) { VALIDATE_VALUE(select_ref_term, term_is_local_reference); - // A process alias is a valid select handle on the BEAM (echoed back verbatim), but AtomVM - // stores only ref ticks and would echo a plain reference the alias would not match. Reject it. + // Unlike the BEAM, AtomVM does not support a process alias as a select handle. if (UNLIKELY(term_is_process_reference(select_ref_term))) { RAISE_ERROR(BADARG_ATOM); } diff --git a/src/libAtomVM/resources.c b/src/libAtomVM/resources.c index 47086d2322..87ca0b5eaa 100644 --- a/src/libAtomVM/resources.c +++ b/src/libAtomVM/resources.c @@ -238,8 +238,7 @@ int enif_select(ErlNifEnv *env, ErlNifEvent event, enum ErlNifSelectFlags mode, if (UNLIKELY(mode & (ERL_NIF_SELECT_READ | ERL_NIF_SELECT_WRITE) && !term_is_local_reference(ref) && ref != UNDEFINED_ATOM)) { return ERL_NIF_SELECT_BADARG; } - // A process alias is a valid enif_select ref on the BEAM (echoed back verbatim), but AtomVM - // stores only ref ticks and would echo a plain reference the alias would not match. Reject it. + // Unlike the BEAM, AtomVM does not support a process alias as an enif_select ref. if (UNLIKELY(term_is_process_reference(ref))) { return ERL_NIF_SELECT_BADARG; } From 8362d046f81d0eef6c804c49516cc8051b37219f Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 17 Jun 2026 11:04:07 +0000 Subject: [PATCH 84/87] Split a semicolon-joined comment in the alias monitor test A leftover comment joined two clauses with a semicolon. The comment style asks for plain separate sentences instead. Comment only; no behavior change. Signed-off-by: Davide Bettio --- tests/erlang_tests/test_monitor.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 686e4b8463..ab33c62619 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -620,7 +620,7 @@ test_monitor_alias_demonitor_flush() -> Mon = erlang:monitor(process, P, [{alias, demonitor}]), Fence = monitor(process, P), P ! quit, - %% Mon's 'DOWN' was enqueued first (installation order); this selective receive leaves it + %% Mon's 'DOWN' was enqueued first (installation order). This selective receive leaves it %% queued for the flush below to remove. ok = receive From 25365fe946d4e30d859a3c32c9335f00534be266 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 17 Jun 2026 12:24:39 +0000 Subject: [PATCH 85/87] Trim the console reply comments to their non-obvious facts Both console gen_call comments narrated what the code already shows. The reply reservation comment re-derived the 2 * TUPLE_SIZE(2) size that the very next line reserves and compared it to the other reply shapes; keep only the reason the reservation exists at all (the reply builders below do not ensure_free). The io_reply comment led with "echo ReplyAs verbatim", which the term_put_tuple_element line shows; keep only the reason not to rebuild it from ref ticks (an alias would become a plain reference the requester would not match). Comment only; no behavior change. Signed-off-by: Davide Bettio --- src/libAtomVM/nifs.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 1f6e57778b..cae1b6f508 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1335,9 +1335,8 @@ static NativeHandlerResult process_console_message(Context *ctx, term msg) { // msg is not in the port's heap NativeHandlerResult result = NativeContinue; - // Reserve the worst-case reply on ctx's heap: the gen_call error path nests an error 2-tuple in - // the reply 2-tuple (2 * TUPLE_SIZE(2)), larger than the io_reply or close reply. The port reply - // helpers don't ensure_free, so this one budget must cover every path. + // The reply builders below don't ensure_free, so this one reservation must cover their largest + // reply, the gen_call error path. if (UNLIKELY(memory_ensure_free_opt(ctx, 2 * TUPLE_SIZE(2), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { fprintf(stderr, "Unable to allocate sufficient memory for console driver.\n"); AVM_ABORT(); @@ -1367,9 +1366,8 @@ static NativeHandlerResult process_console_message(Context *ctx, term msg) term reply = term_alloc_tuple(3, &ctx->heap); term_put_tuple_element(reply, 0, IO_REPLY_ATOM); - // Echo ReplyAs (ref) back verbatim: rebuilding it from ref ticks would turn an - // alias into a plain reference the requester's receive would not match, and ReplyAs - // need not be a reference at all. + // Don't rebuild ReplyAs from ref ticks: that turns an alias into a plain reference + // the requester's receive would not match, and ReplyAs need not be a reference. term_put_tuple_element(reply, 1, ref); term_put_tuple_element(reply, 2, OK_ATOM); From 0a949b6499f5584848e196e15fa6528994d1112b Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 17 Jun 2026 12:38:57 +0000 Subject: [PATCH 86/87] Trim over-narrating comments across the alias C code The spawn, send and DOWN paths carried comments that narrated the adjacent code: re-listing the steps a block is about to run, restating the size the next line reserves, or re-describing a branch condition. Keep only what a reader cannot get from the code. - do_spawn: drop the step enumeration and the result-size restatement; keep the spurious-EXIT atomicity reason, the publish ordering, and the GC-safety note. - The three send paths (nif_erlang_send_2, jit_send, OP_SEND): drop the "not a local process reference" lead, keep the OTP silent-drop divergence and the outbound-unsupported note. - context.c: drop the "remove from the scheduler queue" narration; delete the alias-deactivation comment duplicated within the same function (it is stated at the first site); keep the capture-pid-before-demonitor ordering note. - context.h: trim the active_alias_count field comment to its two real facts. - dist_nifs.c: keep why a stale inbound ref is dropped rather than crashing the distribution connection. Comment only; no behavior change. Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 12 ++++-------- src/libAtomVM/context.h | 4 ++-- src/libAtomVM/dist_nifs.c | 4 ++-- src/libAtomVM/jit.c | 4 ++-- src/libAtomVM/nifs.c | 28 ++++++++++++---------------- src/libAtomVM/opcodesswitch.h | 4 ++-- 6 files changed, 24 insertions(+), 32 deletions(-) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index ce5f3377be..3c70f91f07 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -145,10 +145,9 @@ void context_destroy(Context *ctx) // Hold and release the spin lock for timers and cancel any timer scheduler_cancel_timeout(ctx); - // Remove the process from the scheduler queue. A process terminated by the scheduler was - // already dequeued and its queue item reset, so this is a no-op. A process destroyed before - // it was ever scheduled (e.g. a spawn_opt option error) is still on the waiting queue. - // Dequeuing it here avoids leaving a dangling entry. + // A process terminated by the scheduler was already dequeued and its queue item reset, so this + // is a no-op. A process destroyed before it was ever scheduled (e.g. a spawn_opt option error) + // is still on the waiting queue, and dequeuing it here avoids leaving a dangling entry. SMP_SPINLOCK_LOCK(&ctx->global->processes_spinlock); list_remove(&ctx->processes_list_head); SMP_SPINLOCK_UNLOCK(&ctx->global->processes_spinlock); @@ -479,8 +478,6 @@ void context_process_monitor_down_signal(Context *ctx, struct TermSignal *signal mailbox_send(ctx, signal->signal_term); END_WITH_STACK_HEAP(temp_heap, ctx->global); - // {alias, demonitor} / {alias, reply_demonitor}: deactivate the alias on the - // monitor's automatic removal at 'DOWN' delivery. struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); if (alias != NULL && alias->alias_type != ContextMonitorAliasExplicitUnalias) { context_unalias(ctx, alias); @@ -508,8 +505,7 @@ term context_process_alias_message_signal(Context *ctx, struct TermSignal *signa } if (alias->alias_type == ContextMonitorAliasReplyDemonitor) { - // Deactivate the alias and remove the monitor, like erlang:demonitor/1. Capture the - // monitored pid first, before context_demonitor removes the local monitoring entry. + // Capture the monitored pid before context_demonitor removes the local monitoring entry. bool is_monitoring = false; term monitor_pid = context_get_monitor_pid(ctx, ref_ticks, &is_monitoring); context_demonitor(ctx, ref_ticks); diff --git a/src/libAtomVM/context.h b/src/libAtomVM/context.h index 65dac010f0..e7630a95a3 100644 --- a/src/libAtomVM/context.h +++ b/src/libAtomVM/context.h @@ -138,8 +138,8 @@ struct Context unsigned int leader : 1; unsigned int has_min_heap_size : 1; unsigned int has_max_heap_size : 1; - // Optimization: count of active aliases, so alias lookups can short-circuit for the common - // alias-free process. Saturates at a sticky sentinel (ACTIVE_ALIAS_COUNT_SATURATED, context.c). + // Count of active aliases, so alias lookups short-circuit for the common alias-free process. + // Saturates at ACTIVE_ALIAS_COUNT_SATURATED (context.c). unsigned int active_alias_count : 8; bool trap_exit : 1; diff --git a/src/libAtomVM/dist_nifs.c b/src/libAtomVM/dist_nifs.c index b5dc23d227..1b6d93f24a 100644 --- a/src/libAtomVM/dist_nifs.c +++ b/src/libAtomVM/dist_nifs.c @@ -579,8 +579,8 @@ static term nif_erlang_dist_ctrl_put_data(Context *ctx, int argc, term argv[]) int32_t target_process_id = term_process_ref_to_process_id(target); globalcontext_send_message_to_alias(ctx->global, target_process_id, target, payload); } - // else: not a local process reference (e.g. an alias minted by a previous incarnation - // of this node), so not an active alias. Drop the message. + // A ref minted by a previous incarnation of this node is not an active alias here. + // Drop the message instead of crashing the dist connection with badarg. break; } case OPERATION_SPAWN_REQUEST: { diff --git a/src/libAtomVM/jit.c b/src/libAtomVM/jit.c index a6afc988be..e705e5e12f 100644 --- a/src/libAtomVM/jit.c +++ b/src/libAtomVM/jit.c @@ -862,8 +862,8 @@ static bool jit_send(Context *ctx, JITState *jit_state) globalcontext_send_message_to_alias(ctx->global, process_id, recipient_term, ctx->x[1]); ctx->x[0] = ctx->x[1]; } else { - // Not a local process reference: drop the message but still return it in x0, as OTP drops - // a send to a non-active-alias reference. Outbound distributed aliases are unsupported. + // Drop the send but still return the message in x0, as OTP does for a non-active-alias + // reference. Outbound distributed aliases are unsupported. ctx->x[0] = ctx->x[1]; } diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index cae1b6f508..c9081e481a 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1547,9 +1547,8 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free context_monitor_alias_type_t alias_type; term new_pid = term_from_local_process_id(new_ctx->process_id); - // Run every fallible step (option parsing, allocations, result reservation) before publishing - // any side effect, so a late failure leaves no half-installed link or monitor. Otherwise, - // destroying the never-published new_ctx would send the caller a spurious {'EXIT', Pid, normal}. + // Do every fallible step before publishing any side effect: destroying a never-published + // new_ctx would send the caller a spurious {'EXIT', Pid, normal} for a spawn that raised. struct Monitor *new_link = NULL; struct Monitor *self_link = NULL; struct Monitor *alias_monitor = NULL; @@ -1613,8 +1612,7 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - // Reserve the result space before publishing the monitors (see above). GC here is safe: - // new_pid and ref_data are immediates. + // Reserve before publishing (see above). GC here is safe: new_pid and ref_data are immediates. int res_size = TERM_BOXED_REFERENCE_PROCESS_SIZE + TUPLE_SIZE(2); if (UNLIKELY(memory_ensure_free_opt(ctx, res_size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { monitor_destroy(new_link); @@ -1627,17 +1625,15 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free } } else if (UNLIKELY(!term_is_invalid_term(monitor_term))) { // {monitor, BadTerm} where BadTerm is neither a list nor 'true': raise badarg like OTP - // instead of spawning an unmonitored process. Nothing is published yet. monitor_destroy(NULL) - // is a no-op. + // instead of spawning an unmonitored process. monitor_destroy(new_link); monitor_destroy(self_link); context_destroy(new_ctx); RAISE_ERROR(BADARG_ATOM); } - // Nothing can fail from here on: publish the link and the monitors, in this order so the - // entries keep their relative position in the monitor lists. - // We can call context_add_monitor directly on new process because it's not started yet + // Nothing can fail from here on. Publish in order so the entries keep their relative position + // in the monitor lists. context_add_monitor on new_ctx is safe because it is not started yet. if (new_link != NULL) { context_add_monitor(new_ctx, new_link); context_add_monitor(ctx, self_link); @@ -1882,10 +1878,10 @@ static term nif_erlang_send_2(Context *ctx, int argc, term argv[]) int32_t process_id = term_process_ref_to_process_id(target); globalcontext_send_message_to_alias(glb, process_id, target, argv[1]); } - // else: a reference that is not a local process reference is silently dropped, as OTP drops a - // send to a non-active-alias reference. - // TODO: route sends to external references over distribution (outbound distributed aliases are - // unsupported; the message is currently lost). + // else: a non-local-process reference is silently dropped, as OTP drops a send to a + // non-active-alias reference. + // TODO: route sends to external references over distribution. Outbound distributed aliases are + // unsupported, so the message is currently lost. return argv[1]; } @@ -7615,8 +7611,8 @@ static term nif_erlang_crc32_combine_3(Context *ctx, int argc, term argv[]) static term nif_erlang_alias(Context *ctx, int argc, term argv[]) { - // Default mode is explicit_unalias. The reply option reuses the reply_demonitor machinery: - // with no monitor to remove, the alias is deactivated when the first message via it is delivered. + // The reply option reuses the reply_demonitor machinery: with no monitor to remove, the alias + // is deactivated when the first message via it is delivered. context_monitor_alias_type_t alias_type = ContextMonitorAliasExplicitUnalias; if (argc == 1) { term opts = argv[0]; diff --git a/src/libAtomVM/opcodesswitch.h b/src/libAtomVM/opcodesswitch.h index fe2d5b5473..2e867a17b6 100644 --- a/src/libAtomVM/opcodesswitch.h +++ b/src/libAtomVM/opcodesswitch.h @@ -2386,8 +2386,8 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) TRACE_SEND(ctx, x_regs[0], x_regs[1]); globalcontext_send_message_to_alias(ctx->global, target_process_id, recipient_term, x_regs[1]); } - // else: not a local process reference. Silently dropped, as OTP drops a send - // to a non-active-alias reference. Outbound distributed aliases are unsupported. + // Silently dropped, as OTP does for a send to a non-active-alias reference. + // Outbound distributed aliases are unsupported. x_regs[0] = x_regs[1]; } break; From 6990a17ed0d839b92e06e81d57bdc54f24d19487 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 17 Jun 2026 12:41:15 +0000 Subject: [PATCH 87/87] reformat tests/erlang_tests/test_monitor.erl Signed-off-by: Davide Bettio --- tests/erlang_tests/test_monitor.erl | 41 +++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index ab33c62619..7231f7a90e 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -93,9 +93,14 @@ test_alias_ref_ordering() -> true = Ea < Eb, true = Eb =:= binary_to_term(term_to_binary(Eb)), Parent = self(), - Child = spawn_opt(fun() -> - receive {get, P} -> P ! {child_alias, erlang:alias()} end - end, []), + Child = spawn_opt( + fun() -> + receive + {get, P} -> P ! {child_alias, erlang:alias()} + end + end, + [] + ), Child ! {get, Parent}, ChildAlias = receive @@ -425,8 +430,12 @@ test_alias_pid_send_order() -> fun() -> Alias = erlang:alias(), Parent ! {ready, self(), Alias}, - receive A -> Parent ! {got, A} end, - receive B -> Parent ! {got, B} end + receive + A -> Parent ! {got, A} + end, + receive + B -> Parent ! {got, B} + end end, [] ), @@ -536,7 +545,14 @@ test_spawn_opt_monitor_non_list_badarg() -> %% through the cross-batch deactivation path instead. The same-batch path is reliably %% exercised only on SMP builds. test_monitor_alias_down_before_send_same_batch() -> - P = spawn_opt(fun() -> receive quit -> ok end end, []), + P = spawn_opt( + fun() -> + receive + quit -> ok + end + end, + [] + ), %% Monitor P before the relay does, so the owner's 'DOWN' is posted before the relay's. Mon = erlang:monitor(process, P, [{alias, demonitor}]), Relay = spawn_opt( @@ -546,7 +562,9 @@ test_monitor_alias_down_before_send_same_batch() -> {'DOWN', _, process, P, _} -> Mon ! should_drop, register(down_batch_relay, self()), - receive release -> ok end + receive + release -> ok + end end end, [] @@ -616,7 +634,14 @@ test_alias_as_key() -> ok. test_monitor_alias_demonitor_flush() -> - P = spawn_opt(fun() -> receive quit -> ok end end, []), + P = spawn_opt( + fun() -> + receive + quit -> ok + end + end, + [] + ), Mon = erlang:monitor(process, P, [{alias, demonitor}]), Fence = monitor(process, P), P ! quit,