Skip to content

[Updated] Add support for process aliases#2326

Open
bettio wants to merge 6 commits into
atomvm:release-0.7from
bettio:updated-aliases
Open

[Updated] Add support for process aliases#2326
bettio wants to merge 6 commits into
atomvm:release-0.7from
bettio:updated-aliases

Conversation

@bettio

@bettio bettio commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Implement process aliases, so a process can hand out a reference that
works as a send target and deactivate it again once it is done with
it. This is the OTP mechanism behind request/reply patterns that must
not accept a late reply, and AtomVM had no equivalent. The series
rebases the original implementation on release-0.7 and lands the
fixes found while reviewing it.

Highlights:

  • erlang:alias/0,1, erlang:unalias/1, erlang:monitor/3 with the
    {alias, Mode} option, and spawn_opt {monitor, MonitorOpts}.
  • Sending to an alias from erlang:send/2, the send opcode and the
    JIT send path, and from a remote node over distribution.
  • A new process reference term that carries the owner process id next
    to the ref ticks, with BEAM compliant ordering and
    binary_to_term/term_to_binary round tripping.
  • Alias sends are posted as a mailbox signal, so a sender never walks
    the owner's monitor list and send order is preserved.
  • Three pre-existing defects surfaced along the way are fixed in their
    own commits: a leaked process lock in erlang:monitor/2,3, a switch
    fall through in context_demonitor, and a dangling scheduler queue
    entry left behind by context_destroy.

A send addressed to a remote alias is dropped instead of being routed
over distribution, and the OTP {tag, Term} monitor option and the
alias/1 priority option raise unsupported. All three are
documented.

Supersedes #2027
Closes #2027

These changes are made under both the "Apache 2.0" and the "GNU Lesser General
Public License 2.1 or later" license terms (dual license).

SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later

@bettio
bettio changed the base branch from main to release-0.7 June 2, 2026 12:10
@petermm

This comment was marked as outdated.

@petermm

This comment was marked as outdated.

Comment thread src/libAtomVM/nifs.c Fixed
Comment thread tests/test-enif.c Fixed
@petermm

This comment was marked as outdated.

@bettio
bettio force-pushed the updated-aliases branch from ce79b10 to f6af023 Compare June 7, 2026 19:53
@petermm

This comment was marked as outdated.

@bettio
bettio force-pushed the updated-aliases branch from 64e7fb6 to 73bb408 Compare June 11, 2026 13:43
Comment thread src/libAtomVM/context.c Outdated

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want comments that verbose? The drawback is that eventually they end up being unsync with code, and LLM or humans tend to read comments instead of code.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not at all, I'm leaving them for helping with review activity, but I will cleanup code from all that narration.

