[libc-commits] [libc] [libc] Add 'x' and 'e' mode support to fopen() (PR #224207)
Jeong Jihyeon via libc-commits
libc-commits at lists.llvm.org
Thu Sep 17 07:55:44 PDT 2026
https://github.com/JihyeonJeong129 updated https://github.com/llvm/llvm-project/pull/224207
>From eae873df6de481be9b9275764142448fb2270d06 Mon Sep 17 00:00:00 2001
From: Jihyeon Jeong <jh.jeong129 at gmail.com>
Date: Thu, 17 Sep 2026 07:01:55 +0000
Subject: [PATCH 1/2] [libc] Add 'x' and 'e' mode support to fopen family
---
libc/src/__support/File/file_mode.h | 14 ++-
libc/src/__support/File/linux/CMakeLists.txt | 1 +
libc/src/__support/File/linux/file.cpp | 29 +++++-
libc/src/__support/File/linux/file_flags.h | 2 +
.../src/__support/File/file_mode_test.cpp | 47 ++++++++++
libc/test/src/stdio/CMakeLists.txt | 18 ++++
libc/test/src/stdio/fdopen_test.cpp | 32 +++++++
libc/test/src/stdio/fopen_test.cpp | 90 +++++++++++++++++++
libc/test/src/stdio/freopen_test.cpp | 29 ++++++
9 files changed, 259 insertions(+), 3 deletions(-)
diff --git a/libc/src/__support/File/file_mode.h b/libc/src/__support/File/file_mode.h
index f9aece8d22a36..8049516da261f 100644
--- a/libc/src/__support/File/file_mode.h
+++ b/libc/src/__support/File/file_mode.h
@@ -62,6 +62,9 @@ class FileMode {
case 'x':
file_mode_ |= static_cast<Mode>(CreateType::EXCLUSIVE);
break;
+ case 'e':
+ file_mode_ |= static_cast<Mode>(Extension::CLOSE_ON_EXEC);
+ break;
default:
file_mode_ = 0;
}
@@ -108,6 +111,10 @@ class FileMode {
return (file_mode_ & static_cast<Mode>(CreateType::EXCLUSIVE)) != 0;
}
+ constexpr bool is_close_on_exec() const {
+ return (file_mode_ & static_cast<Mode>(Extension::CLOSE_ON_EXEC)) != 0;
+ }
+
private:
// Mode is a generic or abstract mode bit for all kinds of modes
// (open-mode, 'content-mode', 'create-modes')
@@ -115,7 +122,7 @@ class FileMode {
// Denotes the mode of the file.
//
- // The three different types of flags below are to be used with '|' operator.
+ // The different types of flags below are to be used with '|' operator.
// Their values correspond to mutually exclusive bits in a 32-bit unsigned
// integer value. A flag set can include both READ and WRITE if the file
// is opened in update mode (ie. if the file was opened with a '+' the mode
@@ -138,6 +145,11 @@ class FileMode {
EXCLUSIVE = 0x100,
};
+ // POSIX extensions to the ISO C file modes.
+ enum class Extension : Mode {
+ CLOSE_ON_EXEC = 0x1000,
+ };
+
// This property tracks the mode for the particular file instance (i.e
// currently opened file)
Mode file_mode_;
diff --git a/libc/src/__support/File/linux/CMakeLists.txt b/libc/src/__support/File/linux/CMakeLists.txt
index d0e1df252895c..05efc0f6f6139 100644
--- a/libc/src/__support/File/linux/CMakeLists.txt
+++ b/libc/src/__support/File/linux/CMakeLists.txt
@@ -14,6 +14,7 @@ add_object_library(
libc.hdr.types.off_t
libc.src.__support.CPP.new
libc.src.__support.OSUtil.linux.syscall_wrappers.close
+ libc.src.__support.OSUtil.linux.syscall_wrappers.dup3
libc.src.__support.OSUtil.linux.syscall_wrappers.fcntl
libc.src.__support.OSUtil.linux.syscall_wrappers.lseek
libc.src.__support.OSUtil.linux.syscall_wrappers.open
diff --git a/libc/src/__support/File/linux/file.cpp b/libc/src/__support/File/linux/file.cpp
index cc6a1fcbd3168..29d177f36328d 100644
--- a/libc/src/__support/File/linux/file.cpp
+++ b/libc/src/__support/File/linux/file.cpp
@@ -15,7 +15,7 @@
#include "src/__support/File/file.h"
#include "src/__support/File/file_mode.h"
#include "src/__support/OSUtil/linux/syscall_wrappers/close.h"
-#include "src/__support/OSUtil/linux/syscall_wrappers/dup2.h"
+#include "src/__support/OSUtil/linux/syscall_wrappers/dup3.h"
#include "src/__support/OSUtil/linux/syscall_wrappers/fcntl.h"
#include "src/__support/OSUtil/linux/syscall_wrappers/lseek.h"
#include "src/__support/OSUtil/linux/syscall_wrappers/open.h"
@@ -87,6 +87,14 @@ static int map_c_mode_flags_to_linux_open_flags(const FileMode &file_mode) {
else if (file_mode.is_write())
open_flags |= LinuxFileFlags::CREATE_OR_TRUNCATE;
+ // Ignore 'x' for read modes: O_EXCL is only meaningful with O_CREAT here.
+ if (file_mode.is_exclusive_create() &&
+ (file_mode.is_write() || file_mode.is_append()))
+ open_flags |= LinuxFileFlags::EXCLUSIVE_CREATE;
+
+ if (file_mode.is_close_on_exec())
+ open_flags |= LinuxFileFlags::CLOSE_ON_EXEC;
+
return open_flags;
}
@@ -119,6 +127,14 @@ ErrorOr<File *> openfile(const char *path, const char *mode) {
return file;
}
+static ErrorOr<int> set_close_on_exec(int fd) {
+ auto flags = linux_syscalls::fcntl(fd, F_GETFD);
+ if (!flags)
+ return Error(flags.error());
+ return linux_syscalls::fcntl(
+ fd, F_SETFD, reinterpret_cast<void *>(flags.value() | FD_CLOEXEC));
+}
+
ErrorOr<LinuxFile *> create_file_from_fd(int fd, const char *mode) {
const FileMode file_mode(mode);
@@ -139,6 +155,12 @@ ErrorOr<LinuxFile *> create_file_from_fd(int fd, const char *mode) {
return Error(EINVAL);
}
+ if (file_mode.is_close_on_exec()) {
+ auto cloexec_result = set_close_on_exec(fd);
+ if (!cloexec_result)
+ return Error(cloexec_result.error());
+ }
+
bool do_seek = false;
if (file_mode.is_append() &&
!LinuxFileFlags::file_has_append_flag(fd_flags)) {
@@ -215,7 +237,10 @@ int LinuxFile::reopen_unlocked(const char *path, const char *mode) {
// Else the new file successfully opened, so we move it into the fd the old
// file was using if the old fd exists.
if (old_fd >= 0) {
- auto dup_result = linux_syscalls::dup2(new_fd.value(), old_fd);
+ // Preserve close-on-exec atomically when replacing the old descriptor.
+ int dup_flags =
+ file_mode.is_close_on_exec() ? LinuxFileFlags::CLOSE_ON_EXEC : 0;
+ auto dup_result = linux_syscalls::dup3(new_fd.value(), old_fd, dup_flags);
if (!dup_result) {
linux_syscalls::close(new_fd.value());
reset_stream_state_unlocked(file_mode);
diff --git a/libc/src/__support/File/linux/file_flags.h b/libc/src/__support/File/linux/file_flags.h
index deb357c261d55..ce68496101639 100644
--- a/libc/src/__support/File/linux/file_flags.h
+++ b/libc/src/__support/File/linux/file_flags.h
@@ -27,6 +27,8 @@ class LinuxFileFlags {
static constexpr int WRITE_ONLY = O_WRONLY;
static constexpr int READ_ONLY = O_RDONLY;
static constexpr int CREATE_OR_TRUNCATE = O_CREAT | O_TRUNC;
+ static constexpr int EXCLUSIVE_CREATE = O_EXCL;
+ static constexpr int CLOSE_ON_EXEC = O_CLOEXEC;
// File created will have 0666 permissions.
LIBC_INLINE static constexpr mode_t OPEN_MODE =
diff --git a/libc/test/src/__support/File/file_mode_test.cpp b/libc/test/src/__support/File/file_mode_test.cpp
index 92f58bd154ee3..cb683aa41f3a4 100644
--- a/libc/test/src/__support/File/file_mode_test.cpp
+++ b/libc/test/src/__support/File/file_mode_test.cpp
@@ -47,6 +47,11 @@ TEST(LlvmLibcFileModeTest, FirstCharacterMustBeAValidMode) {
constexpr FileMode exclusive_create("x");
EXPECT_FALSE(exclusive_create.is_valid());
EXPECT_FALSE(exclusive_create.is_exclusive_create());
+
+ // close-on-exec set as first character => invalid
+ constexpr FileMode close_on_exec("e");
+ EXPECT_FALSE(close_on_exec.is_valid());
+ EXPECT_FALSE(close_on_exec.is_close_on_exec());
}
TEST(LlvmLibcFileModeTest, AllPossibleValidCombinations) {
@@ -59,6 +64,7 @@ TEST(LlvmLibcFileModeTest, AllPossibleValidCombinations) {
EXPECT_TRUE(readonly.is_valid());
EXPECT_TRUE(readonly.is_read());
EXPECT_TRUE(readonly.read_allowed());
+ EXPECT_FALSE(readonly.is_close_on_exec());
// b. Read and Update mode
constexpr FileMode read_and_update("r+");
@@ -91,6 +97,7 @@ TEST(LlvmLibcFileModeTest, AllPossibleValidCombinations) {
EXPECT_TRUE(writeonly.is_valid());
EXPECT_TRUE(writeonly.is_write());
EXPECT_TRUE(writeonly.write_allowed());
+ EXPECT_FALSE(writeonly.is_close_on_exec());
// b. Write and Update mode
constexpr FileMode write_and_update("w+");
@@ -157,6 +164,7 @@ TEST(LlvmLibcFileModeTest, AllPossibleValidCombinations) {
EXPECT_TRUE(appendonly.is_valid());
EXPECT_TRUE(appendonly.is_append());
EXPECT_TRUE(appendonly.write_allowed());
+ EXPECT_FALSE(appendonly.is_close_on_exec());
// b. Append and Update
constexpr FileMode append_and_update("a+");
@@ -181,6 +189,45 @@ TEST(LlvmLibcFileModeTest, AllPossibleValidCombinations) {
EXPECT_TRUE(append_update_binary.is_binary_format());
EXPECT_TRUE(append_update_binary.write_allowed());
EXPECT_TRUE(append_update_binary.read_allowed());
+
+ // 4. Close-on-exec: possible valid close-on-exec combinations
+
+ // a. Read Close-on-exec
+ constexpr FileMode read_close_on_exec("re");
+ EXPECT_TRUE(read_close_on_exec.is_valid());
+ EXPECT_TRUE(read_close_on_exec.is_read());
+ EXPECT_TRUE(read_close_on_exec.is_close_on_exec());
+ EXPECT_TRUE(read_close_on_exec.read_allowed());
+ EXPECT_FALSE(read_close_on_exec.write_allowed());
+ EXPECT_FALSE(read_close_on_exec.is_binary_format());
+ EXPECT_FALSE(read_close_on_exec.is_exclusive_create());
+
+ // b. Write Close-on-exec
+ constexpr FileMode write_close_on_exec("we");
+ EXPECT_TRUE(write_close_on_exec.is_valid());
+ EXPECT_TRUE(write_close_on_exec.is_write());
+ EXPECT_TRUE(write_close_on_exec.is_close_on_exec());
+ EXPECT_TRUE(write_close_on_exec.write_allowed());
+ EXPECT_FALSE(write_close_on_exec.read_allowed());
+
+ // c. Append Close-on-exec
+ constexpr FileMode append_close_on_exec("ae");
+ EXPECT_TRUE(append_close_on_exec.is_valid());
+ EXPECT_TRUE(append_close_on_exec.is_append());
+ EXPECT_TRUE(append_close_on_exec.is_close_on_exec());
+ EXPECT_TRUE(append_close_on_exec.write_allowed());
+ EXPECT_FALSE(append_close_on_exec.read_allowed());
+
+ // d. Write Close-on-exec Exclusive Update Binary
+ constexpr FileMode write_all_modifiers("wex+b");
+ EXPECT_TRUE(write_all_modifiers.is_valid());
+ EXPECT_TRUE(write_all_modifiers.is_write());
+ EXPECT_TRUE(write_all_modifiers.is_close_on_exec());
+ EXPECT_TRUE(write_all_modifiers.is_exclusive_create());
+ EXPECT_TRUE(write_all_modifiers.is_update());
+ EXPECT_TRUE(write_all_modifiers.is_binary_format());
+ EXPECT_TRUE(write_all_modifiers.read_allowed());
+ EXPECT_TRUE(write_all_modifiers.write_allowed());
}
TEST(LlvmLibcFileModeTest, InvalidCombinations) {
diff --git a/libc/test/src/stdio/CMakeLists.txt b/libc/test/src/stdio/CMakeLists.txt
index e0ca1ab2ca162..9fc984b08280d 100644
--- a/libc/test/src/stdio/CMakeLists.txt
+++ b/libc/test/src/stdio/CMakeLists.txt
@@ -10,6 +10,7 @@ add_libc_test(
libc.include.stdio
libc.hdr.fcntl_macros
libc.hdr.stdio_macros
+ libc.src.__support.CPP.scope
libc.src.__support.macros.properties.os
libc.src.errno.errno
libc.src.fcntl.fcntl
@@ -482,6 +483,19 @@ add_libc_test(
libc.src.stdio.stderr
)
+set(fopen_test_deps)
+if(LIBC_TARGET_OS STREQUAL "linux")
+ list(APPEND fopen_test_deps
+ libc.hdr.fcntl_macros
+ libc.src.errno.errno
+ libc.src.fcntl.fcntl
+ libc.src.stdio.fileno
+ libc.src.stdio.remove
+ libc.test.UnitTest.ErrnoCheckingTest
+ libc.test.UnitTest.ErrnoSetterMatcher
+ )
+endif()
+
add_libc_test(
fopen_test
SUITE
@@ -489,7 +503,9 @@ add_libc_test(
SRCS
fopen_test.cpp
DEPENDS
+ ${fopen_test_deps}
libc.src.__support.CPP.scope
+ libc.src.__support.macros.properties.os
libc.src.stdio.fread
libc.src.stdio.fwrite
libc.src.stdio.fclose
@@ -555,6 +571,8 @@ if(${LIBC_TARGET_OS} STREQUAL "linux")
DEPENDS
libc.hdr.fcntl_macros
libc.hdr.sys_stat_macros
+ libc.src.__support.CPP.scope
+ libc.src.fcntl.fcntl
libc.src.fcntl.open
libc.src.stdio.fclose
libc.src.stdio.fdopen
diff --git a/libc/test/src/stdio/fdopen_test.cpp b/libc/test/src/stdio/fdopen_test.cpp
index 0a5e9863055aa..d37b315890598 100644
--- a/libc/test/src/stdio/fdopen_test.cpp
+++ b/libc/test/src/stdio/fdopen_test.cpp
@@ -10,6 +10,8 @@
#include "hdr/fcntl_macros.h"
#include "hdr/sys_stat_macros.h" // For S_IRWXU
+#include "src/__support/CPP/scope.h"
+#include "src/fcntl/fcntl.h"
#include "src/fcntl/open.h"
#include "src/stdio/fclose.h"
#include "src/stdio/fgets.h"
@@ -84,3 +86,33 @@ TEST_F(LlvmLibcStdioFdopenTest, InvalidMode) {
LIBC_NAMESPACE::close(fd);
ASSERT_ERRNO_SUCCESS();
}
+
+TEST_F(LlvmLibcStdioFdopenTest, CloseOnExecPreservedWithoutModifier) {
+ using LIBC_NAMESPACE::testing::ErrnoSetterMatcher::Succeeds;
+ int fd = LIBC_NAMESPACE::open("/dev/null", O_WRONLY | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+ FILE *file = LIBC_NAMESPACE::fdopen(fd, "w");
+ LIBC_NAMESPACE::cpp::scope_exit close_file([&] {
+ if (file != nullptr)
+ EXPECT_THAT(LIBC_NAMESPACE::fclose(file), Succeeds(0));
+ else
+ EXPECT_THAT(LIBC_NAMESPACE::close(fd), Succeeds(0));
+ });
+ ASSERT_NE(file, nullptr);
+ EXPECT_THAT(LIBC_NAMESPACE::fcntl(fd, F_GETFD), Succeeds(FD_CLOEXEC));
+}
+
+TEST_F(LlvmLibcStdioFdopenTest, CloseOnExecEnabledByModifier) {
+ using LIBC_NAMESPACE::testing::ErrnoSetterMatcher::Succeeds;
+ int fd = LIBC_NAMESPACE::open("/dev/null", O_WRONLY);
+ ASSERT_GE(fd, 0);
+ FILE *file = LIBC_NAMESPACE::fdopen(fd, "we");
+ LIBC_NAMESPACE::cpp::scope_exit close_file([&] {
+ if (file != nullptr)
+ EXPECT_THAT(LIBC_NAMESPACE::fclose(file), Succeeds(0));
+ else
+ EXPECT_THAT(LIBC_NAMESPACE::close(fd), Succeeds(0));
+ });
+ ASSERT_NE(file, nullptr);
+ EXPECT_THAT(LIBC_NAMESPACE::fcntl(fd, F_GETFD), Succeeds(FD_CLOEXEC));
+}
diff --git a/libc/test/src/stdio/fopen_test.cpp b/libc/test/src/stdio/fopen_test.cpp
index 249ef1356ab71..38cc6498e728b 100644
--- a/libc/test/src/stdio/fopen_test.cpp
+++ b/libc/test/src/stdio/fopen_test.cpp
@@ -8,6 +8,7 @@
#include "src/__support/CPP/scope.h"
#include "src/__support/File/file.h"
+#include "src/__support/macros/properties/os.h"
#include "src/stdio/fclose.h"
#include "src/stdio/fopen.h"
#include "src/stdio/fread.h"
@@ -15,8 +16,22 @@
#include "test/UnitTest/Test.h"
+#ifdef LIBC_TARGET_OS_IS_LINUX
+#include "hdr/fcntl_macros.h"
+#include "src/fcntl/fcntl.h"
+#include "src/stdio/fileno.h"
+#include "src/stdio/remove.h"
+#include "test/UnitTest/ErrnoCheckingTest.h"
+#include "test/UnitTest/ErrnoSetterMatcher.h"
+#endif
+
using LIBC_NAMESPACE::cpp::scope_exit;
+#ifdef LIBC_TARGET_OS_IS_LINUX
+using LIBC_NAMESPACE::testing::ErrnoSetterMatcher::Fails;
+using LIBC_NAMESPACE::testing::ErrnoSetterMatcher::Succeeds;
+#endif
+
TEST(LlvmLibcFOpenTest, PrintToFile) {
size_t result;
@@ -42,3 +57,78 @@ TEST(LlvmLibcFOpenTest, PrintToFile) {
ASSERT_STREQ(data, STRING);
}
}
+
+#ifdef LIBC_TARGET_OS_IS_LINUX
+class LlvmLibcFOpenModeTest
+ : public LIBC_NAMESPACE::testing::ErrnoCheckingTest {
+protected:
+ void check_exclusive_create(const char *mode) {
+ const auto FILENAME = libc_make_test_file_path("fopen_exclusive.test");
+ constexpr char CONTENT[] = "Preserve this content";
+
+ // Remove a file left by an interrupted test run.
+ LIBC_NAMESPACE::remove(FILENAME);
+ libc_errno = 0;
+ FILE *file = LIBC_NAMESPACE::fopen(FILENAME, mode);
+ ASSERT_NE(file, nullptr);
+ scope_exit remove_file(
+ [&] { EXPECT_THAT(LIBC_NAMESPACE::remove(FILENAME), Succeeds(0)); });
+ {
+ scope_exit close_file(
+ [&] { EXPECT_THAT(LIBC_NAMESPACE::fclose(file), Succeeds(0)); });
+ ASSERT_EQ(LIBC_NAMESPACE::fwrite(CONTENT, 1, sizeof(CONTENT) - 1, file),
+ sizeof(CONTENT) - 1);
+ }
+
+ FILE *existing = LIBC_NAMESPACE::fopen(FILENAME, mode);
+ EXPECT_THAT(existing, Fails(EEXIST, static_cast<void *>(nullptr)));
+ if (existing != nullptr)
+ EXPECT_THAT(LIBC_NAMESPACE::fclose(existing), Succeeds(0));
+
+ file = LIBC_NAMESPACE::fopen(FILENAME, "r");
+ ASSERT_NE(file, nullptr);
+ scope_exit close_file(
+ [&] { EXPECT_THAT(LIBC_NAMESPACE::fclose(file), Succeeds(0)); });
+ char buffer[sizeof(CONTENT)] = {};
+ ASSERT_EQ(LIBC_NAMESPACE::fread(buffer, 1, sizeof(buffer) - 1, file),
+ sizeof(CONTENT) - 1);
+ EXPECT_STREQ(buffer, CONTENT);
+ }
+};
+
+TEST_F(LlvmLibcFOpenModeTest, ExclusiveWrite) { check_exclusive_create("wx"); }
+
+TEST_F(LlvmLibcFOpenModeTest, ExclusiveAppend) { check_exclusive_create("ax"); }
+
+TEST_F(LlvmLibcFOpenModeTest, CloseOnExec) {
+ {
+ FILE *file = LIBC_NAMESPACE::fopen("/dev/null", "w");
+ ASSERT_NE(file, nullptr);
+ scope_exit close_file(
+ [&] { EXPECT_THAT(LIBC_NAMESPACE::fclose(file), Succeeds(0)); });
+ EXPECT_THAT(LIBC_NAMESPACE::fcntl(LIBC_NAMESPACE::fileno(file), F_GETFD),
+ Succeeds(0));
+ }
+ {
+ FILE *file = LIBC_NAMESPACE::fopen("/dev/null", "we");
+ ASSERT_NE(file, nullptr);
+ scope_exit close_file(
+ [&] { EXPECT_THAT(LIBC_NAMESPACE::fclose(file), Succeeds(0)); });
+ EXPECT_THAT(LIBC_NAMESPACE::fcntl(LIBC_NAMESPACE::fileno(file), F_GETFD),
+ Succeeds(FD_CLOEXEC));
+ }
+}
+
+TEST_F(LlvmLibcFOpenModeTest, ReadIgnoresExclusiveModifier) {
+ const auto FILENAME = libc_make_test_file_path("fopen_read_exclusive.test");
+ FILE *file = LIBC_NAMESPACE::fopen(FILENAME, "w");
+ ASSERT_NE(file, nullptr);
+ scope_exit remove_file(
+ [&] { EXPECT_THAT(LIBC_NAMESPACE::remove(FILENAME), Succeeds(0)); });
+ ASSERT_THAT(LIBC_NAMESPACE::fclose(file), Succeeds(0));
+
+ file = LIBC_NAMESPACE::fopen(FILENAME, "rx");
+ ASSERT_NE(file, nullptr);
+ EXPECT_THAT(LIBC_NAMESPACE::fclose(file), Succeeds(0));
+}
+#endif
diff --git a/libc/test/src/stdio/freopen_test.cpp b/libc/test/src/stdio/freopen_test.cpp
index 6f824c841b2d7..8de6171cc323e 100644
--- a/libc/test/src/stdio/freopen_test.cpp
+++ b/libc/test/src/stdio/freopen_test.cpp
@@ -11,6 +11,7 @@
///
//===----------------------------------------------------------------------===//
+#include "src/__support/CPP/scope.h"
#include "src/fcntl/fcntl.h"
#include "src/stdio/clearerr.h"
#include "src/stdio/fclose.h"
@@ -88,6 +89,34 @@ TEST_F(LlvmLibcFreopenTest, ReopenFile) {
verify_file_content(FILENAME_B, CONTENT_B);
}
+#ifdef LIBC_TARGET_OS_IS_LINUX
+TEST_F(LlvmLibcFreopenTest, CloseOnExec) {
+ FILE *file = LIBC_NAMESPACE::fopen("/dev/null", "w");
+ ASSERT_NE(file, nullptr);
+ LIBC_NAMESPACE::cpp::scope_exit close_file(
+ [&] { EXPECT_THAT(LIBC_NAMESPACE::fclose(file), Succeeds(0)); });
+ ASSERT_THAT(LIBC_NAMESPACE::fcntl(LIBC_NAMESPACE::fileno(file), F_GETFD),
+ Succeeds(0));
+
+ ASSERT_EQ(LIBC_NAMESPACE::freopen("/dev/null", "we", file), file);
+ EXPECT_THAT(LIBC_NAMESPACE::fcntl(LIBC_NAMESPACE::fileno(file), F_GETFD),
+ Succeeds(FD_CLOEXEC));
+}
+
+TEST_F(LlvmLibcFreopenTest, ClearCloseOnExec) {
+ FILE *file = LIBC_NAMESPACE::fopen("/dev/null", "we");
+ ASSERT_NE(file, nullptr);
+ LIBC_NAMESPACE::cpp::scope_exit close_file(
+ [&] { EXPECT_THAT(LIBC_NAMESPACE::fclose(file), Succeeds(0)); });
+ ASSERT_THAT(LIBC_NAMESPACE::fcntl(LIBC_NAMESPACE::fileno(file), F_GETFD),
+ Succeeds(FD_CLOEXEC));
+
+ ASSERT_EQ(LIBC_NAMESPACE::freopen("/dev/null", "w", file), file);
+ EXPECT_THAT(LIBC_NAMESPACE::fcntl(LIBC_NAMESPACE::fileno(file), F_GETFD),
+ Succeeds(0));
+}
+#endif
+
TEST_F(LlvmLibcFreopenTest, NullFilenameModeChange) {
const auto FILENAME = libc_make_test_file_path("freopen_null_filename.test");
>From fc314937a286213bc459b876fdca47abc811b3eb Mon Sep 17 00:00:00 2001
From: Jihyeon Jeong <jh.jeong129 at gmail.com>
Date: Thu, 17 Sep 2026 14:55:17 +0000
Subject: [PATCH 2/2] [libc] Prevent temporary fd leaks across exec
---
libc/src/__support/File/linux/file.cpp | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/libc/src/__support/File/linux/file.cpp b/libc/src/__support/File/linux/file.cpp
index 29d177f36328d..1d360c15bfa83 100644
--- a/libc/src/__support/File/linux/file.cpp
+++ b/libc/src/__support/File/linux/file.cpp
@@ -217,6 +217,10 @@ int LinuxFile::reopen_unlocked(const char *path, const char *mode) {
int open_flags = map_c_mode_flags_to_linux_open_flags(file_mode);
+ // Prevent the temporary descriptor from being inherited across exec.
+ if (old_fd >= 0)
+ open_flags |= LinuxFileFlags::CLOSE_ON_EXEC;
+
ErrorOr<int> new_fd =
linux_syscalls::open(path, open_flags, LinuxFileFlags::OPEN_MODE);
More information about the libc-commits
mailing list