diff --git a/src/jsc/bindings/linux_perf_tracing.cpp b/src/jsc/bindings/linux_perf_tracing.cpp index 07187ca171fc..136e4061ce84 100644 --- a/src/jsc/bindings/linux_perf_tracing.cpp +++ b/src/jsc/bindings/linux_perf_tracing.cpp @@ -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; } diff --git a/src/perf/lib.rs b/src/perf/lib.rs index fed5f4fcbe33..03e41f69576c 100644 --- a/src/perf/lib.rs +++ b/src/perf/lib.rs @@ -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); } } diff --git a/src/sys/lib.rs b/src/sys/lib.rs index 7c3ced299f4b..1e3ce5de1d25 100644 --- a/src/sys/lib.rs +++ b/src/sys/lib.rs @@ -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 { let dir = dir.as_fd(); + let flags = flags | O::CLOEXEC; // macOS: `openat$NOCANCEL`, retried on EINTR. #[cfg(target_os = "macos")] { @@ -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 { 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())) } @@ -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), diff --git a/test/js/node/fs/fs-open-cloexec.test.ts b/test/js/node/fs/fs-open-cloexec.test.ts new file mode 100644 index 000000000000..d0da585be157 --- /dev/null +++ b/test/js/node/fs/fs-open-cloexec.test.ts @@ -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((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((resolve, reject) => rs.once("open", resolve).once("error", reject)), + new Promise((resolve, reject) => ws.once("open", resolve).once("error", reject)), + ]); + 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"); + }); +});