mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
perf(linux): add memfd optimizations and typed flags (#25597)
## 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
46
src/sys.zig
46
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) {
|
||||
|
||||
Reference in New Issue
Block a user