Skip to content

Commit 27e4ee3

Browse files
committed
Merge pull request atomvm#729 from pguyot/w31/monitor-posix_select-s
Add monitor to posix_select Monitor selecting process to avoid leaks. Add tests 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 edc3f3d + bec1ec1 commit 27e4ee3

File tree

2 files changed

+129
-10
lines changed

2 files changed

+129
-10
lines changed

src/libAtomVM/posix_nifs.c

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
#include "defaultatoms.h"
3737
#include "erl_nif_priv.h"
38+
#include "globalcontext.h"
3839
#include "interop.h"
3940
#include "nifs.h"
4041
#include "posix_nifs.h"
@@ -115,7 +116,8 @@ term posix_errno_to_term(int err, GlobalContext *glb)
115116
struct PosixFd
116117
{
117118
int fd;
118-
bool select;
119+
int32_t selecting_process_id;
120+
ErlNifMonitor selecting_process_monitor;
119121
};
120122

121123
static void posix_fd_dtor(ErlNifEnv *caller_env, void *obj)
@@ -131,18 +133,32 @@ static void posix_fd_dtor(ErlNifEnv *caller_env, void *obj)
131133

132134
static void posix_fd_stop(ErlNifEnv *caller_env, void *obj, ErlNifEvent event, int is_direct_call)
133135
{
134-
UNUSED(caller_env);
135136
UNUSED(event);
136137
UNUSED(is_direct_call);
137138

138139
struct PosixFd *fd_obj = (struct PosixFd *) obj;
139-
fd_obj->select = false;
140+
if (fd_obj->selecting_process_id != INVALID_PROCESS_ID) {
141+
enif_demonitor_process(caller_env, fd_obj, &fd_obj->selecting_process_monitor);
142+
fd_obj->selecting_process_id = INVALID_PROCESS_ID;
143+
}
144+
}
145+
146+
static void posix_fd_down(ErlNifEnv *caller_env, void *obj, ErlNifPid *pid, ErlNifMonitor *mon)
147+
{
148+
UNUSED(pid);
149+
UNUSED(mon);
150+
struct PosixFd *fd_obj = (struct PosixFd *) obj;
151+
if (fd_obj->selecting_process_id != INVALID_PROCESS_ID) {
152+
fd_obj->selecting_process_id = INVALID_PROCESS_ID;
153+
enif_select(caller_env, fd_obj->fd, ERL_NIF_SELECT_STOP, fd_obj, NULL, term_nil());
154+
}
140155
}
141156

142157
const ErlNifResourceTypeInit posix_fd_resource_type_init = {
143-
.members = 2,
158+
.members = 3,
144159
.dtor = posix_fd_dtor,
145160
.stop = posix_fd_stop,
161+
.down = posix_fd_down,
146162
};
147163

