Skip to content

Commit

Permalink
Add atomvm:subprocess/4 performing pipe/fork/execve`
Browse files Browse the repository at this point in the history
This is implemented using pipe/fork/execve or posix_spawn depending on
platforms and availability of `HAVE_POSIX_SPAWN_CLOEXEC_DEFAULT` attribute.
The VM does not leak any file descriptor to the subprocess except for the
pipe used to read stdout.

The function is explicitely disabled on microcontrollers and is meant to be
used on generic_unix platform, for example for tests.

Signed-off-by: Paul Guyot <[email protected]>
  • Loading branch information
pguyot committed Dec 27, 2024
1 parent bd67862 commit 1c7ee2b
Show file tree
Hide file tree
Showing 12 changed files with 272 additions and 44 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added support for external pids and encoded pids in external terms
- Added support for external refs and encoded refs in external terms
- Introduce ports to represent native processes and added support for external ports and encoded ports in external terms
- Added `atomvm:subprocess/4` to perform pipe/fork/execve on POSIX platforms

## [0.6.6] - Unreleased

Expand Down
19 changes: 18 additions & 1 deletion libs/eavmlib/src/atomvm.erl
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
posix_clock_settime/2,
posix_opendir/1,
posix_closedir/1,
posix_readdir/1
posix_readdir/1,
subprocess/4
]).

-export_type([
Expand Down Expand Up @@ -335,3 +336,19 @@ posix_closedir(_Dir) ->
{ok, {dirent, Inode :: integer(), Name :: binary()}} | eof | {error, posix_error()}.
posix_readdir(_Dir) ->
erlang:nif_error(undefined).

%%-----------------------------------------------------------------------------
%% @param Path path to the command to execute
%% @param Args arguments to pass to the command. First item is the name
%% of the command
%% @param Envp environment variables to pass to the command.
%% @param Options options to run execve. Should be `[stdout]'
%% @returns a tuple with the process id and a fd to the stdout of the process.
%% @doc Fork and execute a program using fork(2) and execve(2). Pipe stdout
%% so output of the program can be read with `atomvm:posix_read/2'.
%% @end
%%-----------------------------------------------------------------------------
-spec subprocess(Path :: iodata(), Args :: [iodata()], Env :: [iodata()], Options :: [stdout]) ->
{ok, non_neg_integer(), posix_fd()} | {error, posix_error()}.
subprocess(_Path, _Args, _Env, _Options) ->
erlang:nif_error(undefined).
3 changes: 3 additions & 0 deletions src/libAtomVM/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ define_if_function_exists(libAtomVM closedir "dirent.h" PUBLIC HAVE_CLOSEDIR)
define_if_function_exists(libAtomVM mkfifo "sys/stat.h" PRIVATE HAVE_MKFIFO)
define_if_function_exists(libAtomVM readdir "dirent.h" PUBLIC HAVE_READDIR)
define_if_function_exists(libAtomVM unlink "unistd.h" PRIVATE HAVE_UNLINK)
define_if_function_exists(libAtomVM execve "unistd.h" PRIVATE HAVE_EXECVE)
define_if_function_exists(libAtomVM closefrom "unistd.h" PRIVATE HAVE_CLOSEFROM)
define_if_symbol_exists(libAtomVM POSIX_SPAWN_CLOEXEC_DEFAULT "spawn.h" PRIVATE HAVE_POSIX_SPAWN_CLOEXEC_DEFAULT)
define_if_symbol_exists(libAtomVM O_CLOEXEC "fcntl.h" PRIVATE HAVE_O_CLOEXEC)
define_if_symbol_exists(libAtomVM O_DIRECTORY "fcntl.h" PRIVATE HAVE_O_DIRECTORY)
define_if_symbol_exists(libAtomVM O_DSYNC "fcntl.h" PRIVATE HAVE_O_DSYNC)
Expand Down
6 changes: 6 additions & 0 deletions src/libAtomVM/nifs.c
Original file line number Diff line number Diff line change
Expand Up @@ -845,8 +845,14 @@ DEFINE_MATH_NIF(tanh)
//Handle optional nifs
#if HAVE_OPEN && HAVE_CLOSE
#define IF_HAVE_OPEN_CLOSE(expr) (expr)
#if HAVE_EXECVE
#define IF_HAVE_EXECVE(expr) (expr)
#else
#define IF_HAVE_EXECVE(expr) NULL
#endif
#else
#define IF_HAVE_OPEN_CLOSE(expr) NULL
#define IF_HAVE_EXECVE(expr) NULL
#endif
#if HAVE_MKFIFO
#define IF_HAVE_MKFIFO(expr) (expr)
Expand Down
1 change: 1 addition & 0 deletions src/libAtomVM/nifs.gperf
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ atomvm:posix_write/2, IF_HAVE_OPEN_CLOSE(&atomvm_posix_write_nif)
atomvm:posix_select_read/3, IF_HAVE_OPEN_CLOSE(&atomvm_posix_select_read_nif)
atomvm:posix_select_write/3, IF_HAVE_OPEN_CLOSE(&atomvm_posix_select_write_nif)
atomvm:posix_select_stop/1, IF_HAVE_OPEN_CLOSE(&atomvm_posix_select_stop_nif)
atomvm:subprocess/4, IF_HAVE_EXECVE(&atomvm_subprocess_nif)
atomvm:posix_mkfifo/2, IF_HAVE_MKFIFO(&atomvm_posix_mkfifo_nif)
atomvm:posix_unlink/1, IF_HAVE_UNLINK(&atomvm_posix_unlink_nif)
atomvm:posix_clock_settime/2, IF_HAVE_CLOCK_SETTIME_OR_SETTIMEOFDAY(&atomvm_posix_clock_settime_nif)
Expand Down
245 changes: 202 additions & 43 deletions src/libAtomVM/posix_nifs.c
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@
#include <dirent.h>
#endif

#if HAVE_POSIX_SPAWN_CLOEXEC_DEFAULT
#include <spawn.h>
#endif

#include "defaultatoms.h"
#include "erl_nif_priv.h"
#include "globalcontext.h"
Expand Down Expand Up @@ -130,6 +134,24 @@ term posix_errno_to_term(int err, GlobalContext *glb)
return term_from_int(err);
}

static term error_tuple_maybe_gc(int err, Context *ctx)
{
if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) {
RAISE_ERROR(OUT_OF_MEMORY_ATOM);
}

term result = term_alloc_tuple(2, &ctx->heap);
term_put_tuple_element(result, 0, ERROR_ATOM);
term_put_tuple_element(result, 1, posix_errno_to_term(err, ctx->global));

return result;
}

static term errno_to_error_tuple_maybe_gc(Context *ctx)
{
return error_tuple_maybe_gc(errno, ctx);
}

#if HAVE_OPEN && HAVE_CLOSE
#define CLOSED_FD (-1)

Expand Down Expand Up @@ -340,12 +362,7 @@ static term nif_atomvm_posix_close(Context *ctx, int argc, term argv[])
}
if (UNLIKELY(close(fd_obj->fd) < 0)) {
fd_obj->fd = CLOSED_FD; // even if bad things happen, do not close twice.
if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) {
RAISE_ERROR(OUT_OF_MEMORY_ATOM);
}
result = term_alloc_tuple(2, &ctx->heap);
term_put_tuple_element(result, 0, ERROR_ATOM);
term_put_tuple_element(result, 1, posix_errno_to_term(errno, ctx->global));
return errno_to_error_tuple_maybe_gc(ctx);
}
fd_obj->fd = CLOSED_FD;
}
Expand Down Expand Up @@ -373,13 +390,7 @@ static term nif_atomvm_posix_read(Context *ctx, int argc, term argv[])
int res = read(fd_obj->fd, (void *) term_binary_data(bin_term), count);
if (UNLIKELY(res < 0)) {
// Return an error.
if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) {
RAISE_ERROR(OUT_OF_MEMORY_ATOM);
}
term ret = term_alloc_tuple(2, &ctx->heap);
term_put_tuple_element(ret, 0, ERROR_ATOM);
term_put_tuple_element(ret, 1, posix_errno_to_term(errno, glb));
return ret;
return errno_to_error_tuple_maybe_gc(ctx);
}
if (res == 0) {
return globalcontext_make_atom(glb, ATOM_STR("\x3", "eof"));
Expand Down Expand Up @@ -488,6 +499,175 @@ static term nif_atomvm_posix_select_stop(Context *ctx, int argc, term argv[])

return OK_ATOM;
}

#if HAVE_EXECVE
static char **parse_string_list(term list)
{
if (!term_is_list(list)) {
return NULL;
}
int ok;
size_t result_len = term_list_length(list, &ok);
if (UNLIKELY(!ok)) {
return NULL;
}
// All items are initialized to NULL.
char **result_list = calloc(result_len + 1, sizeof(char *));
if (IS_NULL_PTR(result_list)) {
return NULL;
}
term list_item = list;
int i = 0;
while (term_is_nonempty_list(list_item)) {
term item = term_get_list_head(list_item);
char *str = interop_term_to_string(item, &ok);
if (UNLIKELY(!ok)) {
for (int j = 0; j < i; j++) {
free(result_list[j]);
}
free(result_list);
return NULL;
}
result_list[i++] = str;
list_item = term_get_list_tail(list_item);
if (str == NULL) {
// NULL pointer marks the end of the list, so we need to break
// here.
break;
}
}
return result_list;
}

static void free_string_list(char **list)
{
char **ptr = list;
while (*ptr) {
char *str = *ptr;
free(str);
ptr++;
}
free(list);
}

static term nif_atomvm_subprocess(Context *ctx, int argc, term argv[])
{
UNUSED(argc);

int ok;
char *path = interop_term_to_string(argv[0], &ok);
if (UNLIKELY(!ok)) {
RAISE_ERROR(BADARG_ATOM);
}
char **args = parse_string_list(argv[1]);
if (IS_NULL_PTR(args)) {
free(path);
RAISE_ERROR(BADARG_ATOM);
}
char **envp = parse_string_list(argv[2]);
if (IS_NULL_PTR(envp)) {
free(path);
free_string_list(args);
RAISE_ERROR(BADARG_ATOM);
}

int pstdout[2];
int r = pipe(pstdout);
if (r < 0) {
free(path);
free_string_list(args);
free_string_list(envp);
return errno_to_error_tuple_maybe_gc(ctx);
}
pid_t pid;
#if HAVE_POSIX_SPAWN_CLOEXEC_DEFAULT
do {
posix_spawn_file_actions_t file_actions;
posix_spawnattr_t spawn_attrs;
if (UNLIKELY((r = posix_spawn_file_actions_init(&file_actions)) != 0)) {
break;
}
if (UNLIKELY((r = posix_spawn_file_actions_adddup2(&file_actions, pstdout[1], 1)) != 0)) {
break;
}
if (UNLIKELY((r = posix_spawnattr_init(&spawn_attrs)) != 0)) {
break;
}
if (UNLIKELY((r = posix_spawnattr_setflags(&spawn_attrs, HAVE_POSIX_SPAWN_CLOEXEC_DEFAULT)) != 0)) {
break;
}
if (UNLIKELY((r = posix_spawn(&pid, path, &file_actions, &spawn_attrs, args, envp)) != 0)) {
break;
}
if (UNLIKELY((r = posix_spawnattr_destroy(&spawn_attrs)) != 0)) {
break;
}
if (UNLIKELY((r = posix_spawn_file_actions_destroy(&file_actions)) != 0)) {
break;
}
} while (false);
if (UNLIKELY(r != 0)) {
free(path);
free_string_list(args);
free_string_list(envp);
close(pstdout[0]);
close(pstdout[1]);
return error_tuple_maybe_gc(r, ctx);
}
#else
r = fork();
if (r < 0) {
int err = errno;
free(path);
free_string_list(args);
free_string_list(envp);
close(pstdout[0]);
close(pstdout[1]);
return error_tuple_maybe_gc(err, ctx);
}
if (r == 0) {
// child.
close(0); // close stdin of the child
close(pstdout[0]); // close read end of the pipe
dup2(pstdout[1], 1); // make stdout the write-end of the pipe
#if HAVE_CLOSEFROM
closefrom(2);
#else
int maxfd = sysconf(_SC_OPEN_MAX);
for(int fd=3; fd < maxfd; fd++)
close(fd);
#endif
execve(path, args, envp);
exit(1);
}
pid = r;
#endif
// parent
close(pstdout[1]); // close write-end of the pipe
free(path);
free_string_list(args);
free_string_list(envp);

// Return a resource object
struct PosixFd *stdout_obj = enif_alloc_resource(ctx->global->posix_fd_resource_type, sizeof(struct PosixFd));
if (IS_NULL_PTR(stdout_obj)) {
RAISE_ERROR(OUT_OF_MEMORY_ATOM);
}
stdout_obj->fd = pstdout[0];
stdout_obj->selecting_process_id = INVALID_PROCESS_ID;

if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(3) + TERM_BOXED_RESOURCE_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) {
RAISE_ERROR(OUT_OF_MEMORY_ATOM);
}
term stdout_term = term_from_resource(stdout_obj, &ctx->heap);
term result = term_alloc_tuple(3, &ctx->heap);
term_put_tuple_element(result, 0, OK_ATOM);
term_put_tuple_element(result, 1, term_from_int(pid));
term_put_tuple_element(result, 2, stdout_term);

return result;
}
#endif
#endif

#if HAVE_MKFIFO
Expand All @@ -506,23 +686,15 @@ static term nif_atomvm_posix_mkfifo(Context *ctx, int argc, term argv[])

int mode = term_to_int(mode_term);

term result;
int res = mkfifo(path, mode);
free((void *) path);

if (res < 0) {
// Return an error.
if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) {
RAISE_ERROR(OUT_OF_MEMORY_ATOM);
}
result = term_alloc_tuple(2, &ctx->heap);
term_put_tuple_element(result, 0, ERROR_ATOM);
term_put_tuple_element(result, 1, posix_errno_to_term(errno, ctx->global));
} else {
result = OK_ATOM;
return errno_to_error_tuple_maybe_gc(ctx);
}

return result;
return OK_ATOM;
}
#endif

Expand All @@ -542,13 +714,7 @@ static term nif_atomvm_posix_unlink(Context *ctx, int argc, term argv[])
free((void *) path);
if (res < 0) {
// Return an error.
if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) {
RAISE_ERROR(OUT_OF_MEMORY_ATOM);
}
term result = term_alloc_tuple(2, &ctx->heap);
term_put_tuple_element(result, 0, ERROR_ATOM);
term_put_tuple_element(result, 1, posix_errno_to_term(errno, ctx->global));
return result;
return errno_to_error_tuple_maybe_gc(ctx);
}
return OK_ATOM;
}
Expand Down Expand Up @@ -629,19 +795,6 @@ const ErlNifResourceTypeInit posix_dir_resource_type_init = {
.dtor = posix_dir_dtor
};

static term errno_to_error_tuple_maybe_gc(Context *ctx)
{
if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) {
RAISE_ERROR(OUT_OF_MEMORY_ATOM);
}

term result = term_alloc_tuple(2, &ctx->heap);
term_put_tuple_element(result, 0, ERROR_ATOM);
term_put_tuple_element(result, 1, posix_errno_to_term(errno, ctx->global));

return result;
}

static term nif_atomvm_posix_opendir(Context *ctx, int argc, term argv[])
{
UNUSED(argc);
Expand Down Expand Up @@ -802,6 +955,12 @@ const struct Nif atomvm_posix_select_stop_nif = {
.base.type = NIFFunctionType,
.nif_ptr = nif_atomvm_posix_select_stop
};
#if HAVE_EXECVE
const struct Nif atomvm_subprocess_nif = {
.base.type = NIFFunctionType,
.nif_ptr = nif_atomvm_subprocess
};
#endif
#endif
#if HAVE_MKFIFO
const struct Nif atomvm_posix_mkfifo_nif = {
Expand Down
3 changes: 3 additions & 0 deletions src/libAtomVM/posix_nifs.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ extern const struct Nif atomvm_posix_write_nif;
extern const struct Nif atomvm_posix_select_read_nif;
extern const struct Nif atomvm_posix_select_write_nif;
extern const struct Nif atomvm_posix_select_stop_nif;
#if HAVE_EXECVE
extern const struct Nif atomvm_subprocess_nif;
#endif
#endif
#if HAVE_MKFIFO
extern const struct Nif atomvm_posix_mkfifo_nif;
Expand Down
Loading

0 comments on commit 1c7ee2b

Please sign in to comment.