Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/jsc/bindings/linux_perf_tracing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ int Bun__linux_trace_init()
return 1; // Already initialized
}

trace_fd = open(TRACE_MARKER_PATH, O_WRONLY);
trace_fd = open(TRACE_MARKER_PATH, O_WRONLY | O_CLOEXEC);
return (trace_fd != -1) ? 1 : 0;
}

Expand Down
2 changes: 1 addition & 1 deletion src/perf/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ fn is_enabled_once() {
#[cfg(any(target_os = "linux", target_os = "android"))]
{
is_enabled_on_linux_once();
if !Linux::is_supported() {
if IS_ENABLED.load(Ordering::SeqCst) && !Linux::is_supported() {
IS_ENABLED.store(false, Ordering::SeqCst);
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/sys/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1888,8 +1888,10 @@ mod posix_impl {
// Linux/FreeBSD, `openat$NOCANCEL(AT_FDCWD, ..)` on Darwin.
openat(Fd::cwd(), path, flags, mode)
}
/// Always `O_CLOEXEC`. A child gets a descriptor only through the spawn path.
pub fn openat(dir: impl AsFd, path: &ZStr, flags: i32, mode: Mode) -> Maybe<Fd> {
let dir = dir.as_fd();
let flags = flags | O::CLOEXEC;
// macOS: `openat$NOCANCEL`, retried on EINTR.
#[cfg(target_os = "macos")]
{
Expand Down Expand Up @@ -1920,6 +1922,7 @@ mod posix_impl {
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn openat2_beneath(dir: impl AsFd, path: &ZStr, flags: i32, mode: Mode) -> Maybe<Fd> {
let dir = dir.as_fd();
let flags = flags | O::CLOEXEC;
super::linux_syscall::openat2_beneath(dir, path, flags, mode)
.map_err(|e| Error::from_code_int(e, Tag::open).with_path(path.as_bytes()))
}
Expand All @@ -1932,6 +1935,7 @@ mod posix_impl {
static UNAVAILABLE: AtomicBool = AtomicBool::new(false);

let dir = dir.as_fd();
let flags = flags | O::CLOEXEC;
if !UNAVAILABLE.load(Ordering::Relaxed) {
match super::linux_syscall::openat2_in_root(dir, path, flags, mode) {
Ok(fd) => return Ok(fd),
Expand Down
88 changes: 88 additions & 0 deletions test/js/node/fs/fs-open-cloexec.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { dlopen } from "bun:ffi";
import { describe, expect, test } from "bun:test";
import { isLinux, isMacOS, tempDir } from "harness";
import fs from "node:fs";
import { join } from "node:path";

// libuv ORs O_CLOEXEC into every fs open, so a descriptor a script opens
// through node:fs never leaks into a child that a native addon forks
// outside of Bun.spawn (system(3), forkpty, posix_spawn).
function hasCloexec(fd: number): boolean {
if (isLinux) {
const info = fs.readFileSync(`/proc/self/fdinfo/${fd}`, "utf8");
const flags = parseInt(info.match(/^flags:\s*(\d+)/m)![1], 8);
return (flags & 0o2000000) !== 0;
}
const libc = dlopen("/usr/lib/libSystem.B.dylib", {
fcntl: { args: ["int", "int"], returns: "int" },
});
try {
const F_GETFD = 1;
const FD_CLOEXEC = 1;
return (libc.symbols.fcntl(fd, F_GETFD) & FD_CLOEXEC) !== 0;
} finally {
libc.close();
}
}

describe.skipIf(!isLinux && !isMacOS)("node:fs opens set O_CLOEXEC", () => {
test("fs.openSync", () => {
using dir = tempDir("fs-cloexec", { "a.txt": "hello" });
for (const flags of ["r", "w", "a", "r+", fs.constants.O_RDONLY]) {
const fd = fs.openSync(join(String(dir), "a.txt"), flags);
try {
expect(hasCloexec(fd)).toBe(true);
} finally {
fs.closeSync(fd);
}
}
});

test("fs.promises.open", async () => {
using dir = tempDir("fs-cloexec", { "a.txt": "hello" });
await using handle = await fs.promises.open(join(String(dir), "a.txt"), "r+");
expect(hasCloexec(handle.fd)).toBe(true);
});

test("fs.open callback", async () => {
using dir = tempDir("fs-cloexec", { "a.txt": "hello" });
const fd = await new Promise<number>((resolve, reject) =>
fs.open(join(String(dir), "a.txt"), "r", (err, fd) => (err ? reject(err) : resolve(fd))),
);
try {
expect(hasCloexec(fd)).toBe(true);
} finally {
fs.closeSync(fd);
}
});

test("fs.createReadStream and fs.createWriteStream", async () => {
using dir = tempDir("fs-cloexec", { "a.txt": "hello" });
const rs = fs.createReadStream(join(String(dir), "a.txt"));
const ws = fs.createWriteStream(join(String(dir), "b.txt"));
const [rfd, wfd] = await Promise.all([
new Promise<number>((resolve, reject) => rs.once("open", resolve).once("error", reject)),
new Promise<number>((resolve, reject) => ws.once("open", resolve).once("error", reject)),
]);
Comment thread
claude[bot] marked this conversation as resolved.
try {
expect(hasCloexec(rfd)).toBe(true);
expect(hasCloexec(wfd)).toBe(true);
} finally {
rs.close();
ws.close();
}
});

test("user flags are kept", () => {
using dir = tempDir("fs-cloexec", { "log.txt": "ab" });
const fd = fs.openSync(join(String(dir), "log.txt"), fs.constants.O_WRONLY | fs.constants.O_APPEND);
try {
// Without O_APPEND the write would land at offset 0 and give "xb".
fs.writeSync(fd, "x");
expect(hasCloexec(fd)).toBe(true);
} finally {
fs.closeSync(fd);
}
expect(fs.readFileSync(join(String(dir), "log.txt"), "utf8")).toBe("abx");
});
});
Loading