Skip to content

Commit 23993c1

Browse files
committed
erts: Allow erlang:load_nif/2 to load a NIF from an in-memory binary
Add support for passing `#{memory => NifSO::binary(), label => string():binary()}` as the first argument to erlang:load_nif/2. When a tuple is given, the NIF shared object is loaded directly from the binary image in memory instead of from the file system. This addition is needed so that escripts can package SO dependencies inside and be distributed as a single executable. Implementation -------------- * erts_dlopen_mem() — new static helper in erl_nif.c (Linux/POSIX only). On Linux >= 3.17 it uses memfd_create(2) to create an anonymous in-memory file, writes the SO image into it, and calls dlopen(3) via /proc/self/fd/<fd>. On older Linux kernels and other POSIX systems it falls back to shm_open(3) + /dev/shm/<name>. The implementation is derived from the https://github.com/saleyn/memfd_create proof of concept. * erts_load_nif() — extended argument parsing: - Plain filename() continues to work unchanged. - {Filename, Binary} tuple: Binary is extracted via erts_get_aligned_binary_bytes(), passed to erts_dlopen_mem(), and the resulting dlopen handle is used directly. The Filename string serves only as a label inside the memory-backed file descriptor. - The open-from-memory step is performed before the existing else-if validation chain (version checks, module-name check, create_lib) so both code paths share the same validation and bookkeeping. - erts_sys_ddll_open() is skipped (via !load_from_mem guard) when a handle was already obtained from memory. * The feature is guarded by #if defined(HAVE_DLOPEN) && defined(__unix__). On unsupported platforms load_nif/2 returns {error,{load_failed,…}}. Testing ------- * nif_SUITE: new test case load_nif_from_mem - Happy path: reads nif_mod.1.so into a binary, calls erlang:load_nif({Path, Binary}, []) via nif_mod:load_nif_lib_from_mem/3, and asserts lib_version() == 1. - Error path: non-binary second element returns {error,{bad_lib,_}}. * nif_mod.erl: new exported helper load_nif_lib_from_mem/3 so that the load_nif call site lives inside the NIF module (required by the BIF).
1 parent 6a54781 commit 23993c1

5 files changed

Lines changed: 324 additions & 14 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ JAVADOC-GENERATED
213213
/erts/emulator/test/*_native_SUITE.erl
214214
/erts/emulator/test/*_SUITE_data/Makefile
215215
/erts/emulator/test/*_stripped_types_SUITE.erl
216+
/erts/emulator/test/nif_SUITE_data/*.so
216217
/erts/test/install_SUITE_data/install_bin
217218
/erts/test/autoimport_SUITE_data/erlang.xml
218219
/erts/emulator/make_test_dir/

erts/emulator/beam/erl_nif.c

Lines changed: 214 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
#endif
4545

4646
#include "erl_nif.h"
47-
4847
#include "sys.h"
4948
#include "global.h"
5049
#include "erl_binary.h"
@@ -74,6 +73,144 @@
7473
#include <limits.h>
7574
#include <stddef.h> /* offsetof */
7675

