Skip to content

Commit 8d459f0

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. - #{memory => Binary, label => Filename} map: 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 8d459f0

5 files changed

Lines changed: 382 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: 224 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,154 @@
7473
#include <limits.h>
7574
#include <stddef.h> /* offsetof */
7675

76+
#undef SHMOPEN_USE_LINUX_MEMFD
77+
#undef SHMOPEN_USE_SHM_ANON
78+
#undef SHMOPEN_USE_SHM_MKSTEMP
79+
#undef SHMOPEN_USE_POSIX
80+
#undef USE_ERTS_DLOPEN_MEM
81+
82+
#ifdef HAVE_DLFCN_H
83+
#include <dlfcn.h>
84+
#endif
85+
#ifdef HAVE_FCNTL_H
86+
#include <fcntl.h>
87+
#endif
88+
#ifdef HAVE_SYS_MMAN_H
89+
#include <sys/mman.h>
90+
#endif
91+
#ifdef HAVE_SYS_STAT_H
92+
#include <sys/stat.h>
93+
#endif
94+
#ifdef HAVE_SYS_TIME_H
95+
#include <sys/time.h>
96+
#endif
97+
#ifdef HAVE_UNISTD_H
98+
#include <unistd.h>
99+
#endif
100+
#if defined(__linux__)
101+
# if defined(__linux__)
102+
# include <sys/syscall.h>
103+
# endif
104+
# include <sys/utsname.h>
105+
# ifdef SYS_memfd_create
106+
# define SHMOPEN_USE_LINUX_MEMFD
107+
# else
108+
# define SHMOPEN_USE_SHM_POSIX
109+
# endif
110+
# define USE_ERTS_DLOPEN_MEM
111+
#elif defined(__FreeBSD__)
112+
# define SHMOPEN_USE_SHM_ANON
113+
# define USE_ERTS_DLOPEN_MEM
114+
#elif defined(__OpenBSD__)
115+
# define SHMOPEN_USE_SHM_MKSTEMP
116+
# define USE_ERTS_DLOPEN_MEM
117+
#else
118+
# if defined(__APPLE__) || defined(__MACH__) || defined(__DARWIN__) || \
119+
defined(__NetBSD__) || defined(__DragonFly__) || defined(__HAIKU__) || \
120+
defined(__sun)
121+
# define SHMOPEN_USE_POSIX
122+
# define USE_ERTS_DLOPEN_MEM
123+
# endif
124+
#endif
125+
126+
#define ERTS_NIF_MEM_NAME_MAX 64
127+
#define ERTS_MIN_KERNEL_VSN 317
128+
129+
#ifdef USE_ERTS_DLOPEN_MEM
130+
/* Always declare erts_dlopen_mem for all platforms */
131+
static void *erts_dlopen_mem(const char *filename, const void *mem, size_t size);
132+
133+
static char* erts_shm_name(const char *pfx, const char *sfx, char* buf, size_t buf_len)
134+
{
135+
/* Generate a unique name for the shared memory object. */
136+
struct timeval tv;
137+
gettimeofday(&tv, NULL);
138+
enif_snprintf(buf, buf_len, "%s-%-06ld%s", pfx, (long)tv.tv_usec, sfx);
139+
return buf;
140+
}
141+
142+
/*
143+
* Load a shared object from memory using memfd_create (Linux >= 3.17) or
144+
* shm_open (other POSIX systems). Returns a dlopen(3) handle on success,
145+
* or NULL on failure.
146+
*
147+
* filename - a label associated with the in-memory file (may be NULL)
148+
* mem - pointer to the SO image in memory
149+
* size - byte length of the SO image
150+
*/
151+
static void *erts_dlopen_mem(const char *filename, const void *mem, size_t size)
152+
{
153+
char path[PATH_MAX];
154+
char shm_name[NAME_MAX];
155+
int shm_fd = -1;
156+
void *handle = NULL;
157+
#if defined(SHMOPEN_USE_SHM_MKSTEMP) || defined(SHMOPEN_USE_POSIX)
158+
int need_unlink = 0;
159+
#endif
160+
161+
path[0] = '\0';
162+
shm_name[0] = '\0';
163+
164+
#if defined(SHMOPEN_USE_LINUX_MEMFD)
165+
if (!filename)
166+
filename = erts_shm_name("erl-nif-mem-", ".so", shm_name, sizeof(shm_name));
167+
shm_fd = memfd_create(filename, MFD_CLOEXEC);
168+
if (shm_fd < 0)
169+
return NULL;
170+
enif_snprintf(path, sizeof(path), "/proc/self/fd/%d", shm_fd);
171+
172+
#elif defined(SHMOPEN_USE_SHM_ANON)
173+
if (filename)
174+
return NULL;
175+
shm_fd = shm_open(SHM_ANON, O_RDWR, 0);
176+
if (shm_fd < 0)
177+
return NULL;
178+
# if defined(__APPLE__)
179+
if (fcntl(shm_fd, F_GETPATH, path) < 0)
180+
goto err;
181+
# else
182+
enif_snprintf(path, sizeof(path), "/dev/fd/%d", shm_fd);
183+
# endif
184+
185+
#elif defined(SHMOPEN_USE_SHM_MKSTEMP)
186+
enif_snprintf(path, sizeof(path), "/tmp/%s-XXXXXX", filename ? filename : "enif-shm");
187+
shm_fd = shm_mkstemp(path);
188+
if (shm_fd < 0)
189+
return NULL;
190+
need_unlink = 1;
191+
192+
#else /* SHMOPEN_USE_POSIX */
193+
if (filename) {
194+
enif_snprintf(shm_name, sizeof(shm_name), "%s", filename);
195+
} else {
196+
erts_shm_name("enif-shm-", ".so", shm_name, sizeof(shm_name));
197+
}
198+
enif_snprintf(path, sizeof(path), "/dev/shm/%s", shm_name);
199+
shm_fd = shm_open(path, O_RDWR | O_CREAT | O_EXCL, 0600);
200+
if (shm_fd < 0)
201+
return NULL;
202+
need_unlink = 1;
203+
#endif
204+
205+
if (ftruncate(shm_fd, size) || write(shm_fd, mem, size) != (ssize_t)size)
206+
goto err;
207+
208+
handle = dlopen(path, RTLD_NOW | RTLD_LOCAL);
209+
210+
err:
211+
# if defined(SHMOPEN_USE_SHM_MKSTEMP)
212+
if (need_unlink)
213+
unlink(path);
214+
# elif defined(SHMOPEN_USE_POSIX)
215+
if (need_unlink)
216+
shm_unlink(path);
217+
# endif
218+
if (shm_fd >= 0)
219+
close(shm_fd);
220+
return handle;
221+
}
222+
#endif /* USE_ERTS_DLOPEN_MEM */
223+
77224
#define ERTS_NIF_HALT_INFO_FLAG_BLOCK (1 << 0)
78225
#define ERTS_NIF_HALT_INFO_FLAG_HALTING (1 << 1)
79226
#define ERTS_NIF_HALT_INFO_FLAG_WAITING (1 << 2)
@@ -4721,6 +4868,8 @@ Eterm erts_load_nif(Process *c_p, ErtsCodePtr I, Eterm filename, Eterm args)
47214868
Eterm ret = am_ok;
47224869
int veto;
47234870
int is_static = 0;
4871+
int load_from_mem = 0; /* non-zero when loading from in-memory binary */
4872+
Eterm nif_binary = THE_NON_VALUE;
47244873
struct erl_module_nif* lib = NULL;
47254874
struct erl_module_instance* this_mi;
47264875
struct erl_module_instance* prev_mi;
@@ -4731,12 +4880,41 @@ Eterm erts_load_nif(Process *c_p, ErtsCodePtr I, Eterm filename, Eterm args)
47314880
/* since lib_name is used in error messages */
47324881
encoding = ERL_FILENAME_UTF8;
47334882
}
4883+
4884+
/*
4885+
* Accept either:
4886+
* Filename::string()|binary() -- load from file (existing behaviour)
4887+
* #{memory := NifSO} -- load from memory, auto-generated label
4888+
* #{memory := NifSO, label := Filename} -- load from memory with explicit label
4889+
*/
4890+
if (is_map(filename)) {
4891+
const Eterm *mem_val = erts_maps_get(am_memory, filename);
4892+
const Eterm *lbl_val = erts_maps_get(am_label, filename);
4893+
if (mem_val == NULL) {
4894+
return load_nif_error(c_p, "bad_lib",
4895+
"load_nif/2: map argument must contain a 'memory' key");
4896+
}
4897+
if (!is_bitstring(*mem_val) || TAIL_BITS(bitstring_size(*mem_val)) != 0) {
4898+
return load_nif_error(c_p, "bad_lib",
4899+
"load_nif/2: 'memory' value must be a binary");
4900+
}
4901+
nif_binary = *mem_val;
4902+
load_from_mem = 1;
4903+
if (lbl_val != NULL) {
4904+
filename = *lbl_val; /* fall through to filename decoding for label */
4905+
} else {
4906+
lib_name = NULL; /* anonymous: erts_dlopen_mem will generate a name */
4907+
goto after_lib_name;
4908+
}
4909+
}
4910+
47344911
lib_name = erts_convert_filename_to_encoding(filename, NULL, 0,
47354912
ERTS_ALC_T_TMP, 1, 0, encoding,
4736-
NULL, 0);
4913+
NULL, 0);
47374914
if (!lib_name) {
47384915
return THE_NON_VALUE;
47394916
}
4917+
after_lib_name:;
47404918

47414919
/* Find calling module */
47424920
caller = erts_find_function_from_pc(I);
@@ -4769,11 +4947,52 @@ Eterm erts_load_nif(Process *c_p, ErtsCodePtr I, Eterm filename, Eterm args)
47694947
this_mi = module_p->on_load;
47704948
}
47714949

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

5034-
erts_free(ERTS_ALC_T_TMP, lib_name);
5253+
if (lib_name)
5254+
erts_free(ERTS_ALC_T_TMP, lib_name);
50355255

50365256
BIF_RET(ret);
50375257
}

erts/emulator/test/nif_SUITE.erl

Lines changed: 61 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,64 @@ non_exported_nif(Config) when is_list(Config) ->
357359
false = lists:member({lib_version,0}, ?MODULE:module_info(exports)),
358360
ok.
359361

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

0 commit comments

Comments
 (0)