Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
9c9f218c54 fix(fs): support openSync for embedded files in single-file executables
fs.openSync() now correctly works with embedded files from the VFS
(virtual file system) in single-file executables created with
`bun build --compile`.

Previously, fs.readFileSync() and fs.statSync() supported embedded
files, but fs.openSync() did not, causing ENOENT errors when trying
to open embedded files.

The fix adds VFS path detection to the open() function:
- On Linux: Uses memfd_create for an efficient in-memory file descriptor
- On macOS/Windows: Creates a temporary file with the embedded contents

Write operations (O_WRONLY, O_RDWR, O_CREAT, O_TRUNC, O_APPEND) return
EROFS since embedded files are read-only.

Fixes #25638

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 20:14:21 +00:00
2 changed files with 269 additions and 0 deletions

View File

@@ -162,6 +162,15 @@ pub const Async = struct {
const args: Arguments.Open = task.args;
const path = if (bun.strings.eqlComptime(args.path.slice(), "/dev/null")) "\\\\.\\NUL" else args.path.sliceZ(&this.node_fs.sync_error_buf);
// Check if this is a VFS (embedded file) path
if (bun.StandaloneModuleGraph.get()) |graph| {
if (graph.find(path)) |file| {
task.result = NodeFS.openEmbeddedFile(file, args, path);
task.globalObject.bunVM().eventLoop().enqueueTask(jsc.Task.init(task));
return task.promise.value();
}
}
var flags: c_int = @intFromEnum(args.flags);
flags = uv.O.fromBunO(flags);
@@ -4182,6 +4191,13 @@ pub const NodeFS = struct {
else
args.path.sliceZ(&this.sync_error_buf);
// Check if this is a VFS (embedded file) path
if (bun.StandaloneModuleGraph.get()) |graph| {
if (graph.find(path)) |file| {
return openEmbeddedFile(file, args, path);
}
}
return switch (Syscall.open(path, args.flags.asInt(), args.mode)) {
.err => |err| .{
.err = err.withPath(args.path.slice()),
@@ -4190,6 +4206,114 @@ pub const NodeFS = struct {
};
}
fn openEmbeddedFile(file: *bun.StandaloneModuleGraph.File, args: Arguments.Open, path: [:0]const u8) Maybe(Return.Open) {
const flags = args.flags.asInt();
// VFS files are read-only. If write flags are requested, return EROFS.
if (flags & (bun.O.WRONLY | bun.O.RDWR | bun.O.CREAT | bun.O.TRUNC | bun.O.APPEND) != 0) {
return .{
.err = .{
.errno = @intFromEnum(posix.E.ROFS),
.syscall = .open,
.path = path,
},
};
}
// Create an in-memory file descriptor with the embedded file contents
if (comptime Environment.isLinux) {
// Use memfd_create on Linux for an efficient in-memory file descriptor
const memfd = switch (bun.sys.memfd_create("bun-vfs", .non_executable)) {
.err => |err| return .{ .err = err.withPath(path) },
.result => |fd| fd,
};
// Set the file size
switch (bun.sys.ftruncate(memfd, @intCast(file.contents.len))) {
.err => |err| {
memfd.close();
return .{ .err = err.withPath(path) };
},
.result => {},
}
// Write the contents to the memfd
switch (bun.sys.File.writeAll(.{ .handle = memfd }, file.contents)) {
.err => |err| {
memfd.close();
return .{ .err = err.withPath(path) };
},
.result => {},
}
// Seek back to the beginning of the file
switch (bun.sys.lseek(memfd, 0, std.posix.SEEK.SET)) {
.err => |err| {
memfd.close();
return .{ .err = err.withPath(path) };
},
.result => {},
}
return .{ .result = memfd };
} else {
// On macOS/Windows, create a temporary file with the embedded contents
const tmpname_buf = bun.path_buffer_pool.get();
defer bun.path_buffer_pool.put(tmpname_buf);
const extname = bun.strings.trimLeadingChar(std.fs.path.extension(path), '.');
const tmpfilename = bun.fs.FileSystem.tmpname(extname, tmpname_buf, bun.hash(file.name)) catch {
return .{
.err = .{
.errno = @intFromEnum(posix.E.IO),
.syscall = .open,
.path = path,
},
};
};
const tmpdir: bun.FD = .fromStdDir(bun.fs.FileSystem.instance.tmpdir() catch {
return .{
.err = .{
.errno = @intFromEnum(posix.E.IO),
.syscall = .open,
.path = path,
},
};
});
// Create the temp file and write contents
const tmpfile = bun.Tmpfile.create(tmpdir, tmpfilename).unwrap() catch {
return .{
.err = .{
.errno = @intFromEnum(posix.E.IO),
.syscall = .open,
.path = path,
},
};
};
// Write the embedded file contents
switch (bun.sys.File.writeAll(.{ .handle = tmpfile.fd }, file.contents)) {
.err => |err| {
tmpfile.fd.close();
return .{ .err = err.withPath(path) };
},
.result => {},
}
// Seek back to the beginning of the file
switch (bun.sys.lseek(tmpfile.fd, 0, std.posix.SEEK.SET)) {
.err => |err| {
tmpfile.fd.close();
return .{ .err = err.withPath(path) };
},
.result => {},
}
return .{ .result = tmpfile.fd };
}
}
pub fn uv_open(this: *NodeFS, args: Arguments.Open, rc: i64) Maybe(Return.Open) {
_ = this;
if (rc < 0) {

View File

@@ -0,0 +1,145 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
// Test that fs.openSync works with embedded files in single-file executables
// https://github.com/oven-sh/bun/issues/25638
test("fs.openSync works with embedded files in single-file executables", async () => {
using dir = tempDir("issue-25638", {
"data.txt": "This is embedded data for testing openSync",
"main.ts": `
import dataPath from './data.txt' with {type: "file"};
import * as fs from "fs";
const results: { [key: string]: string | number } = {};
// Test readFileSync - should work
try {
results.readFileSync = fs.readFileSync(dataPath, "utf8");
} catch (e: any) {
results.readFileSyncError = e.message;
}
// Test statSync - should work
try {
results.statSync = fs.statSync(dataPath).size;
} catch (e: any) {
results.statSyncError = e.message;
}
// Test openSync - this is the bug being fixed
try {
const fd = fs.openSync(dataPath, "r");
results.openSync = fd;
// Test fstatSync on the opened file descriptor
try {
results.fstatSync = fs.fstatSync(fd).size;
} catch (e: any) {
results.fstatSyncError = e.message;
}
// Test readSync on the opened file descriptor
try {
const buffer = Buffer.alloc(100);
const bytesRead = fs.readSync(fd, buffer, 0, 100, 0);
results.readSync = buffer.toString("utf8", 0, bytesRead);
} catch (e: any) {
results.readSyncError = e.message;
}
fs.closeSync(fd);
} catch (e: any) {
results.openSyncError = e.message;
}
// Test fs.promises.open - async version
try {
const fh = await fs.promises.open(dataPath, "r");
results.promisesOpen = fh.fd;
// Test read on the file handle
const buffer = Buffer.alloc(100);
const { bytesRead } = await fh.read(buffer, 0, 100, 0);
results.promisesRead = buffer.toString("utf8", 0, bytesRead);
await fh.close();
} catch (e: any) {
results.promisesOpenError = e.message;
}
// Test that opening with write flags returns EROFS
try {
const fd = fs.openSync(dataPath, "w");
fs.closeSync(fd);
results.openSyncWrite = "unexpected success";
} catch (e: any) {
results.openSyncWriteError = e.code;
}
console.log(JSON.stringify(results));
`,
});
// Build the single-file executable
const buildProc = Bun.spawn({
cmd: [bunExe(), "build", "--compile", "--target=bun", "main.ts", "--outfile=app"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [buildStdout, buildStderr, buildExitCode] = await Promise.all([
buildProc.stdout.text(),
buildProc.stderr.text(),
buildProc.exited,
]);
expect(buildExitCode).toBe(0);
// Run the compiled executable
const appProc = Bun.spawn({
cmd: [String(dir) + "/app"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [appStdout, appStderr, appExitCode] = await Promise.all([
appProc.stdout.text(),
appProc.stderr.text(),
appProc.exited,
]);
// Parse the results
const results = JSON.parse(appStdout.trim());
// Verify all operations work correctly
expect(results.readFileSync).toBe("This is embedded data for testing openSync");
expect(results.statSync).toBe(42); // length of "This is embedded data for testing openSync"
// These are the main assertions for the bug fix
expect(results.openSyncError).toBeUndefined();
expect(typeof results.openSync).toBe("number");
expect(results.openSync).toBeGreaterThanOrEqual(0);
// fstatSync should work on the VFS file descriptor
expect(results.fstatSyncError).toBeUndefined();
expect(results.fstatSync).toBe(42);
// readSync should work on the VFS file descriptor
expect(results.readSyncError).toBeUndefined();
expect(results.readSync).toBe("This is embedded data for testing openSync");
// fs.promises.open should also work
expect(results.promisesOpenError).toBeUndefined();
expect(typeof results.promisesOpen).toBe("number");
expect(results.promisesRead).toBe("This is embedded data for testing openSync");
// Opening with write flags should fail with EROFS (read-only file system)
expect(results.openSyncWriteError).toBe("EROFS");
expect(appExitCode).toBe(0);
});