76+
#if defined(HAVE_DLOPEN) && (defined(__unix__) || defined(__APPLE__) || defined(__MACH__) || defined(__DARWIN__))
77+
# define USE_ERTS_DLOPEN_MEM
78+
#endif
79+
80+
#if defined(USE_ERTS_DLOPEN_MEM)
81+
# include <dlfcn.h>
82+
# include <fcntl.h>
83+
# include <sys/mman.h>
84+
# include <sys/stat.h>
85+
# include <sys/time.h>
86+
# include <unistd.h>
87+
# if defined(__linux__)
88+
# include <sys/syscall.h>
89+
# include <sys/utsname.h>
90+
# endif /* __linux__ */
91+
92+
#if defined(__linux__)
93+
static int erts_memfd_create(const char *name, unsigned int flags) {
94+
return (int)syscall(SYS_memfd_create, name, flags);
95+
}
96+
#endif
97+
98+
#if defined(__linux__) || defined(__unix__)
99+
/* Returns kernel version XYY (e.g. 317 for 3.17), or -1 on error */
100+
static int erts_get_kernel_version(void) {
101+
struct utsname buf;
102+
int major, minor;
103+
if (uname(&buf) != 0)
104+
return -1;
105+
if (sscanf(buf.release, "%d.%d", &major, &minor) != 2)
106+
return -1;
107+
return major * 100 + minor;
108+
}
109+
#endif
110+
111+
/* Maximum sizes for the in-memory SO loader.
112+
*
113+
* ERTS_NIF_MEM_NAME_MAX: maximum length (excluding NUL) of the caller-supplied
114+
* library name when loading from memory. On older Linux kernels and other
115+
* POSIX systems the name is used as a POSIX shared-memory object name, so it
116+
* is capped at NAME_MAX (typically 255). A fallback of 255 is provided for
117+
* platforms that do not define NAME_MAX. */
118+
#ifndef NAME_MAX
119+
# define NAME_MAX 255
120+
#endif
121+
#define ERTS_NIF_MEM_NAME_MAX NAME_MAX
122+
123+
/* Always declare erts_dlopen_mem for all platforms */
124+
static void *erts_dlopen_mem(const char *filename, const void *mem, size_t size);
125+
126+
/*
127+
* Load a shared object from memory using memfd_create (Linux >= 3.17) or
128+
* shm_open (other POSIX systems). Returns a dlopen(3) handle on success,
129+
* or NULL on failure.
130+
*
131+
* filename - a label associated with the in-memory file (may be NULL)
132+
* mem - pointer to the SO image in memory
133+
* size - byte length of the SO image
134+
*/
135+
static void *erts_dlopen_mem(const char *filename, const void *mem, size_t size)
136+
{
137+
char path[PATH_MAX];
138+
char shm_name[NAME_MAX];
139+
int shm_fd = -1;
140+
void *handle = NULL;
141+
#if defined(__linux__)
142+
static int kernel_ver = 0;
143+
if (kernel_ver == 0)
144+
kernel_ver = erts_get_kernel_version();
145+
if (kernel_ver < 0)
146+
return NULL;
147+
#endif
148+
149+
path[0] = '\0';
150+
shm_name[0] = '\0';
151+
152+
if (!filename
153+
#if defined(__linux__)
154+
|| kernel_ver < 317
155+
#endif
156+
) {
157+
#if (defined(__APPLE__) && defined(__MACH__)) || defined(__DARWIN__)
158+
filename = SHM_ANNON; /* Only use annonymous files on macOS for ease of cleanup */
159+
#else
160+
/* Generate a unique name for the shared memory object. */
161+
struct timeval tv;
162+
gettimeofday(&tv, NULL);
163+
enif_snprintf(shm_name, sizeof(shm_name), "erl-nif-mem-%ld.so",
164+
1000000L * (long)tv.tv_sec + (long)tv.tv_usec);
165+
filename = shm_name;
166+
#endif
167+
} else {
168+
#if (defined(__APPLE__) && defined(__MACH__)) || defined(__DARWIN__)
169+
/* Only use annonymous files on macOS for ease of cleanup */
170+
return NULL
171+
#else
172+
/* Allow room for "/dev/shm/" prefix and null terminator */
173+
if (sys_strlen(filename) > PATH_MAX - 9 - 1)
174+
return NULL;
175+
#endif
176+
}
177+
178+
#if defined(__linux__)
179+
if (kernel_ver >= 317) {
180+
shm_fd = erts_memfd_create(filename, 1 /*MFD_CLOEXEC*/);
181+
if (shm_fd < 0)
182+
goto err;
183+
enif_snprintf(path, sizeof(path), "/proc/self/fd/%d", shm_fd);
184+
} else {
185+
#endif
186+
shm_fd = shm_open(filename, O_RDWR | O_CREAT, S_IRWXU);
187+
if (shm_fd < 0)
188+
goto err;
189+
#if (defined(__APPLE__) && defined(__MACH__)) || defined(__DARWIN__)
190+
/* Get the path to the annonymous file descriptor so that we can
191+
use it in dlopen() */
192+
if (fcntl(shm_fd, F_GETPATH, path) < 0)
193+
goto err;
194+
#else
195+
enif_snprintf(path, sizeof(path), "/dev/shm/%s", filename);
196+
#endif
197+
#if defined(__linux__)
198+
}
199+
#endif
200+
if (ftruncate(shm_fd, size) || write(shm_fd, mem, size) != (ssize_t)size)
201+
goto err;
202+
203+
handle = dlopen(path, RTLD_NOW | RTLD_LOCAL);
204+
205+
err:
206+
if (shm_fd >= 0)
207+
close(shm_fd);
208+
if (path[0] != '\0') unlink(path);
209+
if (shm_name[0] != '\0') shm_unlink(shm_name);
210+
return handle;
211+
}
212+
#endif /* USE_ERTS_DLOPEN_MEM */
213+
77214
#define ERTS_NIF_HALT_INFO_FLAG_BLOCK (1 << 0)
78215
#define ERTS_NIF_HALT_INFO_FLAG_HALTING (1 << 1)
79216
#define ERTS_NIF_HALT_INFO_FLAG_WAITING (1 << 2)
@@ -4721,6 +4858,8 @@ Eterm erts_load_nif(Process *c_p, ErtsCodePtr I, Eterm filename, Eterm args)
47214858
Eterm ret = am_ok;
47224859
int veto;
47234860
int is_static = 0;
4861+
int load_from_mem = 0; /* non-zero when loading from in-memory binary */
4862+
Eterm nif_binary = THE_NON_VALUE;
47244863
struct erl_module_nif* lib = NULL;
47254864
struct erl_module_instance* this_mi;
47264865
struct erl_module_instance* prev_mi;
@@ -4731,12 +4870,41 @@ Eterm erts_load_nif(Process *c_p, ErtsCodePtr I, Eterm filename, Eterm args)
47314870
/* since lib_name is used in error messages */
47324871
encoding = ERL_FILENAME_UTF8;
47334872
}
4873+
4874+
/*
4875+
* Accept either:
4876+
* Filename::string()|binary() -- load from file (existing behaviour)
4877+
* #{memory := NifSO} -- load from memory, auto-generated label
4878+
* #{memory := NifSO, label := Filename} -- load from memory with explicit label
4879+
*/
4880+
if (is_map(filename)) {
4881+
const Eterm *mem_val = erts_maps_get(am_memory, filename);
4882+
const Eterm *lbl_val = erts_maps_get(am_label, filename);
4883+
if (mem_val == NULL) {
4884+
return load_nif_error(c_p, "bad_lib",
4885+
"load_nif/2: map argument must contain a 'memory' key");
4886+
}
4887+
if (!is_bitstring(*mem_val) || TAIL_BITS(bitstring_size(*mem_val)) != 0) {
4888+
return load_nif_error(c_p, "bad_lib",
4889+
"load_nif/2: 'memory' value must be a binary");
4890+
}
4891+
nif_binary = *mem_val;
4892+
load_from_mem = 1;
4893+
if (lbl_val != NULL) {
4894+
filename = *lbl_val; /* fall through to filename decoding for label */
4895+
} else {
4896+
lib_name = NULL; /* anonymous: erts_dlopen_mem will generate a name */
4897+
goto after_lib_name;
4898+
}
4899+
}
4900+
47344901
lib_name = erts_convert_filename_to_encoding(filename, NULL, 0,
47354902
ERTS_ALC_T_TMP, 1, 0, encoding,
4736-
NULL, 0);
4903+
NULL, 0);
47374904
if (!lib_name) {
47384905
return THE_NON_VALUE;
47394906
}
4907+
after_lib_name:;
47404908

