[libc-commits] [libc] [llvm] [libc] Files now flush on exit (PR #230215)

Michael Jones via libc-commits libc-commits at lists.llvm.org
Mon Oct 12 11:53:46 PDT 2026


https://github.com/michaelrj-google updated https://github.com/llvm/llvm-project/pull/230215

>From 2b3ffe41b55e66fbe350d769d503341ac1539bad Mon Sep 17 00:00:00 2001
From: Michael Jones <michaelrj at google.com>
Date: Thu, 8 Oct 2026 17:04:06 +0000
Subject: [PATCH 1/3] [libc] Files now flush on exit

The C standard specifies that when the `exit` function is called, all
open files are flushed. This PR refactors fflush to be an internal
function so that it can be called on exit.

Fixes: #219219
Assisted-by: Automated tooling, human reviewed
---
 libc/config/config.json                       |   6 +
 libc/src/stdio/CMakeLists.txt                 |  39 +++++-
 libc/src/stdio/baremetal/CMakeLists.txt       |  11 +-
 libc/src/stdio/baremetal/fflush.cpp           |  22 ----
 libc/src/stdio/baremetal/fflush_internal.cpp  |  34 +++++
 libc/src/stdio/fflush.cpp                     |  36 ++++++
 libc/src/stdio/fflush_internal.h              |  30 +++++
 libc/src/stdio/generic/CMakeLists.txt         |  39 +++---
 .../{fflush.cpp => fflush_internal.cpp}       |  44 +++----
 libc/src/stdio/gpu/CMakeLists.txt             |  11 +-
 .../gpu/{fflush.cpp => fflush_internal.cpp}   |  22 +++-
 libc/src/stdlib/CMakeLists.txt                |   7 ++
 libc/src/stdlib/exit.cpp                      |  15 ++-
 libc/test/src/stdlib/CMakeLists.txt           |  29 ++++-
 libc/test/src/stdlib/exit_test.cpp            | 119 +++++++++++++++++-
 .../llvm-project-overlay/libc/BUILD.bazel     |  27 +++-
 .../libc/libc_configure_options.bzl           |   3 +
 17 files changed, 407 insertions(+), 87 deletions(-)
 delete mode 100644 libc/src/stdio/baremetal/fflush.cpp
 create mode 100644 libc/src/stdio/baremetal/fflush_internal.cpp
 create mode 100644 libc/src/stdio/fflush.cpp
 create mode 100644 libc/src/stdio/fflush_internal.h
 rename libc/src/stdio/generic/{fflush.cpp => fflush_internal.cpp} (69%)
 rename libc/src/stdio/gpu/{fflush.cpp => fflush_internal.cpp} (53%)

diff --git a/libc/config/config.json b/libc/config/config.json
index 604b03c9b5688..6f611d60ff727 100644
--- a/libc/config/config.json
+++ b/libc/config/config.json
@@ -165,6 +165,12 @@
       "doc": "Configures sorting algorithm for qsort and qsort_r. Values accepted are LIBC_QSORT_QUICK_SORT, LIBC_QSORT_HEAP_SORT."
     }
   },
+  "stdlib": {
+    "LIBC_CONF_EXIT_FLUSH_STREAMS": {
+      "value": true,
+      "doc": "Flush all open stdio streams when exit is called, as required by the C standard. Can be disabled in configurations without stdio streams (e.g. baremetal without files) so that exit does not depend on stdio."
+    }
+  },
   "setjmp": {
     "LIBC_CONF_SETJMP_AARCH64_RESTORE_PLATFORM_REGISTER": {
       "value": true,
diff --git a/libc/src/stdio/CMakeLists.txt b/libc/src/stdio/CMakeLists.txt
index a9dd34a79441e..c4838b77a039a 100644
--- a/libc/src/stdio/CMakeLists.txt
+++ b/libc/src/stdio/CMakeLists.txt
@@ -21,6 +21,11 @@ endfunction()
 if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${LIBC_TARGET_OS})
   add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/${LIBC_TARGET_OS})
 endif()
+
+add_stdio_entrypoint_object(stdin)
+add_stdio_entrypoint_object(stdout)
+add_stdio_entrypoint_object(stderr)
+
 add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/generic)
 
 if(NOT LLVM_LIBC_FULL_BUILD)
@@ -28,6 +33,36 @@ if(NOT LLVM_LIBC_FULL_BUILD)
   libc_add_definition(use_system_file "LIBC_COPT_STDIO_USE_SYSTEM_FILE")
 endif()
 
