Skip to content

[Updated] Add process_info/1 and process_info/2 with list argument#2374

Open
bettio wants to merge 12 commits into
atomvm:release-0.7from
bettio:updated-nif-erlang-process_info
Open

[Updated] Add process_info/1 and process_info/2 with list argument#2374
bettio wants to merge 12 commits into
atomvm:release-0.7from
bettio:updated-nif-erlang-process_info

Conversation

@bettio

@bettio bettio commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Implement erlang:process_info/1 and the list-of-keys form of
erlang:process_info/2, matching Erlang/OTP behaviour. Both are needed
by Popcorn.

This revamps the original, stalled effort by Mateusz Furga (Software
Mansion) in #2219.

Main differences from that approach:

  • process_info/1 is implemented in Erlang (estdlib) rather than C,
    simplifying the native code and making the default key set easier
    to maintain.
  • Pid and argument validation match OTP: badarg for external pids, and
    badarg for an invalid argument even when the target process is dead.
  • Fixes for pre-existing defects that process_info now exposes:
    message_queue_len no longer counts queued signals, a request racing
    process teardown answers undefined, and a caught trap exception no
    longer leaves the process wedged.
  • Allocation failures raise out_of_memory instead of trapping forever,
    and the result heap size is bounded.

Closes: #2190, #2191
See also: #2219

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 July 18, 2026 23:15
.. doxygenstruct:: BuiltInAtomRequestSignal
.. doxygenstruct:: ProcessInfoRequestSignal
:allow-dot-graphs:
.. doxygenstruct:: BuiltInAtomSignal

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.

Let's also fix this one.

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 Outdated
}
} // else: sender died