47414909
/* Find calling module */
47424910
caller = erts_find_function_from_pc(I);
@@ -4769,11 +4937,52 @@ Eterm erts_load_nif(Process *c_p, ErtsCodePtr I, Eterm filename, Eterm args)
47694937
this_mi = module_p->on_load;
47704938
}
47714939

4940+
/* If the caller passed #{memory => NifSO::binary(), label => Filename},
4941+
* open the shared object from memory now so that `handle` is ready for
4942+
* the common else-if chain below (which runs
4943+
* erts_sys_ddll_load_nif_init / call_nif_init, version checks, and
4944+
* create_lib for both file and memory loads). */
4945+
if (!is_static && load_from_mem) {
4946+
#if defined(USE_ERTS_DLOPEN_MEM)
4947+
const byte *bin_bytes = NULL;
4948+
Uint bin_size = 0;
4949+
const byte *tmp_alloc = NULL;
4950+
static const int max_path_len = PATH_MAX - 9; /* space for "/dev/shm/" */
4951+
if (lib_name != NULL) {
4952+
int lib_name_len = sys_strlen(lib_name);
4953+
if (lib_name_len > max_path_len - 1) { /* -1 for null terminator added */
4954+
ret = load_nif_error(c_p, "load_failed",
4955+
"NIF library path too long: %d (max %d bytes)",
4956+
lib_name_len, max_path_len);
4957+
goto error;
4958+
}
4959+
}
4960+
bin_bytes = erts_get_aligned_binary_bytes(nif_binary, &bin_size, &tmp_alloc);
4961+
if (!bin_bytes) {
4962+
ret = load_nif_error(c_p, "load_failed",
4963+
"Failed to access NIF binary data");
4964+
goto error;
4965+
}
4966+
handle = erts_dlopen_mem(lib_name, bin_bytes, (size_t)bin_size);
4967+
erts_free_aligned_binary_bytes(tmp_alloc);
4968+
if (!handle) {
4969+
ret = load_nif_error(c_p, "load_failed",
4970+
"Failed to load NIF library from memory: '%s'",
4971+
dlerror());
4972+
goto error;
4973+
}
4974+
#else
4975+
ret = load_nif_error(c_p, "load_failed",
4976+
"Loading NIF from memory is not supported on this platform");
4977+
goto error;
4978+
#endif
4979+
}
4980+
47724981
if (this_mi->nif != NULL) {
47734982
ret = load_nif_error(c_p,"reload","NIF library already loaded"
47744983
" (reload disallowed since OTP 20).");
47754984
}
4776-
else if (!is_static &&
4985+
else if (!is_static && !load_from_mem &&
47774986
(err=erts_sys_ddll_open(lib_name, &handle, &errdesc)) != ERL_DE_NO_ERROR) {
47784987
const char slogan[] = "Failed to load NIF library";
47794988
if (strstr(errdesc.str, lib_name) != NULL) {
@@ -5031,7 +5240,8 @@ Eterm erts_load_nif(Process *c_p, ErtsCodePtr I, Eterm filename, Eterm args)
50315240
erts_sys_ddll_free_error(&errdesc);
50325241
}
50335242

5034-
erts_free(ERTS_ALC_T_TMP, lib_name);
5243+
if (lib_name)
5244+
erts_free(ERTS_ALC_T_TMP, lib_name);
50355245

50365246
BIF_RET(ret);
50375247
}

erts/emulator/test/nif_SUITE.erl

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
init_per_group/2, end_per_group/2,
3535
init_per_testcase/2, end_per_testcase/2,
3636
basic/1, reload_error/1, upgrade/1, heap_frag/1,
37+
load_nif_from_mem/1,
3738
t_on_load/1,
3839
t_nifs_attrib/1,
3940
t_load_race/1,
@@ -220,7 +221,8 @@ suite() -> [{ct_hooks,[ts_install_cth]}].
220221

221222
all() ->
222223
[basic,
223-
non_exported_nif]
224+
non_exported_nif,
225+
load_nif_from_mem]
224226
++
225227
[{group, G} || G <- api_groups()]
226228
++
@@ -357,6 +359,65 @@ non_exported_nif(Config) when is_list(Config) ->
357359
false = lists:member({lib_version,0}, ?MODULE:module_info(exports)),
358360
ok.
359361