+if(TARGET libc.src.stdio.${LIBC_TARGET_OS}.fflush_internal)
+  add_object_library(
+    fflush_internal
+    ALIAS
+    DEPENDS
+      .${LIBC_TARGET_OS}.fflush_internal
+  )
+elseif(TARGET libc.src.stdio.generic.fflush_internal)
+  add_object_library(
+    fflush_internal
+    ALIAS
+    DEPENDS
+      .generic.fflush_internal
+  )
+endif()
+
+add_entrypoint_object(
+  fflush
+  SRCS
+    fflush.cpp
+  HDRS
+    fflush.h
+  DEPENDS
+    libc.hdr.stdio_macros
+    libc.hdr.types.FILE
+    libc.src.__support.error_or
+    libc.src.__support.libc_errno
+    .fflush_internal
+)
+
 add_entrypoint_object(
   flockfile
   SRCS
@@ -310,7 +345,6 @@ add_stdio_entrypoint_object(ftell)
 add_stdio_entrypoint_object(fseeko)
 add_stdio_entrypoint_object(fileno)
 add_stdio_entrypoint_object(ftello)
-add_stdio_entrypoint_object(fflush)
 add_stdio_entrypoint_object(clearerr)
 add_stdio_entrypoint_object(clearerr_unlocked)
 add_stdio_entrypoint_object(fopen)
@@ -338,9 +372,6 @@ add_stdio_entrypoint_object(fgets)
 add_stdio_entrypoint_object(ungetc)
 add_stdio_entrypoint_object(scanf)
 add_stdio_entrypoint_object(fscanf)
-add_stdio_entrypoint_object(stdin)
-add_stdio_entrypoint_object(stdout)
-add_stdio_entrypoint_object(stderr)
 add_stdio_entrypoint_object(vprintf)
 add_stdio_entrypoint_object(vfprintf)
 add_stdio_entrypoint_object(vscanf)
diff --git a/libc/src/stdio/baremetal/CMakeLists.txt b/libc/src/stdio/baremetal/CMakeLists.txt
index 844d275240b4d..4741b6d620e36 100644
--- a/libc/src/stdio/baremetal/CMakeLists.txt
+++ b/libc/src/stdio/baremetal/CMakeLists.txt
@@ -151,14 +151,15 @@ add_entrypoint_object(
     libc.src.stdio.baremetal.fgetc
 )
 
-add_entrypoint_object(
-  fflush
+add_object_library(
+  fflush_internal
   SRCS
-    fflush.cpp
+    fflush_internal.cpp
   HDRS
-    ../fflush.h
+    ../fflush_internal.h
   DEPENDS
-    .file_internal
+    libc.hdr.types.FILE
+    libc.src.__support.error_or
 )
 
 add_entrypoint_object(
diff --git a/libc/src/stdio/baremetal/fflush.cpp b/libc/src/stdio/baremetal/fflush.cpp
deleted file mode 100644
index 4a39c25ae11c9..0000000000000
--- a/libc/src/stdio/baremetal/fflush.cpp
+++ /dev/null
@@ -1,22 +0,0 @@
-//===-- Implementation of fflush for baremetal -----------------*- C++ -*-===//
-//
-// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
-// See https://llvm.org/LICENSE.txt for license information.
-// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-//
-//===----------------------------------------------------------------------===//
-
-#include "src/stdio/fflush.h"
-
-#include "src/__support/common.h"
-
-namespace LIBC_NAMESPACE_DECL {
-
-// Baremetal uses unbuffered I/O, so there is nothing to flush.
-LLVM_LIBC_FUNCTION(int, fflush, (::FILE * stream)) {
-  (void)stream;
-  // TODO: Shall we have an embedding API for fflush?
-  return 0;
-}
-
-} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/stdio/baremetal/fflush_internal.cpp b/libc/src/stdio/baremetal/fflush_internal.cpp
new file mode 100644
index 0000000000000..6f2e578e8cc7f
--- /dev/null
+++ b/libc/src/stdio/baremetal/fflush_internal.cpp
@@ -0,0 +1,34 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Baremetal implementation of the internal flushing helpers.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/stdio/fflush_internal.h"
+
+#include "hdr/types/FILE.h"
+#include "src/__support/error_or.h"
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+namespace internal {
+
+// Baremetal uses unbuffered I/O, so there is nothing to flush.
+// TODO: Shall we have an embedding API for fflush?
+
+ErrorOr<int> flush_stream(::FILE *stream) {
+  (void)stream;
+  return 0;
+}
+
+ErrorOr<int> flush_all_streams() { return 0; }
+
+} // namespace internal
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/stdio/fflush.cpp b/libc/src/stdio/fflush.cpp
new file mode 100644
index 0000000000000..4ee86b8acb55e
--- /dev/null
+++ b/libc/src/stdio/fflush.cpp
@@ -0,0 +1,36 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Implementation of fflush.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/stdio/fflush.h"
+
+#include "hdr/stdio_macros.h"
+#include "hdr/types/FILE.h"
+#include "src/__support/common.h"
+#include "src/__support/error_or.h"
+#include "src/__support/libc_errno.h"
+#include "src/__support/macros/config.h"
+#include "src/stdio/fflush_internal.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+LLVM_LIBC_FUNCTION(int, fflush, (::FILE * stream)) {
+  ErrorOr<int> result = stream == nullptr ? internal::flush_all_streams()
+                                          : internal::flush_stream(stream);
+  if (!result.has_value()) {
+    libc_errno = result.error();
+    return EOF;
+  }
+  return result.value();
+}
+
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/stdio/fflush_internal.h b/libc/src/stdio/fflush_internal.h
new file mode 100644
index 0000000000000..dc6ca6a69c3f4
--- /dev/null
+++ b/libc/src/stdio/fflush_internal.h
@@ -0,0 +1,30 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Internal helpers for flushing streams, shared by fflush and exit.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_STDIO_FFLUSH_INTERNAL_H
+#define LLVM_LIBC_SRC_STDIO_FFLUSH_INTERNAL_H
+
+#include "hdr/types/FILE.h"
+#include "src/__support/error_or.h"
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+namespace internal {
+
+ErrorOr<int> flush_stream(::FILE *stream);
+ErrorOr<int> flush_all_streams();
+
+} // namespace internal
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_STDIO_FFLUSH_INTERNAL_H
diff --git a/libc/src/stdio/generic/CMakeLists.txt b/libc/src/stdio/generic/CMakeLists.txt
index 5f45d15ac8f26..b7df0381b41a8 100644
--- a/libc/src/stdio/generic/CMakeLists.txt
+++ b/libc/src/stdio/generic/CMakeLists.txt
@@ -94,22 +94,29 @@ add_generic_entrypoint_object(
     libc.src.__support.File.platform_file
 )
 
-add_generic_entrypoint_object(
-  fflush
-  SRCS
-    fflush.cpp
-  HDRS
-    ../fflush.h
-  DEPENDS
-    libc.src.errno.errno
-    libc.hdr.types.FILE
-    libc.src.__support.File.file
-    libc.src.__support.File.platform_file
-    libc.src.stdio.stdin
-    libc.src.stdio.stdout
-    libc.src.stdio.stderr
-)
-
+# since this isn't an entrypoint we need to do these checks manually.
+if(NOT TARGET libc.src.stdio.${LIBC_TARGET_OS}.fflush_internal AND
+   TARGET libc.src.__support.File.file AND
+   TARGET libc.src.__support.File.platform_file AND
+   "libc.src.stdio.stdin" IN_LIST TARGET_LLVMLIBC_ENTRYPOINTS AND
+   "libc.src.stdio.stdout" IN_LIST TARGET_LLVMLIBC_ENTRYPOINTS AND
+   "libc.src.stdio.stderr" IN_LIST TARGET_LLVMLIBC_ENTRYPOINTS)
+  add_object_library(
+    fflush_internal
+    SRCS
+      fflush_internal.cpp
+    HDRS
+      ../fflush_internal.h
+    DEPENDS
+      libc.hdr.types.FILE
+      libc.src.__support.error_or
+      libc.src.__support.File.file
+      libc.src.__support.File.platform_file
+      libc.src.stdio.stdin
+      libc.src.stdio.stdout
+      libc.src.stdio.stderr
+  )
+endif()
 
 add_generic_entrypoint_object(
   fseek
diff --git a/libc/src/stdio/generic/fflush.cpp b/libc/src/stdio/generic/fflush_internal.cpp
similarity index 69%
rename from libc/src/stdio/generic/fflush.cpp
rename to libc/src/stdio/generic/fflush_internal.cpp
index 87888770937bf..a7c7ff8b84e0d 100644
--- a/libc/src/stdio/generic/fflush.cpp
+++ b/libc/src/stdio/generic/fflush_internal.cpp
@@ -1,36 +1,37 @@
-//===-- Implementation of fflush ------------------------------------------===//
+//===----------------------------------------------------------------------===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
 //===----------------------------------------------------------------------===//
+///
+/// \file
+/// Generic implementation of the internal flushing helpers.
+///
+//===----------------------------------------------------------------------===//
 
-#include "src/stdio/fflush.h"
+#include "src/stdio/fflush_internal.h"
+
+#include "hdr/types/FILE.h"
 #include "src/__support/File/file.h"
+#include "src/__support/error_or.h"
+#include "src/__support/macros/config.h"
 #include "src/stdio/stderr.h"
 #include "src/stdio/stdin.h"
 #include "src/stdio/stdout.h"
 
-#include "hdr/types/FILE.h"
-#include "src/__support/libc_errno.h"
-#include "src/__support/macros/config.h"
-
 namespace LIBC_NAMESPACE_DECL {
+namespace internal {
 
-LLVM_LIBC_FUNCTION(int, fflush, (::FILE * stream)) {
-  // If a non-null stream is specified, we only flush that single stream.
-  if (stream != nullptr) {
-    int result = reinterpret_cast<File *>(stream)->flush();
-    if (result != 0) {
-      libc_errno = result;
-      return EOF;
-    }
-    return 0;
-  }
+ErrorOr<int> flush_stream(::FILE *stream) {
+  int result = reinterpret_cast<File *>(stream)->flush();
+  if (result != 0)
+    return Error(result);
+  return 0;
+}
 
-  // If the stream is null, we flush all open streams as per C and POSIX
-  // requirements.
+ErrorOr<int> flush_all_streams() {
   int total_error = 0;
 
   // We explicitly flush the standard streams as they may not be part of the
@@ -56,11 +57,10 @@ LLVM_LIBC_FUNCTION(int, fflush, (::FILE * stream)) {
   }
   File::unlock_list();
 
-  if (total_error != 0) {
-    libc_errno = total_error;
-    return EOF;
-  }
+  if (total_error != 0)
+    return Error(total_error);
   return 0;
 }
 
+} // namespace internal
 } // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/stdio/gpu/CMakeLists.txt b/libc/src/stdio/gpu/CMakeLists.txt
index b55b6d3ee3cbe..c264f932bb40e 100644
--- a/libc/src/stdio/gpu/CMakeLists.txt
+++ b/libc/src/stdio/gpu/CMakeLists.txt
@@ -100,14 +100,17 @@ add_entrypoint_object(
     .gpu_file
 )
 
-add_entrypoint_object(
-  fflush
+add_object_library(
+  fflush_internal
   SRCS
-    fflush.cpp
+    fflush_internal.cpp
   HDRS
-    ../fflush.h
+    ../fflush_internal.h
   DEPENDS
+    libc.hdr.stdint_proxy
     libc.hdr.types.FILE
+    libc.src.__support.RPC.rpc_client
+    libc.src.__support.error_or
     .gpu_file
 )
 
diff --git a/libc/src/stdio/gpu/fflush.cpp b/libc/src/stdio/gpu/fflush_internal.cpp
similarity index 53%
rename from libc/src/stdio/gpu/fflush.cpp
rename to libc/src/stdio/gpu/fflush_internal.cpp
index 3032554cd11c0..993e5c99e4bab 100644
--- a/libc/src/stdio/gpu/fflush.cpp
+++ b/libc/src/stdio/gpu/fflush_internal.cpp
@@ -1,20 +1,29 @@
-//===-- Implementation of fflush ------------------------------------------===//
+//===----------------------------------------------------------------------===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
 //===----------------------------------------------------------------------===//
+///
+/// \file
+/// GPU implementation of the internal flushing helpers.
+///
+//===----------------------------------------------------------------------===//
 
-#include "src/stdio/fflush.h"
+#include "src/stdio/fflush_internal.h"
 
 #include "file.h"
+#include "hdr/stdint_proxy.h"
 #include "hdr/types/FILE.h"
-#include "src/__support/common.h"
+#include "src/__support/RPC/rpc_client.h"
+#include "src/__support/error_or.h"
+#include "src/__support/macros/config.h"
 
 namespace LIBC_NAMESPACE_DECL {
+namespace internal {
 
-LLVM_LIBC_FUNCTION(int, fflush, (::FILE * stream)) {
+static int flush_on_host(::FILE *stream) {
   int ret;
   rpc::Client::Port port = rpc::client.open<LIBC_FFLUSH>();
   port.send_and_recv(
@@ -27,4 +36,9 @@ LLVM_LIBC_FUNCTION(int, fflush, (::FILE * stream)) {
   return ret;
 }
 
+ErrorOr<int> flush_stream(::FILE *stream) { return flush_on_host(stream); }
+
+ErrorOr<int> flush_all_streams() { return flush_on_host(nullptr); }
+
+} // namespace internal
 } // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/stdlib/CMakeLists.txt b/libc/src/stdlib/CMakeLists.txt
index 28f93be03d92b..5ae809409de0f 100644
--- a/libc/src/stdlib/CMakeLists.txt
+++ b/libc/src/stdlib/CMakeLists.txt
@@ -850,6 +850,13 @@ if(TARGET libc.src.__support.threads.thread)
   libc_add_definition(exit_compile_options "LIBC_COPT_SUPPORT_THREADS")
 endif()
 
+if(LIBC_CONF_EXIT_FLUSH_STREAMS AND TARGET libc.src.stdio.fflush_internal)
+  list(APPEND exit_deps
+    libc.src.stdio.fflush_internal
+  )
+  libc_add_definition(exit_compile_options "LIBC_COPT_EXIT_FLUSH_STREAMS")
+endif()
+
 add_entrypoint_object(
   exit
   SRCS
diff --git a/libc/src/stdlib/exit.cpp b/libc/src/stdlib/exit.cpp
index ab43a52b04b9f..a479019e20e11 100644
--- a/libc/src/stdlib/exit.cpp
+++ b/libc/src/stdlib/exit.cpp
@@ -1,10 +1,15 @@
-//===-- Implementation of exit --------------------------------------------===//
+//===----------------------------------------------------------------------===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
 //===----------------------------------------------------------------------===//
+///
+/// \file
+/// Implementation of exit.
+///
+//===----------------------------------------------------------------------===//
 
 #include "src/stdlib/exit.h"
 #include "src/__support/OSUtil/exit.h"
@@ -15,6 +20,10 @@
 #include "src/__support/threads/thread.h"
 #endif
 
+#ifdef LIBC_COPT_EXIT_FLUSH_STREAMS
+#include "src/stdio/fflush_internal.h"
+#endif
+
 namespace LIBC_NAMESPACE_DECL {
 
 extern "C" void __cxa_finalize(void *);
@@ -26,6 +35,10 @@ extern "C" void __cxa_finalize(void *);
   internal::call_atexit_callbacks();
 #endif
   __cxa_finalize(nullptr);
+#ifdef LIBC_COPT_EXIT_FLUSH_STREAMS
+  // C23 7.24.4.4: streams are flushed after the atexit handlers have run.
+  internal::flush_all_streams();
+#endif
   internal::exit(status);
 }
 
diff --git a/libc/test/src/stdlib/CMakeLists.txt b/libc/test/src/stdlib/CMakeLists.txt
index bc705754b64a5..4b522cb3be59b 100644
--- a/libc/test/src/stdlib/CMakeLists.txt
+++ b/libc/test/src/stdlib/CMakeLists.txt
@@ -582,15 +582,40 @@ if(LLVM_LIBC_FULL_BUILD AND LIBC_TEST_SUBPROCESS_TESTS)
       libc.src.stdlib._Exit
   )
 
+  set(exit_test_copts "")
+  set(exit_test_deps
+    libc.src.__support.macros.config
+    libc.src.stdlib.exit
+  )
+  if(LIBC_CONF_EXIT_FLUSH_STREAMS AND TARGET libc.src.stdio.fflush_internal)
+    libc_add_definition(exit_test_copts "LIBC_COPT_EXIT_FLUSH_STREAMS")
+    list(APPEND exit_test_deps
+      libc.hdr.types.FILE
+      libc.hdr.types.size_t
+      libc.src.__support.CPP.scope
+      libc.src.stdio.fclose
+      libc.src.stdio.fopen
+      libc.src.stdio.fputs
+      libc.src.stdio.fread
+      libc.src.stdio.freopen
+      libc.src.stdio.remove
+      libc.src.stdio.stdout
+      libc.src.stdlib.atexit
+      libc.test.UnitTest.ErrnoCheckingTest
+      libc.test.UnitTest.ErrnoSetterMatcher
+    )
+  endif()
+
   add_libc_test(
     exit_test
     SUITE
       libc-stdlib-tests
     SRCS
       exit_test.cpp
+    COMPILE_OPTIONS
+      ${exit_test_copts}
     DEPENDS
-      libc.src.stdlib._Exit
-      libc.src.stdlib.exit
+      ${exit_test_deps}
   )
 
   add_libc_test(
diff --git a/libc/test/src/stdlib/exit_test.cpp b/libc/test/src/stdlib/exit_test.cpp
index 5c82d8303036a..f9ab3ba38106a 100644
--- a/libc/test/src/stdlib/exit_test.cpp
+++ b/libc/test/src/stdlib/exit_test.cpp
@@ -1,15 +1,132 @@
-//===-- Unittests for exit -----------------------------------------------===//
+//===----------------------------------------------------------------------===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
 //===----------------------------------------------------------------------===//
+///
+/// \file
+/// Unittests for exit.
+///
+//===----------------------------------------------------------------------===//
 
+#include "src/__support/macros/config.h"
 #include "src/stdlib/exit.h"
 #include "test/UnitTest/Test.h"
 
+#ifdef LIBC_COPT_EXIT_FLUSH_STREAMS
+#include "hdr/types/FILE.h"
+#include "hdr/types/size_t.h"
+#include "src/__support/CPP/scope.h"
+#include "src/stdio/fclose.h"
+#include "src/stdio/fopen.h"
+#include "src/stdio/fputs.h"
+#include "src/stdio/fread.h"
+#include "src/stdio/freopen.h"
+#include "src/stdio/remove.h"
+#include "src/stdio/stdout.h"
+#include "src/stdlib/atexit.h"
+#include "test/UnitTest/ErrnoCheckingTest.h"
+#include "test/UnitTest/ErrnoSetterMatcher.h"
+#endif
+
 TEST(LlvmLibcStdlib, exit) {
   EXPECT_EXITS([] { LIBC_NAMESPACE::exit(1); }, 1);
   EXPECT_EXITS([] { LIBC_NAMESPACE::exit(65); }, 65);
 }
+
+#ifdef LIBC_COPT_EXIT_FLUSH_STREAMS
+
+using LIBC_NAMESPACE::testing::ErrnoSetterMatcher::Succeeds;
+
+struct LlvmLibcExitTest : public LIBC_NAMESPACE::testing::ErrnoCheckingTest {
+  static constexpr size_t VERIFY_BUFFER_SIZE = 128;
+
+  void verify_file_content(const char *filepath, const char *expected) {
+    size_t len = 0;
+    while (expected[len] != '\0')
+      ++len;
+    ASSERT_LT(len, VERIFY_BUFFER_SIZE);
+
+    ::FILE *file = LIBC_NAMESPACE::fopen(filepath, "r");
+    ASSERT_FALSE(file == nullptr);
+    char read_buf[VERIFY_BUFFER_SIZE] = {0};
+    ASSERT_THAT(
+        LIBC_NAMESPACE::fread(read_buf, 1, VERIFY_BUFFER_SIZE - 1, file),
+        Succeeds(len));
+    ASSERT_STREQ(read_buf, expected);
+    ASSERT_THAT(LIBC_NAMESPACE::fclose(file), Succeeds(0));
+  }
+};
+
+TEST_F(LlvmLibcExitTest, FlushesOpenStreams) {
+  const auto FILENAME = libc_make_test_file_path("exit_flush.test");
+  const char *fn = FILENAME;
+  constexpr char CONTENT[] = "unflushed data";
+
+  auto test = [=] {
+    ::FILE *file = LIBC_NAMESPACE::fopen(fn, "w");
+    if (file == nullptr)
+      __builtin_trap();
+    if (LIBC_NAMESPACE::fputs(CONTENT, file) < 0)
+      __builtin_trap();
+    // Deliberately neither flushed nor closed: exit has to do it.
+    LIBC_NAMESPACE::exit(0);
+  };
+  EXPECT_EXITS(test, 0);
+  LIBC_NAMESPACE::cpp::scope_exit cleanup(
+      [&] { EXPECT_THAT(LIBC_NAMESPACE::remove(fn), Succeeds(0)); });
+
+  verify_file_content(fn, CONTENT);
+}
+
+// stdout is line buffered and not on the open-file list, so it gets its own
+// test; the content has no newline so that only exit can flush it.
+TEST_F(LlvmLibcExitTest, FlushesStdout) {
+  const auto FILENAME = libc_make_test_file_path("exit_flush_stdout.test");
+  const char *fn = FILENAME;
+  constexpr char CONTENT[] = "unflushed stdout data";
+
+  auto test = [=] {
+    if (LIBC_NAMESPACE::freopen(fn, "w", LIBC_NAMESPACE::stdout) == nullptr)
+      __builtin_trap();
+    if (LIBC_NAMESPACE::fputs(CONTENT, LIBC_NAMESPACE::stdout) < 0)
+      __builtin_trap();
+    LIBC_NAMESPACE::exit(0);
+  };
+  EXPECT_EXITS(test, 0);
+  LIBC_NAMESPACE::cpp::scope_exit cleanup(
+      [&] { EXPECT_THAT(LIBC_NAMESPACE::remove(fn), Succeeds(0)); });
+
+  verify_file_content(fn, CONTENT);
+}
+
+static ::FILE *atexit_file = nullptr;
+
+TEST_F(LlvmLibcExitTest, FlushesAfterAtexitHandlers) {
+  const auto FILENAME = libc_make_test_file_path("exit_flush_atexit.test");
+  const char *fn = FILENAME;
+
+  auto test = [=] {
+    atexit_file = LIBC_NAMESPACE::fopen(fn, "w");
+    if (atexit_file == nullptr)
+      __builtin_trap();
+    int status = LIBC_NAMESPACE::atexit(+[] {
+      if (LIBC_NAMESPACE::fputs("atexit", atexit_file) < 0)
+        __builtin_trap();
+    });
+    if (status != 0)
+      __builtin_trap();
+    if (LIBC_NAMESPACE::fputs("main ", atexit_file) < 0)
+      __builtin_trap();
+    LIBC_NAMESPACE::exit(0);
+  };
+  EXPECT_EXITS(test, 0);
+  LIBC_NAMESPACE::cpp::scope_exit cleanup(
+      [&] { EXPECT_THAT(LIBC_NAMESPACE::remove(fn), Succeeds(0)); });
+
+  verify_file_content(fn, "main atexit");
+}
+
+#endif // LIBC_COPT_EXIT_FLUSH_STREAMS
diff --git a/utils/bazel/llvm-project-overlay/libc/BUILD.bazel b/utils/bazel/llvm-project-overlay/libc/BUILD.bazel
index cbe1c92e9e6be..b0c4e52423d11 100644
--- a/utils/bazel/llvm-project-overlay/libc/BUILD.bazel
+++ b/utils/bazel/llvm-project-overlay/libc/BUILD.bazel
@@ -15543,6 +15543,7 @@ libc_function(
         ":__support_macros_config",
         ":__support_osutil_exit",
         ":__support_threads_thread_headers",
+        ":fflush_internal",
     ],
 )
 
@@ -17482,20 +17483,34 @@ libc_support_library(
     ],
 )
 
+libc_support_library(
+    name = "fflush_internal",
+    srcs = ["src/stdio/generic/fflush_internal.cpp"],
+    hdrs = ["src/stdio/fflush_internal.h"],
+    deps = [
+        ":__support_error_or",
+        ":__support_file_file",
+        ":__support_file_platform_file",
+        ":__support_macros_config",
+        ":stderr",
+        ":stdin",
+        ":stdout",
+        ":types_FILE",
+    ],
+)
+
 libc_function(
     name = "fflush",
-    srcs = ["src/stdio/generic/fflush.cpp"],
+    srcs = ["src/stdio/fflush.cpp"],
     hdrs = ["src/stdio/fflush.h"],
     deps = [
         ":__support_common",
-        ":__support_file_file",
-        ":__support_file_platform_file",
+        ":__support_error_or",
         ":__support_libc_errno",
         ":__support_macros_config",
         ":errno",
-        ":stderr",
-        ":stdin",
-        ":stdout",
+        ":fflush_internal",
+        ":hdr_stdio_macros",
         ":types_FILE",
     ],
 )
diff --git a/utils/bazel/llvm-project-overlay/libc/libc_configure_options.bzl b/utils/bazel/llvm-project-overlay/libc/libc_configure_options.bzl
index 9626f49c6e159..092ce461e4937 100644
--- a/utils/bazel/llvm-project-overlay/libc/libc_configure_options.bzl
+++ b/utils/bazel/llvm-project-overlay/libc/libc_configure_options.bzl
@@ -69,6 +69,9 @@ LIBC_CONFIGURE_FULL_BUILD_OPTIONS = [
 
     # Defined for targets which support threads.
     "LIBC_COPT_SUPPORT_THREADS",
+
+    # Documentation in libc/config/config.json (LIBC_CONF_EXIT_FLUSH_STREAMS).
+    "LIBC_COPT_EXIT_FLUSH_STREAMS",
 ]
 
 LIBC_CONFIGURE_OPTIONS = LIBC_CONFIGURE_COMMON_OPTIONS + select({

>From da69302d5925c4be747f3cf7be934d72924bdfaf Mon Sep 17 00:00:00 2001
From: Michael Jones <michaelrj at google.com>
Date: Thu, 8 Oct 2026 21:06:56 +0000
Subject: [PATCH 2/3] address comments

---
 libc/src/stdio/gpu/fflush_internal.cpp | 15 ++++++++++++---
 libc/test/src/stdlib/CMakeLists.txt    |  1 +
 libc/test/src/stdlib/exit_test.cpp     | 20 ++++++++++----------
 3 files changed, 23 insertions(+), 13 deletions(-)

diff --git a/libc/src/stdio/gpu/fflush_internal.cpp b/libc/src/stdio/gpu/fflush_internal.cpp
index 993e5c99e4bab..91fdb53cd472d 100644
--- a/libc/src/stdio/gpu/fflush_internal.cpp
+++ b/libc/src/stdio/gpu/fflush_internal.cpp
@@ -36,9 +36,18 @@ static int flush_on_host(::FILE *stream) {
   return ret;
 }
 
-ErrorOr<int> flush_stream(::FILE *stream) { return flush_on_host(stream); }
-
-ErrorOr<int> flush_all_streams() { return flush_on_host(nullptr); }
+ErrorOr<int> flush_stream(::FILE *stream) {
+  int ret = flush_on_host(stream);
+  if (ret != 0)
+    return Error(ret);
+  return 0;
+}
 
+ErrorOr<int> flush_all_streams() {
+  int ret = flush_on_host(nullptr);
+  if (ret != 0)
+    return Error(ret);
+  return 0;
+}
 } // namespace internal
 } // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/test/src/stdlib/CMakeLists.txt b/libc/test/src/stdlib/CMakeLists.txt
index 4b522cb3be59b..69ec4cf575b57 100644
--- a/libc/test/src/stdlib/CMakeLists.txt
+++ b/libc/test/src/stdlib/CMakeLists.txt
@@ -593,6 +593,7 @@ if(LLVM_LIBC_FULL_BUILD AND LIBC_TEST_SUBPROCESS_TESTS)
       libc.hdr.types.FILE
       libc.hdr.types.size_t
       libc.src.__support.CPP.scope
+      libc.src.__support.CPP.string_view
       libc.src.stdio.fclose
       libc.src.stdio.fopen
       libc.src.stdio.fputs
diff --git a/libc/test/src/stdlib/exit_test.cpp b/libc/test/src/stdlib/exit_test.cpp
index f9ab3ba38106a..34c8da60bce6e 100644
--- a/libc/test/src/stdlib/exit_test.cpp
+++ b/libc/test/src/stdlib/exit_test.cpp
@@ -19,6 +19,7 @@
 #include "hdr/types/FILE.h"
 #include "hdr/types/size_t.h"
 #include "src/__support/CPP/scope.h"
+#include "src/__support/CPP/string_view.h"
 #include "src/stdio/fclose.h"
 #include "src/stdio/fopen.h"
 #include "src/stdio/fputs.h"
@@ -43,20 +44,19 @@ using LIBC_NAMESPACE::testing::ErrnoSetterMatcher::Succeeds;
 struct LlvmLibcExitTest : public LIBC_NAMESPACE::testing::ErrnoCheckingTest {
   static constexpr size_t VERIFY_BUFFER_SIZE = 128;
 
-  void verify_file_content(const char *filepath, const char *expected) {
-    size_t len = 0;
-    while (expected[len] != '\0')
-      ++len;
-    ASSERT_LT(len, VERIFY_BUFFER_SIZE);
+  void verify_file_content(const char *filepath,
+                           LIBC_NAMESPACE::cpp::string_view expected) {
+    ASSERT_LT(expected.size(), VERIFY_BUFFER_SIZE);
 
     ::FILE *file = LIBC_NAMESPACE::fopen(filepath, "r");
-    ASSERT_FALSE(file == nullptr);
-    char read_buf[VERIFY_BUFFER_SIZE] = {0};
+    ASSERT_NE(file, nullptr);
+    LIBC_NAMESPACE::cpp::scope_exit close_file(
+        [&] { EXPECT_THAT(LIBC_NAMESPACE::fclose(file), Succeeds(0)); });
+    char read_buf[VERIFY_BUFFER_SIZE] = {};
     ASSERT_THAT(
         LIBC_NAMESPACE::fread(read_buf, 1, VERIFY_BUFFER_SIZE - 1, file),
-        Succeeds(len));
-    ASSERT_STREQ(read_buf, expected);
-    ASSERT_THAT(LIBC_NAMESPACE::fclose(file), Succeeds(0));
+        Succeeds(expected.size()));
+    ASSERT_EQ(LIBC_NAMESPACE::cpp::string_view(read_buf), expected);
   }
 };
 

>From 32c2279a426c834ca03d66d33f4b8a414c14bc9c Mon Sep 17 00:00:00 2001
From: Michael Jones <michaelrj at google.com>
Date: Mon, 12 Oct 2026 18:53:13 +0000
Subject: [PATCH 3/3] remove unnecessary config option

---
 libc/config/config.json                                     | 6 ------
 libc/src/stdlib/CMakeLists.txt                              | 2 +-
 libc/test/src/stdlib/CMakeLists.txt                         | 2 +-
 .../llvm-project-overlay/libc/libc_configure_options.bzl    | 3 ---
 4 files changed, 2 insertions(+), 11 deletions(-)

diff --git a/libc/config/config.json b/libc/config/config.json
index 6f611d60ff727..604b03c9b5688 100644
--- a/libc/config/config.json
+++ b/libc/config/config.json
@@ -165,12 +165,6 @@
       "doc": "Configures sorting algorithm for qsort and qsort_r. Values accepted are LIBC_QSORT_QUICK_SORT, LIBC_QSORT_HEAP_SORT."
     }
   },
-  "stdlib": {
-    "LIBC_CONF_EXIT_FLUSH_STREAMS": {
-      "value": true,
-      "doc": "Flush all open stdio streams when exit is called, as required by the C standard. Can be disabled in configurations without stdio streams (e.g. baremetal without files) so that exit does not depend on stdio."
-    }
-  },
   "setjmp": {
     "LIBC_CONF_SETJMP_AARCH64_RESTORE_PLATFORM_REGISTER": {
       "value": true,
diff --git a/libc/src/stdlib/CMakeLists.txt b/libc/src/stdlib/CMakeLists.txt
index 5ae809409de0f..382a52204fcca 100644
--- a/libc/src/stdlib/CMakeLists.txt
+++ b/libc/src/stdlib/CMakeLists.txt
@@ -850,7 +850,7 @@ if(TARGET libc.src.__support.threads.thread)
   libc_add_definition(exit_compile_options "LIBC_COPT_SUPPORT_THREADS")
 endif()
 
-if(LIBC_CONF_EXIT_FLUSH_STREAMS AND TARGET libc.src.stdio.fflush_internal)
+if("libc.src.stdio.fflush" IN_LIST TARGET_LLVMLIBC_ENTRYPOINTS)
   list(APPEND exit_deps
     libc.src.stdio.fflush_internal
   )
diff --git a/libc/test/src/stdlib/CMakeLists.txt b/libc/test/src/stdlib/CMakeLists.txt
index 69ec4cf575b57..5a634f8505cb8 100644
--- a/libc/test/src/stdlib/CMakeLists.txt
+++ b/libc/test/src/stdlib/CMakeLists.txt
@@ -587,7 +587,7 @@ if(LLVM_LIBC_FULL_BUILD AND LIBC_TEST_SUBPROCESS_TESTS)
     libc.src.__support.macros.config
     libc.src.stdlib.exit
   )
-  if(LIBC_CONF_EXIT_FLUSH_STREAMS AND TARGET libc.src.stdio.fflush_internal)
+  if("libc.src.stdio.fflush" IN_LIST TARGET_LLVMLIBC_ENTRYPOINTS)
     libc_add_definition(exit_test_copts "LIBC_COPT_EXIT_FLUSH_STREAMS")
     list(APPEND exit_test_deps
       libc.hdr.types.FILE
diff --git a/utils/bazel/llvm-project-overlay/libc/libc_configure_options.bzl b/utils/bazel/llvm-project-overlay/libc/libc_configure_options.bzl
index 092ce461e4937..9626f49c6e159 100644
--- a/utils/bazel/llvm-project-overlay/libc/libc_configure_options.bzl
+++ b/utils/bazel/llvm-project-overlay/libc/libc_configure_options.bzl
@@ -69,9 +69,6 @@ LIBC_CONFIGURE_FULL_BUILD_OPTIONS = [
 
     # Defined for targets which support threads.
     "LIBC_COPT_SUPPORT_THREADS",
-
-    # Documentation in libc/config/config.json (LIBC_CONF_EXIT_FLUSH_STREAMS).
-    "LIBC_COPT_EXIT_FLUSH_STREAMS",
 ]
 
 LIBC_CONFIGURE_OPTIONS = LIBC_CONFIGURE_COMMON_OPTIONS + select({



More information about the libc-commits mailing list