Skip to content

Commit 3dd59d5

Browse files
committed
Merge pull request #1547 from pguyot/w08/gc-signals
Fix a bug related to process_info memory usage When calling repeatedly process_info on another process, memory allocated to create the reply tuple was never freed until the process did a garbage collection. Fix the bug by allocating the response on a disposed heap since this is copied to a message. 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
2 parents 7244185 + 0578d2f commit 3dd59d5

File tree

6 files changed

+146
-39
lines changed

6 files changed

+146
-39
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ bug when handling errors from BIFs used as NIFs (when called with `CALL_EXT` and
5151
- Fixed potential crashes or memory leaks caused by a mistake in calculation of reference counts
5252
and a race condition in otp_socket code
5353
- Fixed an out of memory issue by forcing GC to copy data from message fragments
54+
- Fixed a bug where calling repeatedly `process_info` on a stopped process could cause an out of
55+
memory error
5456

5557
## [0.6.5] - 2024-10-15
5658

src/libAtomVM/context.c

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <fenv.h>
2424
#include <math.h>
2525

26+
#include "defaultatoms.h"
2627
#include "dictionary.h"
2728
#include "erl_nif.h"
2829
#include "erl_nif_priv.h"
@@ -194,11 +195,22 @@ void context_process_process_info_request_signal(Context *ctx, struct BuiltInAto
194195
{
195196
Context *target = globalcontext_get_process_lock(ctx->global, signal->sender_pid);
196197
if (target) {
197-
term ret;
198-
if (context_get_process_info(ctx, &ret, signal->atom)) {
199-
mailbox_send_term_signal(target, TrapAnswerSignal, ret);
198+
size_t term_size;
199+
if (context_get_process_info(ctx, NULL, &term_size, signal->atom, NULL)) {
200+
Heap heap;
201+
if (UNLIKELY(memory_init_heap(&heap, term_size) != MEMORY_GC_OK)) {
202+
mailbox_send_built_in_atom_signal(target, TrapExceptionSignal, OUT_OF_MEMORY_ATOM);
203+
} else {
204+
term ret;
205+
if (context_get_process_info(ctx, &ret, NULL, signal->atom, &heap)) {
206+
mailbox_send_term_signal(target, TrapAnswerSignal, ret);
207+
} else {
208+
mailbox_send_built_in_atom_signal(target, TrapExceptionSignal, ret);
209+
}
210+
memory_destroy_heap(&heap, ctx->global);
211+
}
200212
} else {
201-
mailbox_send_built_in_atom_signal(target, TrapExceptionSignal, ret);
213+
mailbox_send_built_in_atom_signal(target, TrapExceptionSignal, BADARG_ATOM);
202214
}
203215
globalcontext_get_process_unlock(ctx->global, target);
204216
} // else: sender died
@@ -262,7 +274,7 @@ size_t context_size(Context *ctx)
262274
+ memory_heap_memory_size(&ctx->heap) * BYTES_PER_TERM;
263275
}
264276

265-
bool context_get_process_info(Context *ctx, term *out, term atom_key)
277+
bool context_get_process_info(Context *ctx, term *out, size_t *term_size, term atom_key, Heap *heap)
266278
{
267279
size_t ret_size;
268280
switch (atom_key) {
@@ -286,16 +298,19 @@ bool context_get_process_info(Context *ctx, term *out, term atom_key)
286298
break;
287299
}
288300
default:
289-
*out = BADARG_ATOM;
301+
if (out != NULL) {
302+
*out = BADARG_ATOM;
303+
}
290304
return false;
291305
}
292-
293-
if (UNLIKELY(memory_ensure_free(ctx, ret_size) != MEMORY_GC_OK)) {
294-
*out = OUT_OF_MEMORY_ATOM;
295-
return false;
306+
if (term_size != NULL) {
307+
*term_size = ret_size;
308+
}
309+
if (out == NULL) {
310+
return true;
296311
}
297312

298-
term ret = term_alloc_tuple(2, &ctx->heap);
313+
term ret = term_alloc_tuple(2, heap);
299314
switch (atom_key) {
300315
// heap_size size in words of the heap of the process
301316
case HEAP_SIZE_ATOM: {
@@ -346,7 +361,7 @@ bool context_get_process_info(Context *ctx, term *out, term atom_key)
346361
struct Monitor *monitor = GET_LIST_ENTRY(item, struct Monitor, monitor_list_head);
347362
// Links are struct Monitor entries with ref_ticks equal to 0
348363
if (monitor->ref_ticks == 0) {
349-
list = term_list_prepend(monitor->monitor_obj, list, &ctx->heap);
364+
list = term_list_prepend(monitor->monitor_obj, list, heap);
350365
}
351366
}
352367
term_put_tuple_element(ret, 1, list);

src/libAtomVM/context.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -396,12 +396,14 @@ void context_process_flush_monitor_signal(Context *ctx, uint64_t ref_ticks, bool
396396
* @brief Get process information.
397397
*
398398
* @param ctx the context being executed
399-
* @param out the answer term
399+
* @param out the answer term. Can be NULL if only the size matters.
400+
* @param term_size the size of the answer term, in words.
400401
* @param atom_key the key representing the info to get
402+
* @param heap the heap to allocate the answer to
401403
* @return \c true if successful, \c false in case of an error in which case
402-
* *out is filled with an exception atom
404+
* *out is filled with an exception atom if it was not NULL
403405
*/
404-
bool context_get_process_info(Context *ctx, term *out, term atom_key);
406+
bool context_get_process_info(Context *ctx, term *out, size_t *term_size, term atom_key, Heap *heap);
405407

406408
/**
407409
* @brief Half-link process to another process

src/libAtomVM/nifs.c

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2800,7 +2800,16 @@ static term nif_erlang_process_info(Context *ctx, int argc, term argv[])
28002800

28012801
term ret = term_invalid_term();
28022802
if (ctx == target) {
2803-
if (!context_get_process_info(ctx, &ret, item)) {
2803+
size_t term_size;
2804+
if (UNLIKELY(!context_get_process_info(ctx, NULL, &term_size, item, NULL))) {
2805+
globalcontext_get_process_unlock(ctx->global, target);
2806+
RAISE_ERROR(BADARG_ATOM);
2807+
}
2808+
if (UNLIKELY(memory_ensure_free_opt(ctx, term_size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) {
2809+
globalcontext_get_process_unlock(ctx->global, target);
2810+
RAISE_ERROR(OUT_OF_MEMORY_ATOM);
2811+
}
2812+
if (UNLIKELY(!context_get_process_info(ctx, &ret, NULL, item, &ctx->heap))) {
28042813
globalcontext_get_process_unlock(ctx->global, target);
28052814
RAISE_ERROR(ret);
28062815
}

tests/erlang_tests/test_heap_growth.erl

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,24 +80,80 @@ test_bounded_free_strategy(UseDefault) ->
8080
ok.
8181

8282
test_minimum_strategy() ->
83+
Parent = self(),
8384
{Pid1, Ref1} = spawn_opt(
8485
fun() ->
85-
{total_heap_size, X1} = process_info(self(), total_heap_size),
86+
receive
87+
{step, 1} -> Parent ! ok
88+
end,
89+
receive
90+
{step, 2} -> Parent ! ok
91+
end,
8692
% Cannot really be 0 because we allocate more than just the list.
8793
ok = test_growth_bounded(10),
88-
{total_heap_size, X2} = process_info(self(), total_heap_size),
94+
receive
95+
{step, 3} -> Parent ! ok
96+
end,
97+
receive
98+
{step, 4} -> Parent ! ok
99+
end,
89100
% Allocate again, this is when heap will be shrunk
90101
Var1 = alloc_some_heap_words(10),
91-
{total_heap_size, X3} = process_info(self(), total_heap_size),
92-
20 = erts_debug:flat_size(Var1),
93-
true = X3 < X2,
94-
true = X3 - X1 - erts_debug:flat_size(Var1) =< 10
102+
receive
103+
{step, 5} -> Parent ! ok
104+
end,
105+
receive
106+
{step, 6} -> Parent ! ok
107+
end,
108+
20 = erts_debug:flat_size(Var1)
95109
end,
96110
[monitor, {atomvm_heap_growth, minimum}]
97111
),
112+
% Get heap size from the outside to have no influence on the heap
113+
Pid1 ! {step, 1},
98114
ok =
99115
receive
100-
{'DOWN', Ref1, process, Pid1, normal} -> ok
116+
ok -> ok
117+
after 1000 -> timeout
118+
end,
119+
{total_heap_size, X1} = process_info(Pid1, total_heap_size),
120+
Pid1 ! {step, 2},
121+
ok =
122+
receive
123+
ok -> ok
124+
after 1000 -> timeout
125+
end,
126+
Pid1 ! {step, 3},
127+
ok =
128+
receive
129+
ok -> ok
130+
after 1000 -> timeout
131+
end,
132+
{total_heap_size, X2} = process_info(Pid1, total_heap_size),
133+
Pid1 ! {step, 4},
134+
ok =
135+
receive
136+
ok -> ok
137+
after 1000 -> timeout
138+
end,
139+
Pid1 ! {step, 5},
140+
ok =
141+
receive
142+
ok -> ok
143+
after 1000 -> timeout
144+
end,
145+
{total_heap_size, X3} = process_info(Pid1, total_heap_size),
146+
Pid1 ! {step, 6},
147+
ok =
148+
receive
149+
ok -> ok
150+
after 1000 -> timeout
151+
end,
152+
true = X3 < X2,
153+
true = X3 - X1 - 20 =< 10,
154+
normal =
155+
receive
156+
{'DOWN', Ref1, process, Pid1, Reason} -> Reason
101157
after 500 -> timeout
102158
end,
103159
ok.

tests/erlang_tests/test_process_info.erl

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@ start() ->
3939
receive
4040
{Pid, result, X} -> X
4141
end,
42-
receive
43-
{'DOWN', Ref, process, Pid, Reason} ->
44-
assert(Reason =:= normal)
45-
end,
42+
normal =
43+
receive
44+
{'DOWN', Ref, process, Pid, Reason} -> Reason
45+
end,
4646
MessageQueueLen = process_info(Pid, message_queue_len),
47-
assert(MessageQueueLen =:= undefined),
47+
MessageQueueLen = undefined,
48+
ok = test_process_info_memory_other(),
4849
0.
4950

5051
test_message_queue_len(Pid, Self) ->
@@ -64,20 +65,44 @@ test_message_queue_len(Pid, Self) ->
6465
end,
6566
{total_heap_size, TotalHeapSize2} = process_info(Pid, total_heap_size),
6667
{heap_size, HeapSize2} = process_info(Pid, heap_size),
67-
assert(MessageQueueLen < MessageQueueLen2),
68+
true = MessageQueueLen < MessageQueueLen2,
6869
case erlang:system_info(machine) of
6970
"BEAM" ->
70-
assert(Memory =< Memory2),
71-
assert(HeapSize =< TotalHeapSize),
72-
assert(HeapSize2 =< TotalHeapSize2),
73-
assert(TotalHeapSize =< TotalHeapSize2);
71+
true = Memory =< Memory2,
72+
true = HeapSize =< TotalHeapSize,
73+
true = HeapSize2 =< TotalHeapSize2,
74+
true = TotalHeapSize =< TotalHeapSize2;
7475
_ ->
75-
assert(Memory < Memory2),
76-
assert(HeapSize =< TotalHeapSize),
77-
assert(HeapSize2 =< TotalHeapSize2),
78-
assert(TotalHeapSize < TotalHeapSize2)
76+
true = Memory < Memory2,
77+
true = HeapSize =< TotalHeapSize,
78+
true = HeapSize2 =< TotalHeapSize2,
79+
true = TotalHeapSize =< TotalHeapSize2
7980
end.
8081

82+
test_process_info_memory_other() ->
83+
{Pid, Ref} = spawn_opt(
84+
fun() ->
85+
receive
86+
quit -> ok
87+
end
88+
end,
89+
[monitor]
90+
),
91+
{total_heap_size, THS0} = process_info(Pid, total_heap_size),
92+
test_process_info_memory_other_loop(Pid, THS0, 50),
93+
Pid ! quit,
94+
normal =
95+
receive
96+
{'DOWN', Ref, process, Pid, Reason} -> Reason
97+
end,
98+
ok.
99+
100+
test_process_info_memory_other_loop(_Pid, _THS0, 0) ->
101+
ok;
102+
test_process_info_memory_other_loop(Pid, THS0, N) ->
103+
{total_heap_size, THS0} = process_info(Pid, total_heap_size),
104+
test_process_info_memory_other_loop(Pid, THS0, N - 1).
105+
81106
loop(undefined, Accum) ->
82107
receive
83108
{Pid, stop} ->
@@ -95,5 +120,3 @@ loop(locked, Accum) ->
95120
loop(Pid, Accum) ->
96121
Pid ! ok,
97122
loop(locked, Accum).
98-
99-
assert(true) -> ok.

0 commit comments

Comments
 (0)