362+
%% Test loading a NIF library from an in-memory binary image via
363+
%% erlang:load_nif({Filename, Binary}, LoadInfo).
364+
load_nif_from_mem(Config) when is_list(Config) ->
365+
case os:type() of
366+
{unix, _} -> run_load_nif_from_mem(Config);
367+
_ -> {skip, "load_nif from memory is only supported on Unix"}
368+
end.
369+
370+
run_load_nif_from_mem(Config) ->
371+
372+
Data = proplists:get_value(data_dir, Config),
373+
ModFile = filename:join(Data, "nif_mod"),
374+
{ok, nif_mod, ModBin} = compile:file(ModFile, [binary, return_errors]),
375+
{module, nif_mod} = erlang:load_module(nif_mod, ModBin),
376+
377+
%% Derive the .so path the same way nif_mod:load_nif_lib/2 does,
378+
%% but with the .so extension appended explicitly for file:read_file/1.
379+
SoPath = filename:join(Data, "nif_mod.1.so"),
380+
381+
%% --- happy path: map with label (not supported on macOS) ---
382+
{ok, SoBin} = file:read_file(SoPath),
383+
case os:type() of
384+
{unix, darwin} ->
385+
{error, {load_failed, _}} = nif_mod:load_nif_lib_from_mem(Config, 1, SoBin);
386+
_ ->
387+
ok = nif_mod:load_nif_lib_from_mem(Config, 1, SoBin),
388+
1 = nif_mod:lib_version()
389+
end,
390+
391+
%% cleanup: delete + purge so the module can be reloaded for error tests
392+
true = erlang:delete_module(nif_mod),
393+
true = erlang:purge_module(nif_mod),
394+
receive unloaded -> ok after 1000 -> ok end,
395+
396+
%% --- error: 'memory' value is not a binary ---
397+
{module, nif_mod} = erlang:load_module(nif_mod, ModBin),
398+
{error, {bad_lib, _}} = nif_mod:load_nif_lib_from_mem(Config, 1, not_a_binary),
399+
true = erlang:delete_module(nif_mod),
400+
true = erlang:purge_module(nif_mod),
401+
receive unloaded -> ok after 1000 -> ok end,
402+
403+
%% --- error: label longer than PATH_MAX-9 ---
404+
{module, nif_mod} = erlang:load_module(nif_mod, ModBin),
405+
LongPath = lists:duplicate(4096-9, $x),
406+
{error, {load_failed, _}} = nif_mod:load_nif_mem_path(LongPath, SoBin),
407+
true = erlang:delete_module(nif_mod),
408+
true = erlang:purge_module(nif_mod),
409+
receive unloaded -> ok after 1000 -> ok end,
410+
411+
%% --- anonymous load: map with only 'memory' key ---
412+
{module, nif_mod} = erlang:load_module(nif_mod, ModBin),
413+
ok = nif_mod:load_nif_mem_anon(SoBin),
414+
1 = nif_mod:lib_version(),
415+
true = erlang:delete_module(nif_mod),
416+
true = erlang:purge_module(nif_mod),
417+
receive unloaded -> ok after 1000 -> ok end,
418+
419+
ok.
420+
360421
%% Test old reload feature now always fails
361422
reload_error(Config) when is_list(Config) ->
362423
TmpMem = tmpmem(),