148164
#define O_EXEC_ATOM_STR ATOM_STR("\x6", "o_exec")
@@ -270,7 +286,7 @@ static term nif_atomvm_posix_open(Context *ctx, int argc, term argv[])
270286
RAISE_ERROR(OUT_OF_MEMORY_ATOM);
271287
}
272288
fd_obj->fd = fd;
273-
fd_obj->select = false;
289+
fd_obj->selecting_process_id = INVALID_PROCESS_ID;
274290
if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2) + TERM_BOXED_RESOURCE_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) {
275291
RAISE_ERROR(OUT_OF_MEMORY_ATOM);
276292
}
@@ -295,7 +311,7 @@ static term nif_atomvm_posix_close(Context *ctx, int argc, term argv[])
295311
}
296312
struct PosixFd *fd_obj = (struct PosixFd *) fd_obj_ptr;
297313
if (fd_obj->fd != CLOSED_FD) {
298-
if (fd_obj->select) {
314+
if (fd_obj->selecting_process_id != INVALID_PROCESS_ID) {
299315
fprintf(stderr, "Calling close on a selectable posix file, missing call to posix_select_stop?\n");
300316
}
301317
if (UNLIKELY(close(fd_obj->fd) < 0)) {
@@ -398,11 +414,26 @@ static term nif_atomvm_posix_select(Context *ctx, term argv[], enum ErlNifSelect
398414
RAISE_ERROR(BADARG_ATOM);
399415
}
400416
struct PosixFd *fd_obj = (struct PosixFd *) fd_obj_ptr;
401-
402-
if (UNLIKELY(enif_select(erl_nif_env_from_context(ctx), fd_obj->fd, mode, fd_obj, &process_pid, select_ref_term) < 0)) {
417+
ErlNifEnv *env = erl_nif_env_from_context(ctx);
418+
if (fd_obj->selecting_process_id != process_pid && fd_obj->selecting_process_id != INVALID_PROCESS_ID) {
419+
if (UNLIKELY(enif_demonitor_process(env, fd_obj, &fd_obj->selecting_process_monitor) != 0)) {
420+
RAISE_ERROR(BADARG_ATOM);
421+
}
422+
fd_obj->selecting_process_id = INVALID_PROCESS_ID;
423+
}
424+
// Monitor first as select is less likely to fail and it's less expensive to demonitor
425+
// if select fails than to stop select if monitor fails
426+
if (fd_obj->selecting_process_id != process_pid) {
427+
if (UNLIKELY(enif_monitor_process(env, fd_obj, &process_pid, &fd_obj->selecting_process_monitor) != 0)) {
428+
RAISE_ERROR(BADARG_ATOM);
429+
}
430+
fd_obj->selecting_process_id = process_pid;
431+
}
432+
if (UNLIKELY(enif_select(env, fd_obj->fd, mode, fd_obj, &process_pid, select_ref_term) < 0)) {
433+
enif_demonitor_process(env, fd_obj, &fd_obj->selecting_process_monitor);
434+
fd_obj->selecting_process_id = INVALID_PROCESS_ID;
403435
RAISE_ERROR(BADARG_ATOM);
404436
}
405-
fd_obj->select = 1;
406437

407438
return OK_ATOM;
408439
}

tests/libs/eavmlib/test_file.erl

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ test() ->
2929
ok = test_basic_file(),
3030
ok = test_fifo_select(HasSelect),
3131
ok = test_gc(HasSelect),
32+
ok = test_crash_no_leak(HasSelect),
33+
ok = test_select_with_gone_process(HasSelect),
3234
ok.
3335

3436
test_basic_file() ->
@@ -83,7 +85,26 @@ test_fifo_select(_HasSelect) ->
8385
M2 -> {unexpected, M2}
8486
after 200 -> fail
8587
end,
86-
ok = atomvm:posix_select_stop(RdFd),
88+
Parent = self(),
89+
Child = spawn(fun() ->
90+
ChildRef = make_ref(),
91+
ok = atomvm:posix_select_read(RdFd, self(), ChildRef),
92+
ok =
93+
receive
94+
{select, RdFd, ChildRef, ready_input} -> ok;
95+
M3 -> {unexpected, M3}
96+
after 200 -> fail
97+
end,
98+
ok = atomvm:posix_select_stop(RdFd),
99+
{ok, <<" World">>} = atomvm:posix_read(RdFd, 10),
100+
Parent ! {self(), world}
101+
end),
102+
ok =
103+
receive
104+
{Child, world} -> ok;
105+
M4 -> {unexpected, M4}
106+
after 200 -> fail
107+
end,
87108
ok =
88109
receive
89110
Message -> {unexpected, Message}
@@ -179,3 +200,70 @@ gc_loop(Path, File) ->
179200
{Caller, quit} ->
180201
Caller ! {self(), quit}
181202
end.
203+
204+
% Test is based on the fact that `erlang:memory(binary)` count resources.
205+
test_crash_no_leak(false) ->
206+
ok;
207+
test_crash_no_leak(true) ->
208+
Before = erlang:memory(binary),
209+
Path = "/tmp/atomvm.tmp." ++ integer_to_list(erlang:system_time(millisecond)),
210+
Pid = spawn(fun() -> crash_leak(Path) end),
211+
Ref = monitor(process, Pid),
212+
ok =
213+
receive
214+
{'DOWN', Ref, process, Pid, _} -> ok
215+
after 5000 -> timeout
216+
end,
217+
% Make sure sys_poll_events is called so selected resource is
218+
% properly disposed. This also gives time to context_destroy to
219+
% sweep mso after DOWN message is sent
220+
receive
221+
after 100 -> ok
222+
end,
223+
After = erlang:memory(binary),
224+
0 = After - Before,
225+
ok = atomvm:posix_unlink(Path),
226+
ok.
227+
228+
crash_leak(Path) ->
229+
ok = setup_and_forget(Path),
230+
% We don't really need to crash here
231+
% the test consists in not calling select_stop
232+
ok.
233+
234+
setup_and_forget(Path) ->
235+
{ok, Fd} = atomvm:posix_open(Path, [o_rdwr, o_creat], 8#644),
236+
ok = atomvm:posix_select_write(Fd, self(), undefined),
237+
ok.
238+
239+
test_select_with_gone_process(false) ->
240+
ok;
241+
test_select_with_gone_process(true) ->
242+
Before = erlang:memory(binary),
243+
Path = "/tmp/atomvm.tmp." ++ integer_to_list(erlang:system_time(millisecond)),
244+
test_select_with_gone_process0(Path),
245+
% GC so Fd's ref count is decremented
246+
erlang:garbage_collect(),
247+
After = erlang:memory(binary),
248+
0 = After - Before,
249+
ok = atomvm:posix_unlink(Path),
250+
ok.
251+
252+
test_select_with_gone_process0(Path) ->
253+
GonePid = spawn(fun() -> ok end),
254+
Ref = monitor(process, GonePid),
255+
ok =
256+
receive
257+
{'DOWN', Ref, process, GonePid, _} -> ok
258+
after 5000 -> timeout
259+
end,
260+
{ok, Fd} = atomvm:posix_open(Path, [o_rdwr, o_creat], 8#644),
261+
ok =
262+
try
263+
atomvm:posix_select_write(Fd, GonePid, undefined),
264+
fail
265+
catch
266+
error:badarg -> ok
267+
end,
268+
ok = atomvm:posix_close(Fd),
269+
ok.

0 commit comments

Comments
 (0)