From c08ffadf56e0a698175feebc2c3ee9636670a778 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 19 Dec 2025 23:18:21 -0800 Subject: [PATCH] perf(linux): add memfd optimizations and typed flags (#25597) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add `MemfdFlags` enum to replace raw integer flags for `memfd_create`, providing semantic clarity for different use cases (`executable`, `non_executable`, `cross_process`) - Add support for `MFD_EXEC` and `MFD_NOEXEC_SEAL` flags (Linux 6.3+) with automatic fallback to older kernel flags when `EINVAL` is returned - Use memfd + `/proc/self/fd/{fd}` path for loading embedded `.node` files in standalone builds, avoiding disk writes entirely on Linux ## Test plan - [ ] Verify standalone builds with embedded `.node` files work on Linux - [ ] Verify fallback works on older kernels (pre-6.3) - [ ] Verify subprocess stdio memfd still works correctly 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude --- src/allocators/LinuxMemFdAllocator.zig | 2 +- src/bun.js/ModuleLoader.zig | 46 +++++++++++++++++++++----- src/bun.js/api/bun/process.zig | 2 +- src/bun.js/api/bun/spawn/stdio.zig | 2 +- src/bun.js/api/ffi.zig | 16 +++++++-- src/bun.js/bindings/BunProcess.cpp | 16 +++++++-- src/bun.js/node/node_fs_binding.zig | 2 +- src/sys.zig | 46 +++++++++++++++++++++++--- 8 files changed, 109 insertions(+), 23 deletions(-) diff --git a/src/allocators/LinuxMemFdAllocator.zig b/src/allocators/LinuxMemFdAllocator.zig index 3b6d0ba613..71cfcd4398 100644 --- a/src/allocators/LinuxMemFdAllocator.zig +++ b/src/allocators/LinuxMemFdAllocator.zig @@ -132,7 +132,7 @@ pub fn create(bytes: []const u8) bun.sys.Maybe(bun.webcore.Blob.Store.Bytes) { const label = std.fmt.bufPrintZ(&label_buf, "memfd-num-{d}", .{memfd_counter.fetchAdd(1, .monotonic)}) catch ""; // Using huge pages was slower. - const fd = switch (bun.sys.memfd_create(label, std.os.linux.MFD.CLOEXEC)) { + const fd = switch (bun.sys.memfd_create(label, .non_executable)) { .err => |err| return .{ .err = bun.sys.Error.fromCode(err.getErrno(), .open) }, .result => |fd| fd, }; diff --git a/src/bun.js/ModuleLoader.zig b/src/bun.js/ModuleLoader.zig index 1cfaaedc6a..0ea84ff9b3 100644 --- a/src/bun.js/ModuleLoader.zig +++ b/src/bun.js/ModuleLoader.zig @@ -30,19 +30,45 @@ pub fn resetArena(this: *ModuleLoader, jsc_vm: *VirtualMachine) void { } } -pub fn resolveEmbeddedFile(vm: *VirtualMachine, input_path: []const u8, extname: []const u8) ?[]const u8 { +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 { 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) { - // TODO: use /proc/fd/12346 instead! Avoid the copy! + // 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 + } } // atomically write to a tmpfile and then move it to the final destination - var tmpname_buf: bun.PathBuffer = undefined; - const tmpfilename = bun.fs.FileSystem.tmpname(extname, &tmpname_buf, bun.hash(file.name)) catch return null; - + 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; 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. @@ -50,7 +76,7 @@ pub fn resolveEmbeddedFile(vm: *VirtualMachine, input_path: []const u8, extname: defer tmpfile.fd.close(); switch (bun.api.node.fs.NodeFS.writeFileWithPathBuffer( - &tmpname_buf, // not used + tmpname_buf, // not used .{ .data = .{ @@ -66,7 +92,7 @@ pub fn resolveEmbeddedFile(vm: *VirtualMachine, input_path: []const u8, extname: }, else => {}, } - return bun.path.joinAbs(bun.fs.FileSystem.instance.fs.tmpdirPath(), .auto, tmpfilename); + return bun.path.joinAbsStringBuf(bun.fs.FileSystem.instance.fs.tmpdirPath(), path_buf, &[_]string{tmpfilename}, .auto); } pub export fn Bun__getDefaultLoader(global: *JSGlobalObject, str: *const bun.String) api.Loader { @@ -1300,12 +1326,14 @@ pub const FetchFlags = enum { }; /// Support embedded .node files -export fn Bun__resolveEmbeddedNodeFile(vm: *VirtualMachine, in_out_str: *bun.String) bool { +export fn Bun__resolveEmbeddedNodeFile(vm: *VirtualMachine, in_out_str: *bun.String, linux_memfd_fd_to_close: *i32) bool { if (vm.standalone_module_graph == null) return false; const input_path = in_out_str.toUTF8(bun.default_allocator); defer input_path.deinit(); - const result = ModuleLoader.resolveEmbeddedFile(vm, input_path.slice(), "node") orelse return false; + 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; in_out_str.* = bun.String.cloneUTF8(result); return true; } diff --git a/src/bun.js/api/bun/process.zig b/src/bun.js/api/bun/process.zig index b1b5971946..325740252e 100644 --- a/src/bun.js/api/bun/process.zig +++ b/src/bun.js/api/bun/process.zig @@ -1346,7 +1346,7 @@ pub fn spawnProcessPosix( else => "spawn_stdio_generic", }; - const fd = bun.sys.memfd_create(label, 0).unwrap() catch break :use_memfd; + const fd = bun.sys.memfd_create(label, .cross_process).unwrap() catch break :use_memfd; to_close_on_error.append(fd) catch {}; to_set_cloexec.append(fd) catch {}; diff --git a/src/bun.js/api/bun/spawn/stdio.zig b/src/bun.js/api/bun/spawn/stdio.zig index 457cc34d69..3e932c675b 100644 --- a/src/bun.js/api/bun/spawn/stdio.zig +++ b/src/bun.js/api/bun/spawn/stdio.zig @@ -100,7 +100,7 @@ pub const Stdio = union(enum) { else => "spawn_stdio_memory_file", }; - const fd = bun.sys.memfd_create(label, 0).unwrap() catch return false; + const fd = bun.sys.memfd_create(label, .cross_process).unwrap() catch return false; var remain = this.byteSlice(); diff --git a/src/bun.js/api/ffi.zig b/src/bun.js/api/ffi.zig index 4a5a54535a..24a12e6929 100644 --- a/src/bun.js/api/ffi.zig +++ b/src/bun.js/api/ffi.zig @@ -1001,10 +1001,23 @@ pub const FFI = struct { if (object_value.isEmptyOrUndefinedOrNull()) return invalidOptionsArg(global); const object = object_value.getObject() orelse return invalidOptionsArg(global); - var filepath_buf: bun.PathBuffer = undefined; + var filepath_buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(filepath_buf); + var linux_memfd_to_close: i32 = -1; + defer { + if (Environment.isLinux) { + if (linux_memfd_to_close != -1) { + _ = bun.FD.fromSystem(linux_memfd_to_close).close(); + } + } else { + bun.debugAssert(linux_memfd_to_close == -1); + } + } const name = brk: { if (jsc.ModuleLoader.resolveEmbeddedFile( vm, + filepath_buf, + &linux_memfd_to_close, name_slice.slice(), switch (Environment.os) { .linux => "so", @@ -1013,7 +1026,6 @@ pub const FFI = struct { .wasm => @compileError("TODO"), }, )) |resolved| { - @memcpy(filepath_buf[0..resolved.len], resolved); filepath_buf[resolved.len] = 0; break :brk filepath_buf[0..resolved.len]; } diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index fa7519459c..0c427be8f0 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -296,7 +296,7 @@ JSC_DEFINE_CUSTOM_SETTER(Process_defaultSetter, (JSC::JSGlobalObject * globalObj return true; } -extern "C" bool Bun__resolveEmbeddedNodeFile(void*, BunString*); +extern "C" bool Bun__resolveEmbeddedNodeFile(void*, BunString*, int32_t*); #if OS(WINDOWS) extern "C" HMODULE Bun__LoadLibraryBunString(BunString*); #endif @@ -434,6 +434,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, (JSC::JSGlobalObject * globalOb } CString utf8; + int32_t linuxMemfdToClose = -1; // Support embedded .node files // See StandaloneModuleGraph.zig for what this "$bunfs" thing is @@ -445,8 +446,8 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, (JSC::JSGlobalObject * globalOb bool deleteAfter = false; if (filename.startsWith(StandaloneModuleGraph__base_path)) { BunString bunStr = Bun::toString(filename); - if (Bun__resolveEmbeddedNodeFile(globalObject->bunVM(), &bunStr)) { - filename = bunStr.toWTFString(BunString::ZeroCopy); + if (Bun__resolveEmbeddedNodeFile(globalObject->bunVM(), &bunStr, &linuxMemfdToClose)) { + filename = bunStr.transferToWTFString(); deleteAfter = !filename.startsWith("/proc/"_s); } } @@ -481,11 +482,20 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, (JSC::JSGlobalObject * globalOb delete[] dupeZ; } } + ASSERT(linuxMemfdToClose == -1); #else if (deleteAfter) { deleteAfter = false; Bun__unlink(utf8.data(), utf8.length()); } +#if OS(LINUX) + if (linuxMemfdToClose != -1) { + close(linuxMemfdToClose); + linuxMemfdToClose = -1; + } +#else + ASSERT(linuxMemfdToClose == -1); +#endif #endif }; diff --git a/src/bun.js/node/node_fs_binding.zig b/src/bun.js/node/node_fs_binding.zig index 57c85544a9..156605f221 100644 --- a/src/bun.js/node/node_fs_binding.zig +++ b/src/bun.js/node/node_fs_binding.zig @@ -221,7 +221,7 @@ pub fn createMemfdForTesting(globalObject: *jsc.JSGlobalObject, callFrame: *jsc. } const size = arguments.ptr[0].toInt64(); - switch (bun.sys.memfd_create("my_memfd", std.os.linux.MFD.CLOEXEC)) { + switch (bun.sys.memfd_create("my_memfd", .non_executable)) { .result => |fd| { _ = bun.sys.ftruncate(fd, size); return jsc.JSValue.jsNumber(fd.cast()); diff --git a/src/sys.zig b/src/sys.zig index acc8fbd66e..4bf88c0f3d 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -2971,15 +2971,51 @@ pub fn munmap(memory: []align(page_size_min) const u8) Maybe(void) { } else return .success; } -pub fn memfd_create(name: [:0]const u8, flags: u32) Maybe(bun.FileDescriptor) { +pub const MemfdFlags = enum(u32) { + // Recent Linux kernel versions require MFD_EXEC. + executable = MFD_EXEC | MFD_ALLOW_SEALING | MFD_CLOEXEC, + non_executable = MFD_NOEXEC_SEAL | MFD_ALLOW_SEALING | MFD_CLOEXEC, + cross_process = MFD_NOEXEC_SEAL, + + pub fn olderKernelFlag(this: MemfdFlags) u32 { + return switch (this) { + .non_executable, .executable => MFD_CLOEXEC, + .cross_process => 0, + }; + } + + const MFD_NOEXEC_SEAL: u32 = 0x0008; + const MFD_EXEC: u32 = 0x0010; + const MFD_CLOEXEC: u32 = std.os.linux.MFD.CLOEXEC; + const MFD_ALLOW_SEALING: u32 = std.os.linux.MFD.ALLOW_SEALING; +}; +pub fn memfd_create(name: [:0]const u8, flags_: MemfdFlags) Maybe(bun.FileDescriptor) { if (comptime !Environment.isLinux) @compileError("linux only!"); + var flags: u32 = @intFromEnum(flags_); + while (true) { + const rc = std.os.linux.memfd_create(name, flags); + log("memfd_create({s}, {s}) = {d}", .{ name, @tagName(flags_), rc }); - const rc = std.os.linux.memfd_create(name, flags); + if (Maybe(bun.FileDescriptor).errnoSys(rc, .memfd_create)) |err| { + switch (err.getErrno()) { + .INTR => continue, + .INVAL => { + // MFD_EXEC / MFD_NOEXEC_SEAL require Linux 6.3. + if (@intFromEnum(flags_) == flags) { + flags = flags_.olderKernelFlag(); + log("memfd_create retrying without exec/noexec flag, using {d}", .{flags}); + continue; + } + }, + else => {}, + } - log("memfd_create({s}, {d}) = {d}", .{ name, flags, rc }); + return err; + } - return Maybe(bun.FileDescriptor).errnoSys(rc, .memfd_create) orelse - .{ .result = .fromNative(@intCast(rc)) }; + return .{ .result = .fromNative(@intCast(rc)) }; + } + unreachable; } pub fn setPipeCapacityOnLinux(fd: bun.FileDescriptor, capacity: usize) Maybe(usize) {