Skip to content

Commit 2ba4729

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 2ba4729

5 files changed

Lines changed: 312 additions & 13 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: 202 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,133 @@
7474
#include <limits.h>
7575
#include <stddef.h> /* offsetof */
7676

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

47414899
/* Find calling module */
47424900
caller = erts_find_function_from_pc(I);
@@ -4769,11 +4927,51 @@ Eterm erts_load_nif(Process *c_p, ErtsCodePtr I, Eterm filename, Eterm args)
47694927
this_mi = module_p->on_load;
47704928
}
47714929

4930+
/* If the caller passed {Filename, NifSO::binary()}, open the shared object
4931+
* from memory now so that `handle` is ready for the common else-if chain
4932+
* below (which runs erts_sys_ddll_load_nif_init / call_nif_init, version
4933+
* checks, and create_lib for both file and memory loads). */
4934+
if (!is_static && load_from_mem) {
4935+
#if defined(HAVE_DLOPEN) && (defined(__unix__) || defined(__APPLE__))
4936+
const byte *bin_bytes = NULL;
4937+
Uint bin_size = 0;
4938+
const byte *tmp_alloc = NULL;
4939+
static const int max_path_len = PATH_MAX - 9; /* space for "/dev/shm/" */
4940+
if (lib_name != NULL) {
4941+
int lib_name_len = sys_strlen(lib_name);
4942+
if (lib_name_len > max_path_len - 1) { /* -1 for null terminator added */
4943+
ret = load_nif_error(c_p, "load_failed",
4944+
"NIF library path too long: %d (max %d bytes)",
4945+
lib_name_len, max_path_len);
4946+
goto error;
4947+
}
4948+
}
4949+
bin_bytes = erts_get_aligned_binary_bytes(nif_binary, &bin_size, &tmp_alloc);
4950+
if (!bin_bytes) {
4951+
ret = load_nif_error(c_p, "load_failed",
4952+
"Failed to access NIF binary data");
4953+
goto error;
4954+
}
4955+
handle = erts_dlopen_mem(lib_name, bin_bytes, (size_t)bin_size);
4956+
erts_free_aligned_binary_bytes(tmp_alloc);
4957+
if (!handle) {
4958+
ret = load_nif_error(c_p, "load_failed",
4959+
"Failed to load NIF library from memory: '%s'",
4960+
dlerror());
4961+
goto error;
4962+
}
4963+
#else
4964+
ret = load_nif_error(c_p, "load_failed",
4965+
"Loading NIF from memory is not supported on this platform");
4966+
goto error;
4967+
#endif
4968+
}
4969+
47724970
if (this_mi->nif != NULL) {
47734971
ret = load_nif_error(c_p,"reload","NIF library already loaded"
47744972
" (reload disallowed since OTP 20).");
47754973
}
4776-
else if (!is_static &&
4974+
else if (!is_static && !load_from_mem &&
47774975
(err=erts_sys_ddll_open(lib_name, &handle, &errdesc)) != ERL_DE_NO_ERROR) {
47784976
const char slogan[] = "Failed to load NIF library";
47794977
if (strstr(errdesc.str, lib_name) != NULL) {
@@ -5031,7 +5229,8 @@ Eterm erts_load_nif(Process *c_p, ErtsCodePtr I, Eterm filename, Eterm args)
50315229
erts_sys_ddll_free_error(&errdesc);
50325230
}
50335231

5034-
erts_free(ERTS_ALC_T_TMP, lib_name);
5232+
if (lib_name)
5233+
erts_free(ERTS_ALC_T_TMP, lib_name);
50355234

50365235
BIF_RET(ret);
50375236
}

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)