Comment thread src/libAtomVM/context.c Outdated
Comment thread src/libAtomVM/context.h Outdated
Comment thread src/libAtomVM/globalcontext.c Outdated
{
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this comment adds any value

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these comments/narration will be removed while squashing, after review.

*
* @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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last sentence sounds obvious here.

Comment thread src/libAtomVM/jit.c Outdated
Comment thread src/libAtomVM/jit.c Outdated
@petermm

petermm commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

AMP:

PR Review — Process aliases for AtomVM

Range reviewed: f90e0ee9a..HEAD (62 commits), branched from
9402220d7 (Merge pull request #2331 … valgrind-spinlock-sched).

Diffstat: ~40 files, +1975 / −173. The series adds OTP process aliases:
erlang:alias/0,1, erlang:unalias/1, erlang:monitor/3 with the
{alias, Mode} option, spawn_opt {monitor, MonitorOpts}, sending to an
alias reference (local + inbound distribution), a new boxed process
reference
term, and per-process saturating alias counters.

Verdict

Strong, ship-quality work. The design is sound: aliases are validated in
the owner's own context against the owner's own monitor list (the same
single-owner discipline as alias/0/unalias/1/demonitor), so there is no
cross-process race on alias state. Error paths in do_spawn/monitor are
carefully "allocate-everything-then-publish", the 32-bit reference padding and
size-distinctness are guarded by _Static_assert, and the commit history shows
the tricky cases (same-batch ordering, saturation, wire ordering in
term_compare, REF_SIZE deprecation) were each addressed deliberately. Test
coverage is substantial (tests/erlang_tests/test_monitor.erl +672).

The findings below are mostly pre-existing OOM-robustness gaps that this
feature widens slightly, plus one portability note. None block merge; I'd fix
#1 in this series since the alias code adds to the half-published surface.


Findings

1. (Medium) OOM in mailbox_send_monitor_signal leaves a half-published monitor/alias

nifs.c:5187,
helper at mailbox.c:338.

mailbox_send_monitor_signal() returns void and silently drops the signal on
malloc failure (the FIXME at mailbox.c:340 acknowledges this). In
nif_erlang_monitor the call is the publish step for the target's half of the
monitor. If it fails:

  • other_monitor is leaked,
  • self_monitor and (new in this PR) alias_monitor are still added right
    after at nifs.c:5190-5192,
  • the caller still receives a reference,
  • the target never gets the monitor.

So the monitoring process believes it monitors / has an alias to a target that
knows nothing about it. The alias addition makes the inconsistency worse (a
live alias with no monitored half).

Suggested fix — make the helper report failure and clean up at the call site:

--- a/src/libAtomVM/mailbox.h
+++ b/src/libAtomVM/mailbox.h
-void mailbox_send_monitor_signal(Context *c, enum MessageType type, struct Monitor *monitor);
+bool mailbox_send_monitor_signal(Context *c, enum MessageType type, struct Monitor *monitor);
--- a/src/libAtomVM/mailbox.c
+++ b/src/libAtomVM/mailbox.c
-void mailbox_send_monitor_signal(Context *c, enum MessageType type, struct Monitor *monitor)
+bool mailbox_send_monitor_signal(Context *c, enum MessageType type, struct Monitor *monitor)
 {
     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;
+        return false;
     }
     ...
     mailbox_post_message(c, &monitor_signal->base);
+    return true;
 }
--- a/src/libAtomVM/nifs.c
+++ b/src/libAtomVM/nifs.c
-    mailbox_send_monitor_signal(target, MonitorSignal, other_monitor);
-    globalcontext_get_process_unlock(ctx->global, target);
+    if (UNLIKELY(!mailbox_send_monitor_signal(target, MonitorSignal, 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);
+    }
+    globalcontext_get_process_unlock(ctx->global, target);

The other callers (nifs.c:5275 link, dist_nifs.c, resources.c) then need
to either check the return or explicitly ignore it with cleanup. If a full
audit is out of scope for this PR, at minimum guard the new alias-bearing path
above so the alias is not left dangling.

2. (Low) Same OOM class on mailbox_send / mailbox_send_ref_signal

  • noproc monitor branch: nifs.c:5133 installs the
    explicit_unalias alias before mailbox_send(ctx, down_message_tuple); if
    that send OOMs the DOWN is lost but the alias remains (tolerable, since an
    explicit-unalias alias lives until unalias/1).
  • reply_demonitor: context.c:527
    mailbox_send_ref_signal(target, DemonitorSignal, …) can drop the remote
    demonitor on OOM, leaving the monitored side installed until target exit.

Same root cause as #1 (void mailbox-send helpers). Worth a tracking note rather
than a blocker.

3. (Low, pre-existing) Registered-name DOWN writes a temp-heap tuple into the retained signal

context.c:470-476:

BEGIN_WITH_STACK_HEAP(TUPLE_SIZE(2), temp_heap)
term name_tuple = term_alloc_tuple(2, &temp_heap);
...
term_put_tuple_element(signal->signal_term, 3, name_tuple);  // temp-heap term into the signal
mailbox_send(ctx, signal->signal_term);
END_WITH_STACK_HEAP(temp_heap, ctx->global);

mailbox_send copies the term out before the temp heap is torn down, so the
delivered message is fine. But signal->signal_term now holds a pointer into
freed stack-heap storage, and the original signal fragment is later appended to
ctx->heap by mailbox_message_dispose, where a future GC could scan the
dangling slot.

This code is unchanged by this PR (it predates f90e0ee9a; the series only
renamed ref_data.ref_ticksref_ticks here), so it is out of scope — noted
for a follow-up. Fix is to build a fresh 5-tuple on the temp heap and send that
instead of mutating signal->signal_term.

4. (Low / portability) In-place re-type of TermSignalMessage

mailbox.c:458-459
(and the CONTAINER_OF(current, Message, base) disposal casts at lines 403,
472) re-type an AliasMessageSignal in place. The _Static_asserts above the
function prove identical layout, so this works on every current toolchain, but
identical layout does not by itself satisfy C effective-type / strict-aliasing
rules. Low risk; the clean long-term fix is to unify Message and TermSignal
into one heap-backed mailbox term with a union { term message; term signal_term; }. Not needed for merge.


Things verified as correct (not bugs)

  • Unlocked active_alias_count read in process_outer_list
    (mailbox.c:388):
    owner-written only, runs in owner context, stable for the batch. Fast path
    (no alias) preserves the original single-pass reversal; alias path reverses
    to received order first. Sound.
  • Same-batch ordering: pre-deactivating an alias on a MonitorDownSignal
    seen earlier in the batch (mailbox.c:483)
    so a later same-batch alias send is dropped; idempotent with the later
    context_process_monitor_down_signal. Matches OTP.
  • Saturating counter (context.h): pins at 0xFFFF and stops decrementing,
    so context_find_alias never wrongly short-circuits — correctness over the
    optimization. Sound.
  • Process-reference sizing (term.h): 64-bit = SHORT+1, 32-bit = SHORT+2
    with the trailing pad word initialized to nil; all reference kinds stay
    size-distinguishable, guarded by _Static_assert. Sound.
  • reply_demonitor lock discipline (context.c): monitor_pid captured as
    an immediate before context_demonitor frees the alias; target taken under
    the process-table lock; context_destroy deliberately uses the alias-blind
    drain to avoid taking that lock while holding the write lock. No UAF/deadlock.
  • Inbound distributed alias send (dist_nifs.c, external_term.c): control
    tuple kept rooted across payload-decode GC; decode rejects process_id == 0
    (the INVALID_PROCESS_ID short-ref sentinel) and > TERM_MAX_LOCAL_PROCESS_ID;
    delivery only for node+creation match.
  • term_compare wire ordering: local process refs laid out as
    [ticks_hi, ticks_lo, process_id] to agree with the serialized form and the
    external-ref path; the mixed local/external len==3 branch never has both
    operands local (guarded by the outer is_external check), so the shared
    local_data scratch buffer is not a hazard.
  • do_spawn: all parsing/allocation/result reservation before any publish,
    monitor_destroy on every error path, monitor_destroy(NULL) no-op. No leak
    or half-published state found.
  • REF_SIZE deprecation: no remaining REF_SIZE uses in the tree, so the
    _Pragma("GCC warning …") will not fire (safe even under -Werror).
    CHANGELOG documents both the feature and the deprecation.

Suggested follow-up tests

  • Same-batch: alias-send-before-DOWN (delivered) vs DOWN-before-alias-send
    (dropped); multiple sends to one reply/reply_demonitor alias in a batch
    (first delivered, rest dropped).
  • OOM fault injection on mailbox_send_monitor_signal at nifs.c:5187 (if a
    malloc hook is available) to lock in the bifs_hash.h not found #1 fix.

Comment thread tests/erlang_tests/test_monitor.erl Outdated
@bettio
bettio force-pushed the updated-aliases branch from 787f573 to 6990a17 Compare June 17, 2026 13:26
@bettio
bettio requested a review from pguyot June 17, 2026 15:26
Comment thread src/libAtomVM/mailbox.c Outdated
// Reverse the LIFO list into received order (oldest first), splitting it into a normal sublist
// 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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok with the renaming, but keeping the old names would reduce the diff.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Comment thread src/libAtomVM/context.c
TERM_DEBUG_ASSERT(ctx->active_alias_count > 0);
if (LIKELY(ctx->active_alias_count != ACTIVE_ALIAS_COUNT_SATURATED)) {
ctx->active_alias_count--;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would put this after the list_remove and if the count is saturated but there is no monitor left (after the remove) I would reset it to 0, so the alias count gets a chance to be reset.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Comment thread src/libAtomVM/context.h Outdated
CONTEXT_MONITOR_LINK_REMOTE,
CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME,
CONTEXT_MONITOR_ALIAS,
// Like CONTEXT_MONITOR_MONITORED_LOCAL, but the 'DOWN' carries a process reference (an alias).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this comment confusing

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Comment thread src/libAtomVM/context.h
Comment thread src/libAtomVM/context.c
Comment thread src/libAtomVM/nifs.c

term refcopy = term_from_ref_ticks(ref_ticks, &ctx->heap);

term reply = term_alloc_tuple(3, &ctx->heap);

@pguyot pguyot Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a tuple 3 but we alloc for two tuple 2 above.

We had 12 and 2 * TUPLE(2) is 6 I believe, and we need at least 7 in this branch, don't we?

I wonder why we copied the ref and no longer need to. With the ref, the branch was 9. Is there another branch that required 12?

EDIT: Using MAX(TUPLE_SIZE(3), 2 * TUPLE_SIZE(2)) above would avoid my confusion.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Comment thread src/libAtomVM/mailbox.c Outdated
mbox->inner_last = last;
}

MailboxMessage *mailbox_process_outer_list(Mailbox *mbox)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not convinced this works.
There are several calls of mailbox_process_outer_list that haven't been replaced with the _with_alias variant, so Alias is considered as a signal and order is lost. For example get_process_info it seems. This would break message ordering semantics, wouldn't it?

I'm not sure we should have the variant in the first place.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems correct but was confusing. Maybe the alias variant should have the default name and the other should be teardown or native as it's only called for ports, crash dump or destroy.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

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 <davide@uninstall.it>
@bettio
bettio force-pushed the updated-aliases branch 2 times, most recently from 0fb9965 to 8ded4a5 Compare July 14, 2026 17:35
@bettio
bettio requested a review from pguyot July 15, 2026 08:55

@pguyot pguyot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three small nits, no need to go through another review round IMHO. It's high time we merge this ;-)

Comment thread src/libAtomVM/nifs.c Outdated
static term nif_erlang_monitor(Context *ctx, int argc, term argv[])
{
UNUSED(argc);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we want to drop this empty line now

Comment thread src/libAtomVM/nifs.c Outdated
if (ref_ticks) {
int res_size = REF_SIZE + TUPLE_SIZE(2);
// 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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use is_alias to avoid the small memory over-reservation here?

Comment thread src/libAtomVM/nifs.c Outdated

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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use is_alias to avoid the small memory over-reservation here?

bettio added 3 commits July 19, 2026 14:38
nif_erlang_monitor locks the target process, then raises badarg when
the object type does not match the target, as in monitor(process,
Port, ...) or the other way around. 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.

Signed-off-by: Davide Bettio <davide@uninstall.it>
The CONTEXT_MONITOR_RESOURCE case fell through to the following
monitor cases when the ref ticks did not match. It is benign today,
since the next cases only break, but the process alias support added
CONTEXT_MONITOR_ALIAS to the same switch, so tighten the fall
through.

Signed-off-by: Davide Bettio <davide@uninstall.it>
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 and port drivers on initialization failures, left a dangling
queue entry pointing into freed memory, corrupting the queue on the
next scheduler operation. The defect predates the process alias
support; it was first exercised by the spawn_opt atomicity test added
later in this series.

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 <davide@uninstall.it>
bettio added a commit to bettio/AtomVM-fork that referenced this pull request Jul 19, 2026
Address the issues found while reviewing the process alias support:
alias message send races and ordering, monitor and alias lifecycle,
out of memory unwinding, reference representation and comparison, and
the delivery of alias messages through the mailbox. Complete the API
with erlang:alias/1 and with incoming distributed alias sends, and add
the missing tests and documentation.

Each issue is described and discussed in the pull requests, so the
individual findings are not repeated here.

See also: GH atomvm#2326, GH atomvm#2027

Signed-off-by: Davide Bettio <davide@uninstall.it>
@bettio
bettio force-pushed the updated-aliases branch from 8ded4a5 to 5a30bdb Compare July 19, 2026 12:43
@bettio bettio changed the title Updated aliases [Updated] Add support for process aliases Jul 19, 2026
@petermm

petermm commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

clean new AMP:

PR review: process alias support

Range reviewed: 2903b59b19fcd639368772c690d7b8aba4a81acd..5a30bdb4ed8c4c58ab29eab5e6a4299c7a009637 (the last six commits)

Recommendation: Request changes.

The alias lifecycle and same-mailbox-batch ordering fixes are generally coherent, and the final monitor-layout change avoids imposing an unnecessary per-monitor memory cost. However, the range still contains one remotely triggerable abort, two broken allocation-failure paths in newly exercised alias/monitor code, and several reference/lifecycle inconsistencies.

Findings

1. High: malformed distributed alias payloads can abort the VM

OPERATION_ALIAS_SEND and OPERATION_ALIAS_SEND_TT deserialize the message payload at src/libAtomVM/dist_nifs.c:575, but do not check the result before passing it to globalcontext_send_message_to_alias at lines 577–580. external_term_from_binary_with_roots returns term_invalid_term() for a missing/truncated payload and for allocation failure (src/libAtomVM/external_term.c:119–148).

For a process-shaped alias, globalcontext_send_message_to_alias embeds that invalid term in a tuple and copies it into a mailbox signal. memory_estimate_usage has no valid primary-tag case for term_invalid_term() and reaches UNREACHABLE() (src/libAtomVM/memory.c:457–568). Therefore, an authenticated distribution peer can send a valid alias-send control tuple followed by no payload (or an invalid payload) and abort AtomVM instead of having the packet rejected.

The new branch should validate deserialization just as it validates the control term:

diff --git a/src/libAtomVM/dist_nifs.c b/src/libAtomVM/dist_nifs.c
--- a/src/libAtomVM/dist_nifs.c
+++ b/src/libAtomVM/dist_nifs.c
@@ -572,6 +572,9 @@ static term nif_erlang_dist_ctrl_put_data(Context *ctx, int argc, term argv[])
             roots[1] = argv[1];
             roots[2] = control;
             term payload = external_term_from_binary_with_roots(ctx, 1, 1 + bytes_read, &bytes_read, 3, roots);
+            if (UNLIKELY(term_is_invalid_term(payload))) {
+                RAISE_ERROR(BADARG_ATOM);
+            }
             control = roots[2];
             term target = term_get_tuple_element(control, 2);
             if (LIKELY(term_is_process_reference(target))) {

Add a distribution regression test with a valid {DOP_ALIAS_SEND, FromPid, Alias} control term and an absent/truncated payload; it should return badarg, not crash.

2. High: monitor creation can return a reference after failing to install the target half

mailbox_send_monitor_signal returns void and explicitly drops the operation when allocation of MonitorPointerSignal fails (src/libAtomVM/mailbox.c:338–350). nif_erlang_monitor nevertheless continues after the call at src/libAtomVM/nifs.c:5175: it installs self_monitor and any alias monitor, then returns a valid reference.

On this failure path:

  • other_monitor leaks;
  • the target never learns it is monitored;
  • the caller receives a monitor reference that will never produce a DOWN when the target exits;
  • an alias may remain active despite the failed monitor installation.

This contradicts the invariant documented immediately above the call: all fallible work must finish before monitor/alias state is published.

The smallest correct fix is contractual rather than a one-line guard: make mailbox_send_monitor_signal return bool, retain ownership of monitor on failure, and have nif_erlang_monitor destroy all three unpublished monitor objects, unlock the target, and raise out_of_memory. The other call sites in nifs.c, dist_nifs.c, and resources.c must also adopt the ownership result. Merely freeing other_monitor inside the mailbox function would avoid the leak but would still return a broken monitor.

This path needs allocator fault injection because ordinary monitor tests cannot exercise it deterministically.

3. High: alias-send allocation failure dereferences NULL

The new alias delivery path calls mailbox_send_term_signal from globalcontext_send_message_to_alias (src/libAtomVM/globalcontext.c:417–428). mailbox_message_create_from_term can return NULL when malloc fails (src/libAtomVM/mailbox.c:239–262), but mailbox_send_term_signal unconditionally posts that pointer (src/libAtomVM/mailbox.c:277–281). Both SMP and non-SMP enqueue paths immediately dereference it.

On memory-constrained embedded targets, sending to a live alias can therefore crash the VM rather than dropping the message. At minimum, make this wrapper match the best-effort behavior of the other signal helpers:

diff --git a/src/libAtomVM/mailbox.c b/src/libAtomVM/mailbox.c
--- a/src/libAtomVM/mailbox.c
+++ b/src/libAtomVM/mailbox.c
@@ -276,7 +276,9 @@ void mailbox_send_term_signal(Context *c, enum MessageType type, term t)
 {
     MailboxMessage *signal = mailbox_message_create_from_term(type, t);
-    mailbox_post_message(c, signal);
+    if (LIKELY(signal != NULL)) {
+        mailbox_post_message(c, signal);
+    }
 }

Returning a success value would be preferable for callers that require transactional behavior, but the guard is the smallest crash fix.

4. Medium: mixed external/process-reference comparison uses an object after its lifetime ends

In src/libAtomVM/term.c:772–848, data or other_data is assigned to a local_data array declared inside one arm of the if (len == ...) chain. The comparison loop runs after that block has ended, so the pointer refers to an object outside its C lifetime. The process-reference work adds this undefined behavior to the new three-word-reference path at lines 793–812 (the older two- and four-word paths have the same latent issue).

The branch is unusual but reachable when an external reference remains live while its node/creation later becomes the local node/creation. Optimizing compilers are then free to miscompile equality or ordering, affecting maps, ETS keys, and sorting.

Give the scratch storage the lifetime of the whole comparison block:

diff --git a/src/libAtomVM/term.c b/src/libAtomVM/term.c
--- a/src/libAtomVM/term.c
+++ b/src/libAtomVM/term.c
@@ -772,8 +772,9 @@ TermCompareResult term_compare(term t, term other, TermCompareOpts opts, GlobalC
                                     if (len == other_len) {
                                         const uint32_t *data;
                                         const uint32_t *other_data;
+                                        uint32_t local_data[4];
+
                                         if (len == 2) {
-                                            uint32_t local_data[2];
                                             if (term_is_external(t)) {
                                                 data = term_get_external_reference_words(t);
                                             } else {
@@ -791,7 +792,6 @@ TermCompareResult term_compare(term t, term other, TermCompareOpts opts, GlobalC
                                                 other_data = local_data;
                                             }
                                         } else if (len == 3) {
-                                            uint32_t local_data[3];
                                             if (term_is_external(t)) {
                                                 data = term_get_external_reference_words(t);
                                             } else {
@@ -812,7 +812,6 @@ TermCompareResult term_compare(term t, term other, TermCompareOpts opts, GlobalC
                                                 other_data = local_data;
                                             }
                                         } else {
                                             // len == 4 (one is a resource)
-                                            uint32_t local_data[4];
                                             if (term_is_external(t)) {
                                                 data = term_get_external_reference_words(t);
                                             } else {

5. Medium: the waiting-queue lifecycle fix leaves early spawn_opt/4 errors leaking contexts

This is an incomplete fix to a pre-existing path rather than a regression introduced by alias support. Commit 63544d886 makes context_destroy safe for never-scheduled Spawning contexts, and later option-validation paths correctly call it. Earlier exits in nif_erlang_spawn_opt, however, still do not:

  • missing module at src/libAtomVM/nifs.c:1772–1775;
  • improper argument list at lines 1777–1781;
  • AVM_NO_EMU JIT load failure at lines 1801–1803.

Each call has already allocated and registered new_ctx and marked it Spawning. Because it is never scheduled, repeated invalid calls retain process-table entries, PIDs, heaps, and waiting-queue nodes indefinitely.

diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c
--- a/src/libAtomVM/nifs.c
+++ b/src/libAtomVM/nifs.c
@@ -1771,12 +1771,14 @@ term nif_erlang_spawn_opt(Context *ctx, int argc, term argv[])
 
     Module *found_module = globalcontext_get_module(ctx->global, term_to_atom_index(module_term));
     if (UNLIKELY(!found_module)) {
+        context_destroy(new_ctx);
         return UNDEFINED_ATOM;
     }
 
     int proper;
     int args_len = term_list_length(argv[2], &proper);
     if (UNLIKELY(!proper)) {
+        context_destroy(new_ctx);
         RAISE_ERROR(BADARG_ATOM);
     }
@@ -1799,6 +1801,7 @@ term nif_erlang_spawn_opt(Context *ctx, int argc, term argv[])
 #else
     if (UNLIKELY(jit_trap_and_load(new_ctx, found_module, label) != TRAP_AND_LOAD_OK)) {
+        context_destroy(new_ctx);
         return UNDEFINED_ATOM;
     }
 #endif

A regression test should compare process_count before and after repeated missing-module and improper-list calls.

6. Medium: monitor/3 on self() loses the alias reference representation

The self-monitor special case at src/libAtomVM/nifs.c:5073–5082 always allocates and returns a short reference, even when {alias, Mode} was requested. All other alias-producing monitor/3 paths return a process reference carrying the owner PID, including the immediate-noproc path.

Not installing an active monitor or alias for self() matches the intended semantics, but changing the returned reference shape does not: term_to_binary/1, ref_to_list/1, and ordering differ depending only on whether the target happened to be the caller. It also conflicts with the PR's own representation rule that alias references carry the owner PID.

Preserve the inactive behavior while constructing the same reference shape as other alias-monitor calls:

diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c
--- a/src/libAtomVM/nifs.c
+++ b/src/libAtomVM/nifs.c
@@ -5061,6 +5061,7 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[])
         target_pid = target_proc;
     }
 
+    int ref_size = is_alias ? TERM_BOXED_REFERENCE_PROCESS_SIZE : TERM_BOXED_REFERENCE_SHORT_SIZE;
     Context *target;
     int32_t local_process_id;
@@ -5074,17 +5075,15 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[])
         // 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)) {
+            if (UNLIKELY(memory_ensure_free_opt(ctx, ref_size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) {
                 RAISE_ERROR(OUT_OF_MEMORY_ATOM);
             }
-            uint64_t ref_ticks = globalcontext_get_ref_ticks(ctx->global);
-            return term_from_ref_ticks(ref_ticks, &ctx->heap);
+            RefData ref_data = { .ref_ticks = globalcontext_get_ref_ticks(ctx->global),
+                .process_id = is_alias ? ctx->process_id : INVALID_PROCESS_ID };
+            return term_from_ref_data(&ref_data, &ctx->heap);
         }
 
         target = globalcontext_get_process_lock(ctx->global, local_process_id);
     }
 
-    int ref_size = is_alias ? TERM_BOXED_REFERENCE_PROCESS_SIZE : TERM_BOXED_REFERENCE_SHORT_SIZE;
-
     if (IS_NULL_PTR(target)) {

Extend test_monitor_alias_self_installs_nothing with a representation-sensitive ETF assertion in addition to the existing delivery/unalias assertions.

Review coverage and verification

Inspected the six commits and the complete HEAD~6..HEAD diff, with particular attention to monitor/alias ownership, local and distributed delivery, mailbox ordering, process teardown, reference representation/comparison, ETF encode/decode, and the added tests. An independent Oracle pass was used to stress-test the findings; each item above was then checked against the current source.

git diff --check HEAD~6..HEAD passes. No runtime test suite was executed: the existing build directory exposes no CTest tests, and the allocation-failure findings require fault injection not present in the normal suite.

No additional high-confidence correctness findings were identified. Outbound distributed alias sends are documented as unsupported and were treated as intentionally out of scope.

bettio added 2 commits July 19, 2026 16:59
Address the issues found while reviewing the process alias support:
alias message send races and ordering, monitor and alias lifecycle,
out of memory unwinding, reference representation and comparison, and
the delivery of alias messages through the mailbox. Complete the API
with erlang:alias/1 and with incoming distributed alias sends, and add
the missing tests and documentation.

Each issue is described and discussed in the pull requests, so the
individual findings are not repeated here.

See also: GH atomvm#2326, GH atomvm#2027

Signed-off-by: Davide Bettio <davide@uninstall.it>
Storing a RefData (ref ticks plus owner pid) in every local monitor
grew each MonitorLocalMonitor, MonitorLocalRegisteredNameMonitor and
MonitorAlias by 8 bytes (uint64_t alignment), paid by every plain
monitor/2, that is by 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, that 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, and process_info(monitored_by) keeps listing alias shaped
monitors.

Signed-off-by: Davide Bettio <davide@uninstall.it>
@bettio
bettio force-pushed the updated-aliases branch from 5a30bdb to e11cc94 Compare July 19, 2026 14:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants