fix(compile): fix native module export corruption with multiple NAPI modules (#26080)

## Summary

Fixes native module export corruption when compiling multiple NAPI
modules with `bun build --compile` on Linux.

- When loading multiple `.node` files in a compiled binary, the second
module would incorrectly get the first module's exports
- Root cause: memfd file descriptors were closed after dlopen, allowing
fd reuse. Since dlopen caches by path (`/proc/self/fd/N`), it returned
the wrong cached handle
- This bug occurs when loading native modules in quick succession, as
the fd number is likely to be reused immediately after being closed
- Fix: Disable the memfd optimization and always use temp files with
unique paths

## Test plan

- [x] Added regression test in `test/regression/issue/26045/`
- [x] Test fails with production bun (v1.3.6)
- [x] Test passes with the fix

Fixes https://github.com/oven-sh/bun/issues/26045

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Dylan Conway
2026-01-20 12:40:12 -08:00
committed by GitHub
parent 170f8f7962
commit 66d8397bd7
4 changed files with 14 additions and 59 deletions

View File

@@ -30,45 +30,19 @@ pub fn resetArena(this: *ModuleLoader, jsc_vm: *VirtualMachine) void {
}
}
fn resolveEmbeddedNodeFileViaMemfd(file: *bun.StandaloneModuleGraph.File, path_buffer: *bun.PathBuffer, fd: *i32) ![]const u8 {
var label_buf: [128]u8 = undefined;
const count = struct {
pub var counter = std.atomic.Value(u32).init(0);
pub fn get() u32 {
return counter.fetchAdd(1, .seq_cst);
}
}.get();
const label = std.fmt.bufPrintZ(&label_buf, "node-addon-{d}", .{count}) catch "";
const memfd = try bun.sys.memfd_create(label, .executable).unwrap();
errdefer memfd.close();
fd.* = @intCast(memfd.cast());
errdefer fd.* = -1;
try bun.sys.ftruncate(memfd, @intCast(file.contents.len)).unwrap();
try bun.sys.File.writeAll(.{ .handle = memfd }, file.contents).unwrap();
return try std.fmt.bufPrint(path_buffer, "/proc/self/fd/{d}", .{memfd.cast()});
}
pub fn resolveEmbeddedFile(vm: *VirtualMachine, path_buf: *bun.PathBuffer, linux_memfd: *i32, input_path: []const u8, extname: []const u8) ?[]const u8 {
pub fn resolveEmbeddedFile(vm: *VirtualMachine, path_buf: *bun.PathBuffer, input_path: []const u8, extname: []const u8) ?[]const u8 {
if (input_path.len == 0) return null;
var graph = vm.standalone_module_graph orelse return null;
const file = graph.find(input_path) orelse return null;
if (comptime Environment.isLinux) {
// Best-effort: use memfd to avoid hitting the disk
if (resolveEmbeddedNodeFileViaMemfd(file, path_buf, linux_memfd)) |path| {
return path;
} else |_| {
// fall back to temp file
}
// TODO: use /proc/fd/12346 instead! Avoid the copy!
}
// atomically write to a tmpfile and then move it to the final destination
const tmpname_buf = bun.path_buffer_pool.get();
defer bun.path_buffer_pool.put(tmpname_buf);
const tmpfilename = bun.fs.FileSystem.tmpname(extname, tmpname_buf, bun.hash(file.name)) catch return null;
var tmpname_buf: bun.PathBuffer = undefined;
const tmpfilename = bun.fs.FileSystem.tmpname(extname, &tmpname_buf, bun.hash(file.name)) catch return null;
const tmpdir: bun.FD = .fromStdDir(bun.fs.FileSystem.instance.tmpdir() catch return null);
// First we open the tmpfile, to avoid any other work in the event of failure.
@@ -76,7 +50,7 @@ pub fn resolveEmbeddedFile(vm: *VirtualMachine, path_buf: *bun.PathBuffer, linux
defer tmpfile.fd.close();
switch (bun.api.node.fs.NodeFS.writeFileWithPathBuffer(
tmpname_buf, // not used
&tmpname_buf, // not used
.{
.data = .{
@@ -1326,14 +1300,14 @@ pub const FetchFlags = enum {
};
/// Support embedded .node files
export fn Bun__resolveEmbeddedNodeFile(vm: *VirtualMachine, in_out_str: *bun.String, linux_memfd_fd_to_close: *i32) bool {
export fn Bun__resolveEmbeddedNodeFile(vm: *VirtualMachine, in_out_str: *bun.String) bool {
if (vm.standalone_module_graph == null) return false;
const input_path = in_out_str.toUTF8(bun.default_allocator);
defer input_path.deinit();
const path_buffer = bun.path_buffer_pool.get();
defer bun.path_buffer_pool.put(path_buffer);
const result = ModuleLoader.resolveEmbeddedFile(vm, path_buffer, linux_memfd_fd_to_close, input_path.slice(), "node") orelse return false;
const path_buf = bun.path_buffer_pool.get();
defer bun.path_buffer_pool.put(path_buf);
const result = ModuleLoader.resolveEmbeddedFile(vm, path_buf, input_path.slice(), "node") orelse return false;
in_out_str.* = bun.String.cloneUTF8(result);
return true;
}