if (signal->len == 0) {

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.

Let's make it an assert instead (or two, (signal->mode == PROCESS_INFO_LIST) and len > 0). nif_erlang_process_info doesn't send PROCESS_INFO_LIST if length is 0.

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.

@petermm

petermm commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

AMP, usual caveats:

PR review: process_info/1,2 changes

Range reviewed: 2903b59b19fcd639368772c690d7b8aba4a81acd..ce3e2994e (last 13 commits)

Recommendation: Request changes. The normal behavior and teardown synchronization are well covered, but allocation failures and unchecked list-size arithmetic can still crash, corrupt, or permanently suspend the VM.

Findings

1. High — A reply allocation failure can crash the VM or leave the caller trapped forever

The caller sets Trap after the request has been posted (src/libAtomVM/nifs.c:3287-3292, 3318-3327), so every accepted request must eventually deliver a completion signal.

That guarantee does not hold:

  • mailbox_message_create_from_term() returns NULL when allocation fails (src/libAtomVM/mailbox.c:255-264), but mailbox_send_term_signal() passes the result directly to mailbox_post_message() (mailbox.c:293-297). Both mailbox implementations immediately dereference it (mailbox.c:232-250). This is a native null-pointer crash.
  • mailbox_send_immediate_signal() silently returns when its allocation fails (mailbox.c:299-305). The only exception reply is then lost, and the requester remains trapped indefinitely.
  • All new completion paths use one of these functions, including the teardown undefined reply and list-result replies (src/libAtomVM/context.c:327-397).

Commit c1c79aeb0 correctly makes request allocation failure raise out_of_memory, but response allocation remains fallible after the request has become irrevocable. The unchecked helper predates this range, but the new protocol depends on it for every list and teardown response and therefore does not satisfy its completion/liveness contract.

Suggested fix: reserve a no-allocation fallback completion before posting the request. For example, allocate an ImmediateSignal containing OUT_OF_MEMORY_ATOM together with the ProcessInfoRequestSignal; transfer it to the requester if normal response serialization fails, and free it after a successful response or if the requester has died. Merely adding a NULL check or returning bool from the send helpers would avoid the crash but would still strand the trapped caller, so this is not presented as a small patch.

Add allocator-fault tests for (1) term-response allocation, (2) exception-response allocation, and (3) the teardown undefined response.

2. High — List result-size overflow can under-allocate the heap and corrupt memory

Both list paths sum the required heap size without checking for size_t overflow:

  • Cross-process: src/libAtomVM/context.c:360-368
  • Self-process: src/libAtomVM/nifs.c:3330-3340

This is not limited by the request list's own memory use. A list can repeat a dynamic key such as links or monitored_by, so request length and per-item result size multiply. On a 32-bit target, two individually representable sizes can wrap total_size; memory_init_heap() also performs unchecked multiplication (src/libAtomVM/memory.c:61-68). The subsequent loops build the full result into the undersized heap (context.c:381-396, nifs.c:3348-3359), causing out-of-bounds writes instead of out_of_memory.

Use checked accumulation in both paths:

diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c
--- a/src/libAtomVM/context.c
+++ b/src/libAtomVM/context.c
@@
 #include <fenv.h>
 #include <math.h>
+#include <stdint.h>
@@
         for (size_t i = 0; i < signal->len; i++) {
             size_t item_size;
             if (UNLIKELY(!context_get_process_info(ctx, NULL, &item_size, signal->atoms[i], NULL))) {
                 mailbox_send_immediate_signal(target, TrapExceptionSignal, BADARG_ATOM);
                 goto done;
             }
-            total_size += item_size + CONS_SIZE;
+            if (UNLIKELY(item_size > SIZE_MAX - CONS_SIZE)
+                || UNLIKELY(total_size > SIZE_MAX - (item_size + CONS_SIZE))) {
+                mailbox_send_immediate_signal(target, TrapExceptionSignal, OUT_OF_MEMORY_ATOM);
+                goto done;
+            }
+            total_size += item_size + CONS_SIZE;
         }
diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c
--- a/src/libAtomVM/nifs.c
+++ b/src/libAtomVM/nifs.c
@@
     for (size_t i = 0; i < items_len; i++) {
         size_t item_size;
         // NOLINT(allocations-without-ensure-free) called with NULL heap, only computes size
         if (UNLIKELY(!context_get_process_info(ctx, NULL, &item_size, items[i], NULL))) {
             free(items_alloc);
             globalcontext_get_process_unlock(ctx->global, target);
             RAISE_ERROR(BADARG_ATOM);
         }
-        total_size += item_size + CONS_SIZE;
+        if (UNLIKELY(item_size > SIZE_MAX - CONS_SIZE)
+            || UNLIKELY(total_size > SIZE_MAX - (item_size + CONS_SIZE))) {
+            free(items_alloc);
+            globalcontext_get_process_unlock(ctx->global, target);
+            RAISE_ERROR(OUT_OF_MEMORY_ATOM);
+        }
+        total_size += item_size + CONS_SIZE;
     }

The signed reverse loops should also use size_t. This avoids depending on POSIX ssize_t in core/bare-metal builds and avoids implementation-defined conversion when a size_t length exceeds SSIZE_MAX:

diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c
--- a/src/libAtomVM/context.c
+++ b/src/libAtomVM/context.c
@@
-        for (ssize_t i = (ssize_t) signal->len - 1; i >= 0; i--) {
+        for (size_t i = signal->len; i-- > 0;) {
diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c
--- a/src/libAtomVM/nifs.c
+++ b/src/libAtomVM/nifs.c
@@
-    for (ssize_t i = (ssize_t) items_len - 1; i >= 0; i--) {
+    for (size_t i = items_len; i-- > 0;) {

The generic memory_init_heap() allocation arithmetic should also be hardened, but that broader pre-existing issue is outside this PR's smallest fix. The asynchronous branch still needs finding 1's guaranteed fallback response.

3. Medium — An asynchronous exception consumes the reply but leaves Trap set

Normal answers clear Trap in context_process_signal_trap_answer() (src/libAtomVM/context.c:406-410). TrapExceptionSignal handling only installs the exception in both execution engines:

  • Interpreter: src/libAtomVM/opcodesswitch.h:824-829
  • JIT: src/libAtomVM/jit.c:919-924

If an asynchronous out_of_memory is caught, exception handling clears the exception state but not the context flag (opcodesswitch.h:3253-3263, jit.c:1414-1422). At the next signal-processing boundary, the still-set Trap flag takes SCHEDULE_WAIT_ANY (opcodesswitch.h:920-928), despite the one expected completion already having been consumed. The process can then sleep forever.

The signal handler predates this range, but the new list allocation path adds another concrete producer at context.c:375-378. Clear the flag exactly as the successful completion path does:

diff --git a/src/libAtomVM/opcodesswitch.h b/src/libAtomVM/opcodesswitch.h
--- a/src/libAtomVM/opcodesswitch.h
+++ b/src/libAtomVM/opcodesswitch.h
@@
                 case TrapExceptionSignal: {                                                     \
                     struct ImmediateSignal *trap_exception                                      \
                         = CONTAINER_OF(signal_message, struct ImmediateSignal, base);           \
+                    context_update_flags(ctx, ~Trap, NoFlags);                                  \
                     SET_ERROR(trap_exception->immediate);                                       \
                     handle_error = true;                                                        \
                     break;                                                                      \
diff --git a/src/libAtomVM/jit.c b/src/libAtomVM/jit.c
--- a/src/libAtomVM/jit.c
+++ b/src/libAtomVM/jit.c
@@
             case TrapExceptionSignal: {
                 struct ImmediateSignal *trap_exception
                     = CONTAINER_OF(signal_message, struct ImmediateSignal, base);
+                context_update_flags(ctx, ~Trap, NoFlags);
                 set_error(ctx, jit_state, 0, trap_exception->immediate);
                 handle_error = true;
                 break;

Add a regression that injects or fault-induces a trap exception, catches it, and proves that the requester continues executing.

4. Low — The process_info/2 spec omits undefined for single-item queries

libs/estdlib/src/erlang.erl:366-376 permits undefined only for the list overload. AtomVM intentionally returns undefined for a dead process for every overload, as asserted throughout tests/erlang_tests/test_process_info.erl (for example lines 109, 125, 146, and 196). OTP has the same process-exit race.

The current spec therefore marks valid handling as unreachable and tells callers that an unconditional tuple match is safe when it is not.

diff --git a/libs/estdlib/src/erlang.erl b/libs/estdlib/src/erlang.erl
--- a/libs/estdlib/src/erlang.erl
+++ b/libs/estdlib/src/erlang.erl
@@
 -spec process_info
-    (Pid :: pid(), heap_size) -> {heap_size, non_neg_integer()};
-    (Pid :: pid(), total_heap_size) -> {total_heap_size, non_neg_integer()};
-    (Pid :: pid(), registered_name) -> {registered_name, term()} | [];
-    (Pid :: pid(), stack_size) -> {stack_size, non_neg_integer()};
-    (Pid :: pid(), message_queue_len) -> {message_queue_len, non_neg_integer()};
-    (Pid :: pid(), memory) -> {memory, non_neg_integer()};
-    (Pid :: pid(), links) -> {links, [pid()]};
-    (Pid :: pid(), monitored_by) -> {monitored_by, [pid() | resource() | port()]};
-    (Pid :: pid(), trap_exit) -> {trap_exit, boolean()};
+    (Pid :: pid(), heap_size) -> {heap_size, non_neg_integer()} | undefined;
+    (Pid :: pid(), total_heap_size) -> {total_heap_size, non_neg_integer()} | undefined;
+    (Pid :: pid(), registered_name) -> {registered_name, term()} | [] | undefined;
+    (Pid :: pid(), stack_size) -> {stack_size, non_neg_integer()} | undefined;
+    (Pid :: pid(), message_queue_len) -> {message_queue_len, non_neg_integer()} | undefined;
+    (Pid :: pid(), memory) -> {memory, non_neg_integer()} | undefined;
+    (Pid :: pid(), links) -> {links, [pid()]} | undefined;
+    (Pid :: pid(), monitored_by) -> {monitored_by, [pid() | resource() | port()]} | undefined;
+    (Pid :: pid(), trap_exit) -> {trap_exit, boolean()} | undefined;
     (Pid :: pid(), [atom()]) -> [{atom(), term()}] | undefined.

Additional suggested fixes

Guard the mailbox request's flexible-array allocation

The sizeof(header) + len * sizeof(term) expression is unchecked in src/libAtomVM/mailbox.c:315-316. Guard it before allocating:

diff --git a/src/libAtomVM/mailbox.c b/src/libAtomVM/mailbox.c
--- a/src/libAtomVM/mailbox.c
+++ b/src/libAtomVM/mailbox.c
@@
 bool mailbox_send_process_info_request_signal(
     Context *c, int32_t sender_pid, process_info_mode_t mode, const term *atoms, size_t len)
 {
+    if (UNLIKELY(len > (SIZE_MAX - sizeof(struct ProcessInfoRequestSignal)) / sizeof(term))) {
+        return false;
+    }
+
     struct ProcessInfoRequestSignal *signal = malloc(
         sizeof(struct ProcessInfoRequestSignal) + len * sizeof(term));

This can reuse the existing false/out_of_memory path and is independent of the larger guaranteed-response change in finding 1.

Make test-process cleanup unconditional

The with_other_pid/1 helper only cleans up after Fun(Pid) returns successfully. If an assertion fails, the child and its monitor message can survive into subsequent test cases. Use try ... after and a bounded wait for the monitored child:

diff --git a/tests/erlang_tests/test_process_info.erl b/tests/erlang_tests/test_process_info.erl
--- a/tests/erlang_tests/test_process_info.erl
+++ b/tests/erlang_tests/test_process_info.erl
@@
-    Fun(Pid),
-    Pid ! quit,
-    normal =
-        receive
-            {'DOWN', Ref, process, Pid, Reason} -> Reason
-        end.
+    try
+        Fun(Pid)
+    after
+        Pid ! quit,
+        normal =
+            receive
+                {'DOWN', Ref, process, Pid, Reason} -> Reason
+            after 1000 ->
+                erlang:error(timeout_waiting_for_child_exit)
+            end
+    end.

Apply the same fixture discipline to the estdlib process-info test where practical.

What was checked

  • Full diff and commit sequence for HEAD~13..HEAD, with surrounding process table, mailbox ownership/disposal, scheduler, interpreter, JIT, NIF, and estdlib code.
  • Request validation, external/dead PID handling, teardown locking, list order/duplicates, registered_name, and signal exclusion from message_queue_len.
  • Independent Oracle review, followed by validation of each reported issue against the current source and the pre-range implementation.
  • git diff --check HEAD~13..HEAD — clean.
  • Current AtomVM, test_estdlib, and tests/erlang_tests/test_process_info.beam targets build successfully.
  • build/src/AtomVM build/tests/erlang_tests/test_process_info.beam build/libs/atomvmlib.avm build/libs/estdlib/src/estdlib.avm — returns 0.
  • Rebuilt build/tests/libs/estdlib/test_estdlib.avm — full suite returns ok, including {test_process_info,ok}.

Allocator-fault and 32-bit overflow tests were not available, so findings 1–3 are source-verified rather than dynamically reproduced.

@bettio
bettio force-pushed the updated-nif-erlang-process_info branch from ce3e299 to 712dd87 Compare July 19, 2026 15:00
@petermm

petermm commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

AMP keeps insisting on these, your call..

Follow-up review: process_info/1,2 fixes

Head verified: 712dd8776
Fix range: 9900c9a0f..712dd8776

Recommendation: Changes still requested. The contributor fixed the list-size accumulation, portable reverse iteration, and single-item specifications. Reply delivery under allocation failure and asynchronous trap-exception handling remain correctness blockers.

Resolved findings

List result-size accumulation is now bounded

Both the self and cross-process paths now check each item and the running total against MEMORY_HEAP_MAX_TERMS before addition (src/libAtomVM/context.c:363-372, src/libAtomVM/nifs.c:3330-3348). This prevents total_size wrapping and makes the immediate memory_init_heap(total_size) multiplication safe.

The new 100-item self/remote test exercises repeated-key result construction and ordering. It is not an allocation-boundary test, but it provides useful coverage of the changed loops.

Reverse list construction is portable

Both loops now use:

for (size_t i = len; i-- > 0;)

This removes the dependency on POSIX ssize_t in core code and avoids implementation-defined conversion from a large size_t.

Single-item specs now include undefined

Every single-item process_info/2 overload in libs/estdlib/src/erlang.erl:366-376 now includes | undefined, matching the dead-process behavior and tests.

Remaining findings

1. High — Reply allocation can crash the VM or leave the requester trapped forever

An accepted cross-process request sets Trap, so it must receive exactly one completion. That guarantee is still absent:

  • mailbox_message_create_from_term() returns NULL on allocation failure (src/libAtomVM/mailbox.c:255-264).
  • mailbox_send_term_signal() passes that pointer directly to mailbox_post_message() (mailbox.c:293-297), whose enqueue paths dereference it. A failed reply allocation therefore causes a native null-pointer crash.
  • mailbox_send_immediate_signal() silently returns when its allocation fails (mailbox.c:299-305). The exception reply is lost and the requester remains trapped indefinitely.

The new list-size check does not close this path. A cross-process result is first built on a temporary heap and then copied into a separately allocated TermSignal; that second allocation still has unchecked size arithmetic and unchecked failure handling.

Required direction: reserve a no-allocation fallback completion before posting the request. One contained design is to allocate an ImmediateSignal carrying OUT_OF_MEMORY_ATOM with the ProcessInfoRequestSignal, transfer it to the requester if normal reply construction or serialization fails, and free it after successful completion or if the requester has died.

Merely making mailbox_send_term_signal() return false is insufficient: once the requester is trapped, the failure still needs a guaranteed completion path. For that reason, no small patch is suggested for this finding.

Add allocator-fault tests for:

  1. TrapAnswerSignal allocation failure;
  2. TrapExceptionSignal allocation failure;
  3. teardown's undefined response allocation failure.

2. Medium — TrapExceptionSignal still leaves Trap set

Normal answers clear Trap through context_process_signal_trap_answer(). Exception answers only install the exception:

  • Interpreter: src/libAtomVM/opcodesswitch.h:824-829
  • JIT: src/libAtomVM/jit.c:919-924

If the asynchronous exception is caught, exception state is cleared but the context remains marked Trap. At the next scheduling boundary it can enter SCHEDULE_WAIT_ANY even though its one expected completion was already consumed.

Clear Trap before raising the asynchronous exception in both engines:

diff --git a/src/libAtomVM/opcodesswitch.h b/src/libAtomVM/opcodesswitch.h
--- a/src/libAtomVM/opcodesswitch.h
+++ b/src/libAtomVM/opcodesswitch.h
@@
                 case TrapExceptionSignal: {                                                     \
                     struct ImmediateSignal *trap_exception                                      \
                         = CONTAINER_OF(signal_message, struct ImmediateSignal, base);           \
+                    context_update_flags(ctx, ~Trap, NoFlags);                                  \
                     SET_ERROR(trap_exception->immediate);                                       \
                     handle_error = true;                                                        \
                     break;                                                                      \
diff --git a/src/libAtomVM/jit.c b/src/libAtomVM/jit.c
--- a/src/libAtomVM/jit.c
+++ b/src/libAtomVM/jit.c
@@
             case TrapExceptionSignal: {
                 struct ImmediateSignal *trap_exception
                     = CONTAINER_OF(signal_message, struct ImmediateSignal, base);
+                context_update_flags(ctx, ~Trap, NoFlags);
                 set_error(ctx, jit_state, 0, trap_exception->immediate);
                 handle_error = true;
                 break;

Add a regression that fault-injects or directly delivers a trap exception, catches it, and proves the process continues executing.

Additional small suggestions

Guard the request signal's flexible-array allocation

The mailbox helper still evaluates sizeof(header) + len * sizeof(term) without checking for overflow (src/libAtomVM/mailbox.c:312-326). Guard the allocation at its API boundary and reuse the existing false/out_of_memory caller path:

diff --git a/src/libAtomVM/mailbox.c b/src/libAtomVM/mailbox.c
--- a/src/libAtomVM/mailbox.c
+++ b/src/libAtomVM/mailbox.c
@@
 bool mailbox_send_process_info_request_signal(
     Context *c, int32_t sender_pid, process_info_mode_t mode, const term *atoms, size_t len)
 {
+    if (UNLIKELY(len > (SIZE_MAX - sizeof(struct ProcessInfoRequestSignal)) / sizeof(term))) {
+        return false;
+    }
+
     struct ProcessInfoRequestSignal *signal = malloc(
         sizeof(struct ProcessInfoRequestSignal) + len * sizeof(term));

Make child cleanup unconditional in process-info tests

Both with_other_pid/1 helpers only clean up after Fun(Pid) succeeds. An assertion failure can leave the child and its monitor message alive. Use try ... after and a bounded wait in tests/erlang_tests/test_process_info.erl and tests/libs/estdlib/test_process_info.erl:

diff --git a/tests/erlang_tests/test_process_info.erl b/tests/erlang_tests/test_process_info.erl
--- a/tests/erlang_tests/test_process_info.erl
+++ b/tests/erlang_tests/test_process_info.erl
@@
-    Fun(Pid),
-    Pid ! quit,
-    normal =
-        receive
-            {'DOWN', Ref, process, Pid, Reason} -> Reason
-        end.
+    try
+        Fun(Pid)
+    after
+        Pid ! quit,
+        normal =
+            receive
+                {'DOWN', Ref, process, Pid, Reason} -> Reason
+            after 1000 ->
+                erlang:error(timeout_waiting_for_child_exit)
+            end
+    end.

Apply the same hunk to the estdlib helper.

Verification

  • git diff --check 9900c9a0f..712dd8776 — clean.
  • AtomVM, test_estdlib, and tests/erlang_tests/test_process_info.beam rebuilt successfully.
  • Focused native test_process_info — returned 0.
  • Full estdlib suite — returned ok, including {test_process_info,ok}.
  • Independent Oracle follow-up review confirmed which original findings are resolved and which remain.
  • Sphinx generation reached the estdlib documentation, then failed in unrelated alisp edoc generation because edoc scanned an erlfmt plugin source containing an unsupported declaration. No failure was observed in the changed data-structure documentation.

No ordinary-input behavioral regression was found in the pushed fixes. Allocator-fault and allocation-boundary behavior remains source-verified rather than dynamically reproduced.

@bettio
bettio force-pushed the updated-nif-erlang-process_info branch from 712dd87 to 054d8ce Compare July 20, 2026 16:34
@bettio

bettio commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

AMP keeps insisting on these, your call..

My answers:

1. High — Reply allocation can crash the VM or leave the requester trapped forever

I don't think this really applies to the PR. The issue is eventually somewhere else.

2. Medium — TrapExceptionSignal still leaves Trap set

Fixed.

Additional small suggestions

Guard the request signal's flexible-array allocation

This issue cannot be triggered on a real world scenario.

Make child cleanup unconditional in process-info tests

I think that this makes things hard to debug.

@bettio
bettio force-pushed the updated-nif-erlang-process_info branch from 054d8ce to b708c0c Compare July 22, 2026 17:34
Comment thread src/libAtomVM/nifs.c Fixed
Comment thread src/libAtomVM/nifs.c Fixed
mfurga and others added 12 commits July 24, 2026 17:21
Signed-off-by: Mateusz Furga <mateusz.furga@swmansion.com>
process_info/1 is by OTP definition one process_info/2 list call plus
a presentation rule, so implement it in Erlang, which simplifies the
C code and makes it easier to maintain. The /1 tests move to
tests/libs/estdlib accordingly, and the previously commented out
cross-process check is enabled.

Signed-off-by: Davide Bettio <davide@uninstall.it>
OTP raises badarg for external pids and validates the info request
argument before checking whether the target process is alive, so an
invalid item or an improper list is a badarg even for a dead pid.
Match that.

Signed-off-by: Davide Bettio <davide@uninstall.it>
A failed request signal allocation left the caller trapped forever
waiting for an answer: raise out_of_memory instead. Also bound the
accumulated result heap size, which a request repeating dynamically
sized items could otherwise overflow.

Signed-off-by: Davide Bettio <davide@uninstall.it>
Erlang/OTP answers undefined for an exiting process, but a request
processed from the destroy path answered with a mid-destruction
snapshot. Reply undefined there instead; the destroy path is uniquely
identified by process_table_locked, as context_destroy is the only
caller holding the process table write lock.

Signed-off-by: Davide Bettio <davide@uninstall.it>
message_queue_len counted unprocessed system signals in the mailbox,
while Erlang/OTP counts only ordinary messages, including accepted
alias messages even if the alias is deactivated before reception.
Count NormalMessage and AliasMessageSignal entries only.

Signed-off-by: Davide Bettio <davide@uninstall.it>
The exception path of a trap completion did not clear the Trap flag,
so a process that caught the exception parked for good at the next
signal processing point, never draining its mailbox again. Clear it
like the answer path does.

Signed-off-by: Davide Bettio <davide@uninstall.it>
Follow C_CODING_STYLE.md naming for the new identifiers, replace
unreachable branches with asserts, use size_t for the list build
loops, remove redundant comments and wrap over-limit lines.

Signed-off-by: Davide Bettio <davide@uninstall.it>
The CodeQL allocations-without-ensure-free query flagged the item
validity probes, which cannot allocate, and its suppression window
cannot cover the multi-line call sites. Validate items against a new
non-allocating key switch instead.

Signed-off-by: Davide Bettio <davide@uninstall.it>
Assert invariants instead of exact double-read equality for metrics
that GC may change between two reads, and do not require the OTP-wise
unspecified registered_name position on BEAM. Add missing coverage:
memory growth with queued messages, trap_exit values, port and
nested-list badargs, and C-level mailbox_normal_message_len tests.

Signed-off-by: Davide Bettio <davide@uninstall.it>
Document process_info/1 and the list form in erlang.erl and in the
programmers guide, record in UPDATING.md that process_info/2 no
longer accepts port identifiers, and fix the stale ImmediateSignal
entry in the data structures docs.

Signed-off-by: Davide Bettio <davide@uninstall.it>
AtomsHashTable was removed in 2ab1acb, but the API documentation
still listed it, leaving a directive that resolves to nothing.

Signed-off-by: Davide Bettio <davide@uninstall.it>
@bettio
bettio force-pushed the updated-nif-erlang-process_info branch from b708c0c to e72f93f Compare July 24, 2026 15:22
@bettio bettio changed the title Updated nif erlang process info [Updated] Add process_info/1 and process_info/2 with list argument Jul 24, 2026
@bettio
bettio requested a review from pguyot July 24, 2026 15:34
@petermm

petermm commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

AMP: https://ampcode.com/threads/T-019f9561-3858-759a-8ed8-13c4ff1ccd17

review last 12 commits, use gh and consider comments on #2374 - ask the oracle, write findings to PR_REVIEW.md in markdown, include code diffs for small fixes/suggestions

seems happy - except for this allocate reply pattern, as adressed before..

PR review: process_info/1,2 final 12-commit series

Pull request: atomvm/AtomVM#2374
Range reviewed: c4afa59b5..e72f93f3a (HEAD~12..HEAD)
Recommendation: Request changes. The final series resolves the previously reported trap-state, teardown, validation, sizing, and queue-counting defects, but an accepted cross-process request can still crash the VM or leave its caller trapped forever if reply allocation fails.

Findings

1. High — Reply allocation failure can crash the VM or permanently strand the trapped caller

The remote single and list paths publish a request and then set Trap (src/libAtomVM/nifs.c:3394-3401, 3427-3437). From that point, the requester needs exactly one TrapAnswerSignal or TrapExceptionSignal.

The responder cannot guarantee that completion:

  • mailbox_message_create_from_term() returns NULL when allocation fails (src/libAtomVM/mailbox.c:257-265), but mailbox_send_term_signal() passes the result directly to mailbox_post_message() (mailbox.c:295-299). Both mailbox implementations dereference the pointer (mailbox.c:234-253), so a failed answer allocation is a native null-pointer crash.
  • mailbox_send_immediate_signal() silently returns after its allocation fails (mailbox.c:301-307). An exception answer is then lost and the caller remains trapped indefinitely.
  • Every completion in context_process_process_info_request_signal() uses one of those helpers (src/libAtomVM/context.c:344-419), including the teardown undefined response.

The generic unchecked helpers predate this PR, but the new list form materially increases the defect's reachability: it builds a caller-sized result on a temporary heap and then allocates a second full-sized block to serialize the TermSignal. The target can therefore successfully build a large result at context.c:396-419, fail the second allocation, and crash while posting NULL. This also contradicts the PR description's claim that allocation failures raise out_of_memory rather than trapping forever.

Required direction: keep the fix local to the process-info protocol. Reserve an ImmediateSignal carrying OUT_OF_MEMORY_ATOM before publishing the request, store it with ProcessInfoRequestSignal, and make normal answer creation fallible instead of posting NULL. On answer-allocation failure, transfer the reserved signal to the requester; after a successful answer, or when the requester has already died, free it. Request disposal must likewise free an unused fallback. Merely adding a NULL check avoids the crash but still strands the trapped caller, so there is no safe one-line patch for this finding.

Add allocator-fault tests for:

  1. TrapAnswerSignal allocation after a list result was built;
  2. TrapExceptionSignal allocation after temporary-heap failure;
  3. the teardown undefined answer.

Small suggestion

Low — The monitor-flood test can pass without the flooder ever running

test_message_queue_len_signals/0 spawns Flooder and immediately samples the caller's queue 200 times (tests/erlang_tests/test_process_info.erl:208-232). There is no synchronization proving that Flooder completed even one monitor/demonitor cycle. On a single scheduler, every sample can run against an untouched mailbox and the test still passes.

The deterministic C coverage in tests/test-mailbox.c:127-152 does validate the filtering helper, so this is not an implementation blocker. A small out-of-band registration handshake would make the Erlang concurrency test exercise its intended setup without adding a normal message to the queue under test:

diff --git a/tests/erlang_tests/test_process_info.erl b/tests/erlang_tests/test_process_info.erl
--- a/tests/erlang_tests/test_process_info.erl
+++ b/tests/erlang_tests/test_process_info.erl
@@
 test_message_queue_len_signals() ->
     Self = self(),
     {Flooder, Ref} = spawn_opt(fun() -> monitor_flood(Self) end, [monitor]),
+    ok = wait_registered(process_info_monitor_flood_started, 1000000),
     ok = sample_empty_message_queue(Self, 200),
     Flooder ! stop,
@@
 monitor_flood(Target) ->
     Mon = monitor(process, Target),
     demonitor(Mon),
+    register(process_info_monitor_flood_started, self()),
+    monitor_flood_loop(Target).
+
+monitor_flood_loop(Target) ->
+    Mon = monitor(process, Target),
+    demonitor(Mon),
     receive
         stop -> ok
-    after 0 -> monitor_flood(Target)
+    after 0 -> monitor_flood_loop(Target)
     end.

(wait_registered/2 already exists later in this test module.)

Prior review comments checked against final HEAD

  • Resolved: TrapExceptionSignal now clears Trap in both the interpreter and JIT (src/libAtomVM/opcodesswitch.h:824-830, src/libAtomVM/jit.c:926-932).
  • Resolved: both self and remote list-result paths bound accumulation by MEMORY_HEAP_MAX_TERMS and use portable reverse size_t loops (src/libAtomVM/nifs.c:3440-3474, src/libAtomVM/context.c:380-419).
  • Resolved: PID type and the complete proper-list/key validation happen before target lookup, preserving OTP's badarg versus undefined behavior (src/libAtomVM/nifs.c:3342-3368).
  • Resolved under successful reply allocation: process teardown removes the target while holding the process-table write lock, drains accepted requests, and answers undefined (src/libAtomVM/context.c:145-178, 344-348).
  • Resolved: message_queue_len excludes control signals while counting normal and pending alias messages (src/libAtomVM/mailbox.c:192-207).
  • Not actionable: the CodeQL comments on context_get_process_info(..., NULL) are false positives for those call sites; with a NULL output/heap the function only validates the key and/or computes the required size.
  • Not a practical input overflow: a valid resident Erlang list consumes enough heap that list_len * sizeof(term) and the request signal's flexible-array allocation cannot independently approach SIZE_MAX first.

Verification

  • Reviewed the complete final diff and all 12 commits, plus surrounding mailbox ownership, process-table locking, signal processing, interpreter/JIT trap handling, and result construction.
  • Rechecked all inline PR review comments and the subsequent issue discussion through gh.
  • Independent Oracle review confirmed the remaining allocation/liveness blocker and the monitor-flood coverage gap.
  • git diff --check HEAD~12..HEAD — clean.
  • Rebuilt AtomVM, test-mailbox, test_process_info.beam, and test_estdlib successfully.
  • build/tests/test-mailbox — passed.
  • Focused native test_process_info — returned 0.
  • Full estdlib suite — returned ok, including {test_process_info,ok}.
  • GitHub's matrix was still running at review time. The available logs for two visible failed ESP32/STM32 jobs show erlef/setup-beam dependency-download HTTP 504s, not source or test failures; logs for failures in still-running workflows were not yet available.

Allocator-fault injection was not available, so finding 1 is source-verified rather than dynamically reproduced.

Comment thread src/libAtomVM/jit.c
case TrapAnswerSignal: {
struct TermSignal *trap_answer
= CONTAINER_OF(signal_message, struct TermSignal, base);
if (UNLIKELY(!context_process_signal_trap_answer(ctx, trap_answer))) {

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.

As you're fixing the trap flag clear below, we could take advantage of this to change the signature of this function as it seems it always returns true.

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