Skip to content

Commit bec1ec1

Browse files
authored
Add monitor to posix_select
Monitor selecting process to avoid leaks. Add tests Signed-off-by: Paul Guyot <pguyot@kallisys.net>
1 parent 7c07c96 commit bec1ec1

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)