erts/emulator/test/nif_SUITE_data/nif_mod.erl

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424

2525
-include_lib("common_test/include/ct.hrl").
2626

27-
-export([load_nif_lib/2, load_nif_lib/3, start/0,
27+
-export([load_nif_lib/2, load_nif_lib/3, load_nif_lib_from_mem/3,
28+
load_nif_mem_path/2, load_nif_mem_anon/1, start/0,
2829
lib_version/0, lib_version_check/0, trace_me/1,
2930
get_priv_data_ptr/0, make_new_resource/2, get_resource/2,
3031
monitor_process/3]).
@@ -72,6 +73,23 @@ load_nif_lib(Config, Ver, LoadInfo) ->
7273
R = erlang:load_nif(filename:join(Path,libname(Ver,API)), LoadInfo),
7374
check_api_version(R, API).
7475

76+
%% Load the NIF library from an in-memory binary image.
77+
%% SoBin must be the contents of the .so file.
78+
load_nif_lib_from_mem(Config, Ver, SoArg) ->
79+
Path = proplists:get_value(data_dir, Config),
80+
API = proplists:get_value(nif_api_version, Config, ""),
81+
LibName = filename:join(Path, libname(Ver, API)),
82+
R = erlang:load_nif(#{memory => SoArg, label => LibName}, []),
83+
check_api_version(R, API).
84+
85+
%% Load from memory with an explicit label (string or binary).
86+
load_nif_mem_path(Path, SoBin) ->
87+
erlang:load_nif(#{memory => SoBin, label => Path}, []).
88+
89+
%% Load from memory with no label (auto-generated shm name).
90+
load_nif_mem_anon(SoBin) ->
91+
erlang:load_nif(#{memory => SoBin}, []).
92+
7593
libname(no_init,API) -> libname(3,API);
7694
libname(Ver,API) when is_integer(Ver) ->
7795
"nif_mod." ++ integer_to_list(Ver) ++ API.

0 commit comments

Comments
 (0)