Skip to content

Commit 059819e

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 059819e

5 files changed

Lines changed: 320 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: 210 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@
4444
#endif
4545

4646
#include "erl_nif.h"
47+
#if (defined(__APPLE) || defined(__MACH__) || defined(__DARWIN__))
48+
#include <dlfcn.h>
49+
#endif
4750

4851
#include "sys.h"
4952
#include "global.h"
@@ -74,6 +77,138 @@
7477
#include <limits.h>
7578
#include <stddef.h> /* offsetof */
7679

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

47414907
/* Find calling module */
47424908
caller = erts_find_function_from_pc(I);
@@ -4769,11 +4935,51 @@ Eterm erts_load_nif(Process *c_p, ErtsCodePtr I, Eterm filename, Eterm args)
47694935
this_mi = module_p->on_load;
47704936
}
47714937

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

5034-
erts_free(ERTS_ALC_T_TMP, lib_name);
5240+
if (lib_name)
5241+
erts_free(ERTS_ALC_T_TMP, lib_name);
50355242

50365243
BIF_RET(ret);
50375244
}

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)