[libc-commits] [libc] [llvm] [libc] implement popen/pclose (PR #232846)

Michael Jones via libc-commits libc-commits at lists.llvm.org
Fri Oct 16 09:59:57 PDT 2026


https://github.com/michaelrj-google created https://github.com/llvm/llvm-project/pull/232846

This is a deceptively complex set of functions. They need to have
their own FILE subclass to track the PID of the child process and a list
of other open files specifically for popen. I also ended up refactoring
`system` since it shares the fork-and-shell mechanism. The POSIX
definition is here: https://pubs.opengroup.org/onlinepubs/9799919799/functions/popen.html

Assisted-by: Automated tooling, human reviewed.


>From abe85752215433ab1e26c59986b7ab5076b4719e Mon Sep 17 00:00:00 2001
From: Michael Jones <michaelrj at google.com>
Date: Fri, 16 Oct 2026 16:56:49 +0000
Subject: [PATCH] [libc] implement popen/pclose

This is a deceptively complex set of functions. They need to have
their own FILE subclass to track the PID of the child process and a list
of other open files specifically for popen. I also ended up refactoring
`system` since it shares the fork-and-shell mechanism. The POSIX
definition is here: https://pubs.opengroup.org/onlinepubs/9799919799/functions/popen.html

Assisted-by: Automated tooling, human reviewed.
---
 libc/config/linux/aarch64/entrypoints.txt     |   2 +
 libc/config/linux/riscv/entrypoints.txt       |   2 +
 libc/config/linux/x86_64/entrypoints.txt      |   2 +
 libc/include/stdio.yaml                       |  13 ++
 libc/src/__support/File/file_mode.h           |   4 +
 libc/src/__support/File/linux/CMakeLists.txt  |  17 ++
 libc/src/__support/File/linux/file.cpp        |  69 ++++----
 libc/src/__support/File/linux/file.h          |  60 ++++++-
 libc/src/__support/File/linux/popen_file.cpp  |  58 +++++++
 libc/src/__support/File/linux/popen_file.h    |  66 ++++++++
 .../src/__support/OSUtil/linux/CMakeLists.txt |  19 +++
 libc/src/__support/OSUtil/linux/shell_fork.h  |  79 +++++++++
 .../linux/syscall_wrappers/CMakeLists.txt     |  13 ++
 .../OSUtil/linux/syscall_wrappers/pipe2.h     |  37 ++++
 libc/src/stdio/CMakeLists.txt                 |  14 ++
 libc/src/stdio/linux/CMakeLists.txt           |  43 +++++
 libc/src/stdio/linux/pclose.cpp               |  53 ++++++
 libc/src/stdio/linux/popen.cpp                | 131 +++++++++++++++
 libc/src/stdio/pclose.h                       |  26 +++
 libc/src/stdio/popen.h                        |  26 +++
 libc/src/stdlib/linux/CMakeLists.txt          |   8 +-
 libc/src/stdlib/linux/system.cpp              |  62 ++-----
 libc/src/unistd/linux/CMakeLists.txt          |   7 +-
 libc/src/unistd/linux/pipe2.cpp               |  20 ++-
 libc/test/src/stdio/CMakeLists.txt            |  23 +++
 libc/test/src/stdio/popen_pclose_test.cpp     | 158 ++++++++++++++++++
 .../llvm-project-overlay/libc/BUILD.bazel     |   1 +
 27 files changed, 906 insertions(+), 107 deletions(-)
 create mode 100644 libc/src/__support/File/linux/popen_file.cpp
 create mode 100644 libc/src/__support/File/linux/popen_file.h
 create mode 100644 libc/src/__support/OSUtil/linux/shell_fork.h
 create mode 100644 libc/src/__support/OSUtil/linux/syscall_wrappers/pipe2.h
 create mode 100644 libc/src/stdio/linux/pclose.cpp
 create mode 100644 libc/src/stdio/linux/popen.cpp
 create mode 100644 libc/src/stdio/pclose.h
 create mode 100644 libc/src/stdio/popen.h
 create mode 100644 libc/test/src/stdio/popen_pclose_test.cpp

diff --git a/libc/config/linux/aarch64/entrypoints.txt b/libc/config/linux/aarch64/entrypoints.txt
index 13130efdf79bb3..3186fbe0f14a8e 100644
--- a/libc/config/linux/aarch64/entrypoints.txt
+++ b/libc/config/linux/aarch64/entrypoints.txt
@@ -1332,7 +1332,9 @@ if(LLVM_LIBC_FULL_BUILD)
     libc.src.stdio.getc_unlocked
     libc.src.stdio.getchar
     libc.src.stdio.getchar_unlocked
+    libc.src.stdio.pclose
     libc.src.stdio.perror
+    libc.src.stdio.popen
     libc.src.stdio.putc
     libc.src.stdio.putchar
     libc.src.stdio.puts
diff --git a/libc/config/linux/riscv/entrypoints.txt b/libc/config/linux/riscv/entrypoints.txt
index e53f61dd43e90a..f9085437e0b2a7 100644
--- a/libc/config/linux/riscv/entrypoints.txt
+++ b/libc/config/linux/riscv/entrypoints.txt
@@ -1513,7 +1513,9 @@ if(LLVM_LIBC_FULL_BUILD)
     libc.src.stdio.getc_unlocked
     libc.src.stdio.getchar
     libc.src.stdio.getchar_unlocked
+    libc.src.stdio.pclose
     libc.src.stdio.perror
+    libc.src.stdio.popen
     libc.src.stdio.putc
     libc.src.stdio.putchar
     libc.src.stdio.puts
diff --git a/libc/config/linux/x86_64/entrypoints.txt b/libc/config/linux/x86_64/entrypoints.txt
index dcfa2ef81338e7..325e4e73c0fb03 100644
--- a/libc/config/linux/x86_64/entrypoints.txt
+++ b/libc/config/linux/x86_64/entrypoints.txt
@@ -1539,7 +1539,9 @@ if(LLVM_LIBC_FULL_BUILD)
     libc.src.stdio.getc_unlocked
     libc.src.stdio.getchar
     libc.src.stdio.getchar_unlocked
+    libc.src.stdio.pclose
     libc.src.stdio.perror
+    libc.src.stdio.popen
     libc.src.stdio.putc
     libc.src.stdio.putchar
     libc.src.stdio.puts
diff --git a/libc/include/stdio.yaml b/libc/include/stdio.yaml
index c910c53d5f3a79..251840d8670a63 100644
--- a/libc/include/stdio.yaml
+++ b/libc/include/stdio.yaml
@@ -329,12 +329,25 @@ functions:
       - posix
     return_type: int
     arguments: []
+  - name: pclose
+    standards:
+      - posix
+    return_type: int
+    arguments:
+      - type: FILE *
   - name: perror
     standards:
       - stdc
     return_type: void
     arguments:
       - type: const char *
+  - name: popen
+    standards:
+      - posix
+    return_type: FILE *
+    arguments:
+      - type: const char *
+      - type: const char *
   - name: printf
     standards:
       - stdc
diff --git a/libc/src/__support/File/file_mode.h b/libc/src/__support/File/file_mode.h
index 8049516da261f0..1c4c8b7b7e3986 100644
--- a/libc/src/__support/File/file_mode.h
+++ b/libc/src/__support/File/file_mode.h
@@ -86,6 +86,10 @@ class FileMode {
 
   constexpr bool is_valid() const { return file_mode_ != 0; }
 
+  constexpr bool operator==(const FileMode &other) const {
+    return file_mode_ == other.file_mode_;
+  }
+
   constexpr bool is_append() const {
     return (file_mode_ & static_cast<Mode>(OpenMode::APPEND)) != 0;
   }
diff --git a/libc/src/__support/File/linux/CMakeLists.txt b/libc/src/__support/File/linux/CMakeLists.txt
index 05efc0f6f61395..57d3d5c025e7b7 100644
--- a/libc/src/__support/File/linux/CMakeLists.txt
+++ b/libc/src/__support/File/linux/CMakeLists.txt
@@ -12,7 +12,9 @@ add_object_library(
     libc.hdr.sys_stat_macros
     libc.hdr.types.FILE
     libc.hdr.types.off_t
+    libc.src.__support.alloc_checker
     libc.src.__support.CPP.new
+    libc.src.__support.CPP.type_traits
     libc.src.__support.OSUtil.linux.syscall_wrappers.close
     libc.src.__support.OSUtil.linux.syscall_wrappers.dup3
     libc.src.__support.OSUtil.linux.syscall_wrappers.fcntl
@@ -25,6 +27,21 @@ add_object_library(
     libc.src.errno.errno
 )
 
+add_object_library(
+  popen_file
+  SRCS
+    popen_file.cpp
+  HDRS
+    popen_file.h
+  DEPENDS
+    .file
+    libc.hdr.types.pid_t
+    libc.src.__support.CPP.new
+    libc.src.__support.File.file
+    libc.src.__support.macros.config
+    libc.src.__support.threads.mutex
+)
+
 add_object_library(
   linux_dir
   SRCS
diff --git a/libc/src/__support/File/linux/file.cpp b/libc/src/__support/File/linux/file.cpp
index 1d360c15bfa834..0bc3ba37a4a91d 100644
--- a/libc/src/__support/File/linux/file.cpp
+++ b/libc/src/__support/File/linux/file.cpp
@@ -1,10 +1,15 @@
-//===--- Implementation of the Linux specialization of File ---------------===//
+//===----------------------------------------------------------------------===//
 //
 // 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 the Linux specialization of File.
+///
+//===----------------------------------------------------------------------===//
 
 #include "file.h"
 #include "file_flags.h"
@@ -21,7 +26,6 @@
 #include "src/__support/OSUtil/linux/syscall_wrappers/open.h"
 #include "src/__support/OSUtil/linux/syscall_wrappers/read.h"
 #include "src/__support/OSUtil/linux/syscall_wrappers/write.h"
-#include "src/__support/alloc-checker.h"
 #include "src/__support/libc_errno.h" // For error macros
 #include "src/__support/macros/config.h"
 
@@ -50,20 +54,22 @@ ErrorOr<off_t> linux_file_seek(File *f, off_t offset, int whence) {
   return linux_syscalls::lseek(lf->get_fd(), offset, whence);
 }
 
+int LinuxFile::close_fd() {
+  if (fd < 0)
+    return EBADF;
+  // Linux closes the file descriptor early in the syscall, so we assume it's
+  // always closed after the call. That means callers should also delete the
+  // file object on error.
+  auto result = linux_syscalls::close(fd);
+  if (!result)
+    return result.error();
+  return 0;
+}
+
 int linux_file_close(File *f) {
   File::remove_file(f);
   auto *lf = reinterpret_cast<LinuxFile *>(f);
-  int retval = 0;
-  if (lf->get_fd() >= 0) {
-    // Linux closes the file descriptor early in the syscall, so we assume it's
-    // always closed after the call. That means we should also delete the
-    // LinuxFile on error.
-    auto result = linux_syscalls::close(lf->get_fd());
-    if (!result)
-      retval = result.error();
-  } else {
-    retval = EBADF;
-  }
+  int retval = lf->close_fd();
   delete lf;
   return retval;
 }
@@ -111,20 +117,11 @@ ErrorOr<File *> openfile(const char *path, const char *mode) {
   if (!fd)
     return Error(fd.error());
 
-  uint8_t *buffer;
-  {
-    AllocChecker ac;
-    buffer = new (ac) uint8_t[File::DEFAULT_BUFFER_SIZE];
-    if (!ac)
-      return Error(ENOMEM);
-  }
-  AllocChecker ac;
-  auto *file = new (ac) LinuxFile(fd.value(), buffer, File::DEFAULT_BUFFER_SIZE,
-                                  _IOFBF, true, file_mode);
-  if (!ac)
-    return Error(ENOMEM);
-  File::add_file(file);
-  return file;
+  ErrorOr<LinuxFile *> file = LinuxFile::create_buffered(fd.value(), file_mode);
+  if (!file)
+    return Error(file.error());
+  File::add_file(file.value());
+  return file.value();
 }
 
 static ErrorOr<int> set_close_on_exec(int fd) {
@@ -172,20 +169,10 @@ ErrorOr<LinuxFile *> create_file_from_fd(int fd, const char *mode) {
     }
   }
 
-  uint8_t *buffer;
-  {
-    AllocChecker ac;
-    buffer = new (ac) uint8_t[File::DEFAULT_BUFFER_SIZE];
-    if (!ac) {
-      return Error(ENOMEM);
-    }
-  }
-  AllocChecker ac;
-  auto *file = new (ac)
-      LinuxFile(fd, buffer, File::DEFAULT_BUFFER_SIZE, _IOFBF, true, file_mode);
-  if (!ac) {
-    return Error(ENOMEM);
-  }
+  ErrorOr<LinuxFile *> new_file = LinuxFile::create_buffered(fd, file_mode);
+  if (!new_file)
+    return Error(new_file.error());
+  LinuxFile *file = new_file.value();
   File::add_file(file);
   if (do_seek) {
     result = file->seek(0, SEEK_END);
diff --git a/libc/src/__support/File/linux/file.h b/libc/src/__support/File/linux/file.h
index 539957b8b22c8c..d1ea9da9294f4c 100644
--- a/libc/src/__support/File/linux/file.h
+++ b/libc/src/__support/File/linux/file.h
@@ -1,14 +1,27 @@
-//===--- Linux specialization of the File data structure ------------------===//
+//===----------------------------------------------------------------------===//
 //
 // 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
+/// Linux specialization of the File data structure.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC___SUPPORT_FILE_LINUX_FILE_H
+#define LLVM_LIBC_SRC___SUPPORT_FILE_LINUX_FILE_H
 
+#include "hdr/stdio_macros.h"
 #include "hdr/types/off_t.h"
+#include "src/__support/CPP/new.h"
+#include "src/__support/CPP/type_traits.h"
 #include "src/__support/File/file.h"
 #include "src/__support/File/file_mode.h"
+#include "src/__support/alloc-checker.h"
+#include "src/__support/error_or.h"
 #include "src/__support/macros/config.h"
 
 namespace LIBC_NAMESPACE_DECL {
@@ -21,12 +34,49 @@ int linux_file_close(File *);
 class LinuxFile : public File {
   int fd;
 
+  friend int linux_file_close(File *);
+
+protected:
+  // For subclasses which need to do extra cleanup when the file is closed.
+  constexpr LinuxFile(int file_descriptor, uint8_t *buffer, size_t buffer_size,
+                      int buffer_mode, bool owned, FileMode mode,
+                      CloseFunc *close_func)
+      : File(&linux_file_write, &linux_file_read, &linux_file_seek, close_func,
+             buffer, buffer_size, buffer_mode, owned, mode),
+        fd(file_descriptor) {}
+
+  // Closes the underlying file descriptor. Returns 0 on success, or an error
+  // code. This does not free the file object.
+  int close_fd();
+
 public:
   constexpr LinuxFile(int file_descriptor, uint8_t *buffer, size_t buffer_size,
                       int buffer_mode, bool owned, FileMode mode)
-      : File(&linux_file_write, &linux_file_read, &linux_file_seek,
-             &linux_file_close, buffer, buffer_size, buffer_mode, owned, mode),
-        fd(file_descriptor) {}
+      : LinuxFile(file_descriptor, buffer, buffer_size, buffer_mode, owned,
+                  mode, &linux_file_close) {}
+
+  // Allocates a FileT owning a new default-size buffer. FileT must be
+  // LinuxFile or a subclass with LinuxFile's constructor signature.
+  template <typename FileT = LinuxFile>
+  LIBC_INLINE static ErrorOr<FileT *> create_buffered(int fd, FileMode mode) {
+    static_assert(cpp::is_base_of_v<LinuxFile, FileT>,
+                  "FileT must be LinuxFile or derive from it");
+    uint8_t *buffer;
+    {
+      AllocChecker ac;
+      buffer = new (ac) uint8_t[DEFAULT_BUFFER_SIZE];
+      if (!ac)
+        return Error(ENOMEM);
+    }
+    AllocChecker ac;
+    auto *file = new (ac) FileT(fd, buffer, DEFAULT_BUFFER_SIZE, _IOFBF,
+                                /*owned=*/true, mode);
+    if (!ac) {
+      delete buffer;
+      return Error(ENOMEM);
+    }
+    return file;
+  }
 
   int get_fd() const { return fd; }
   void set_fd(int new_fd) { fd = new_fd; }
@@ -38,3 +88,5 @@ class LinuxFile : public File {
 ErrorOr<LinuxFile *> create_file_from_fd(int fd, const char *mode);
 
 } // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC___SUPPORT_FILE_LINUX_FILE_H
diff --git a/libc/src/__support/File/linux/popen_file.cpp b/libc/src/__support/File/linux/popen_file.cpp
new file mode 100644
index 00000000000000..18435e47bb2497
--- /dev/null
+++ b/libc/src/__support/File/linux/popen_file.cpp
@@ -0,0 +1,58 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 the Linux File specialization for popen streams.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/__support/File/linux/popen_file.h"
+
+#include "src/__support/CPP/new.h"
+#include "src/__support/File/file.h"
+#include "src/__support/macros/config.h"
+#include "src/__support/threads/mutex.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+PopenFile *PopenFile::popen_list = nullptr;
+Mutex PopenFile::popen_list_lock(/*timed=*/false, /*recursive=*/false,
+                                 /*robust=*/false, /*pshared=*/false);
+
+void PopenFile::add_to_popen_list_unlocked(PopenFile *f) {
+  f->popen_prev = nullptr;
+  f->popen_next = popen_list;
+  if (popen_list != nullptr)
+    popen_list->popen_prev = f;
+  popen_list = f;
+}
+
+void PopenFile::close_popen_list_fds_unlocked() {
+  for (PopenFile *f = popen_list; f != nullptr; f = f->popen_next)
+    f->close_fd();
+}
+
+int PopenFile::popen_file_close(File *f) {
+  auto *pf = reinterpret_cast<PopenFile *>(f);
+
+  lock_popen_list();
+  if (pf->popen_prev != nullptr)
+    pf->popen_prev->popen_next = pf->popen_next;
+  if (pf->popen_next != nullptr)
+    pf->popen_next->popen_prev = pf->popen_prev;
+  if (popen_list == pf)
+    popen_list = pf->popen_next;
+  unlock_popen_list();
+
+  File::remove_file(f);
+  int retval = pf->close_fd();
+  delete pf;
+  return retval;
+}
+
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/__support/File/linux/popen_file.h b/libc/src/__support/File/linux/popen_file.h
new file mode 100644
index 00000000000000..9c959b069c52dc
--- /dev/null
+++ b/libc/src/__support/File/linux/popen_file.h
@@ -0,0 +1,66 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+/// Linux File specialization for streams created by popen.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC___SUPPORT_FILE_LINUX_POPEN_FILE_H
+#define LLVM_LIBC_SRC___SUPPORT_FILE_LINUX_POPEN_FILE_H
+
+#include "hdr/types/pid_t.h"
+#include "src/__support/File/file.h"
+#include "src/__support/File/file_mode.h"
+#include "src/__support/File/linux/file.h"
+#include "src/__support/macros/config.h"
+#include "src/__support/threads/mutex.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+// A LinuxFile connected to a pipe to a child process created by popen. Open
+// popen streams are kept on their own list, so that each new popen child can
+// close the streams of earlier popen calls.
+class PopenFile : public LinuxFile {
+  pid_t pid = -1;
+  PopenFile *popen_prev = nullptr;
+  PopenFile *popen_next = nullptr;
+
+  static PopenFile *popen_list;
+  static Mutex popen_list_lock;
+
+  // Removes |f| from the popen list, then closes and frees it like a
+  // LinuxFile. This does not wait for the child process.
+  static int popen_file_close(File *f);
+
+public:
+  PopenFile(int file_descriptor, uint8_t *buffer, size_t buffer_size,
+            int buffer_mode, bool owned, FileMode mode)
+      : LinuxFile(file_descriptor, buffer, buffer_size, buffer_mode, owned,
+                  mode, &popen_file_close) {}
+
+  pid_t get_pid() const { return pid; }
+  void set_pid(pid_t new_pid) { pid = new_pid; }
+
+  // The popen list lock must be held while calling the *_unlocked functions
+  // below. popen holds it from before forking until the new stream is on the
+  // list, so that the child sees a consistent list.
+  static void lock_popen_list() { popen_list_lock.lock(); }
+  static void unlock_popen_list() { popen_list_lock.unlock(); }
+
+  // Adds |f| to the popen list.
+  static void add_to_popen_list_unlocked(PopenFile *f);
+
+  // Closes the file descriptors of every stream on the popen list without
+  // freeing anything. This is meant to be called in a newly forked child.
+  static void close_popen_list_fds_unlocked();
+};
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC___SUPPORT_FILE_LINUX_POPEN_FILE_H
diff --git a/libc/src/__support/OSUtil/linux/CMakeLists.txt b/libc/src/__support/OSUtil/linux/CMakeLists.txt
index e01f079c6780e5..728166d37be6de 100644
--- a/libc/src/__support/OSUtil/linux/CMakeLists.txt
+++ b/libc/src/__support/OSUtil/linux/CMakeLists.txt
@@ -66,6 +66,25 @@ add_header_library(
     libc.src.__support.CPP.string_view
 )
 
+add_header_library(
+  shell_fork
+  HDRS
+    shell_fork.h
+  DEPENDS
+    libc.hdr.errno_macros
+    libc.hdr.signal_macros
+    libc.hdr.types.pid_t
+    libc.src.__support.common
+    libc.src.__support.CPP.functional
+    libc.src.__support.error_or
+    libc.src.__support.macros.config
+    libc.src.__support.OSUtil.osutil
+    libc.src.__support.OSUtil.linux.syscall_wrappers.clone3
+    libc.src.__support.OSUtil.linux.syscall_wrappers.execle
+    libc.src.__support.OSUtil.linux.syscall_wrappers.wait4
+    libc.src.unistd.environ
+)
+
 add_header_library(
   sysinfo
   HDRS
diff --git a/libc/src/__support/OSUtil/linux/shell_fork.h b/libc/src/__support/OSUtil/linux/shell_fork.h
new file mode 100644
index 00000000000000..3dd64fad34e80a
--- /dev/null
+++ b/libc/src/__support/OSUtil/linux/shell_fork.h
@@ -0,0 +1,79 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+/// Helpers for running a command with the shell in a child process, shared by
+/// functions like system and popen.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC___SUPPORT_OSUTIL_LINUX_SHELL_FORK_H
+#define LLVM_LIBC_SRC___SUPPORT_OSUTIL_LINUX_SHELL_FORK_H
+
+#include "hdr/errno_macros.h"
+#include "hdr/signal_macros.h"
+#include "hdr/types/pid_t.h"
+#include "src/__support/CPP/functional.h"
+#include "src/__support/OSUtil/exit.h"
+#include "src/__support/OSUtil/linux/syscall_wrappers/clone3.h"
+#include "src/__support/OSUtil/linux/syscall_wrappers/execle.h"
+#include "src/__support/OSUtil/linux/syscall_wrappers/wait4.h"
+#include "src/__support/common.h"
+#include "src/__support/error_or.h"
+#include "src/__support/macros/config.h"
+#include "src/unistd/environ.h"
+
+#include <linux/sched.h> // For struct clone_args
+
+namespace LIBC_NAMESPACE_DECL {
+
+LIBC_INLINE_VAR constexpr char SHELL_PATH[] = "/bin/sh";
+
+// Work to do in the child process after forking and before executing the
+// shell. Returns false if the setup failed and the shell should not be run.
+using ShellChildSetup = cpp::function<bool()>;
+
+// Forks a child process which runs |setup| and then executes
+// "sh -c |command|" with the current environment. If |setup| or the exec
+// fails, the child exits with status 127. Returns the child's pid in the
+// parent.
+LIBC_INLINE ErrorOr<pid_t> shell_fork(const char *command,
+                                      ShellChildSetup setup) {
+  struct clone_args cl_args{};
+  cl_args.exit_signal = SIGCHLD;
+  ErrorOr<pid_t> fork_res = linux_syscalls::clone3(&cl_args, sizeof(cl_args));
+  if (!fork_res.has_value())
+    return Error(fork_res.error());
+
+  if (fork_res.value() == 0) {
+    // Error checking isn't helpful since this is the forked process, so we
+    // can't set errno. All we can meaningfully do is exit with status 127.
+    if (setup())
+      linux_syscalls::execle(SHELL_PATH, "sh", "-c", command, 0, environ);
+    internal::exit(127);
+  }
+
+  return fork_res.value();
+}
+
+// Waits for the child process |pid| to terminate, retrying if interrupted by
+// a signal. Returns the child's wait status.
+LIBC_INLINE ErrorOr<int> shell_wait(pid_t pid) {
+  int status = 0;
+  while (true) {
+    ErrorOr<pid_t> wait_res = linux_syscalls::wait4(pid, &status, 0, nullptr);
+    if (wait_res.has_value())
+      return status;
+    if (wait_res.error() != EINTR)
+      return Error(wait_res.error());
+  }
+}
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC___SUPPORT_OSUTIL_LINUX_SHELL_FORK_H
diff --git a/libc/src/__support/OSUtil/linux/syscall_wrappers/CMakeLists.txt b/libc/src/__support/OSUtil/linux/syscall_wrappers/CMakeLists.txt
index 5247a6c71ffce9..85d0d76ba7f077 100644
--- a/libc/src/__support/OSUtil/linux/syscall_wrappers/CMakeLists.txt
+++ b/libc/src/__support/OSUtil/linux/syscall_wrappers/CMakeLists.txt
@@ -588,6 +588,19 @@ add_header_library(
     libc.include.sys_syscall
 )
 
+add_header_library(
+  pipe2
+  HDRS
+    pipe2.h
+  DEPENDS
+    libc.src.__support.OSUtil.osutil
+    libc.src.__support.common
+    libc.src.__support.error_or
+    libc.src.__support.macros.config
+    libc.src.__support.macros.sanitizer
+    libc.include.sys_syscall
+)
+
 add_header_library(
   posix_fadvise
   HDRS
diff --git a/libc/src/__support/OSUtil/linux/syscall_wrappers/pipe2.h b/libc/src/__support/OSUtil/linux/syscall_wrappers/pipe2.h
new file mode 100644
index 00000000000000..dec98c869fcb23
--- /dev/null
+++ b/libc/src/__support/OSUtil/linux/syscall_wrappers/pipe2.h
@@ -0,0 +1,37 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+/// Syscall wrapper for pipe2.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC___SUPPORT_OSUTIL_SYSCALL_WRAPPERS_PIPE2_H
+#define LLVM_LIBC_SRC___SUPPORT_OSUTIL_SYSCALL_WRAPPERS_PIPE2_H
+
+#include "src/__support/OSUtil/linux/syscall.h" // syscall_checked
+#include "src/__support/common.h"
+#include "src/__support/error_or.h"
+#include "src/__support/macros/config.h"
+#include "src/__support/macros/sanitizer.h" // LIBC_MSAN_UNPOISON
+#include <sys/syscall.h>                    // For syscall numbers
+
+namespace LIBC_NAMESPACE_DECL {
+namespace linux_syscalls {
+
+LIBC_INLINE ErrorOr<int> pipe2(int pipefd[2], int flags) {
+  ErrorOr<int> result = syscall_checked<int>(SYS_pipe2, pipefd, flags);
+  if (result.has_value())
+    LIBC_MSAN_UNPOISON(pipefd, sizeof(int) * 2);
+  return result;
+}
+
+} // namespace linux_syscalls
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC___SUPPORT_OSUTIL_SYSCALL_WRAPPERS_PIPE2_H
diff --git a/libc/src/stdio/CMakeLists.txt b/libc/src/stdio/CMakeLists.txt
index 5bcfb534737192..30cff885b526fe 100644
--- a/libc/src/stdio/CMakeLists.txt
+++ b/libc/src/stdio/CMakeLists.txt
@@ -320,6 +320,20 @@ add_entrypoint_object(
     .${LIBC_TARGET_OS}.fdopen
 )
 
+add_entrypoint_object(
+  popen
+  ALIAS
+  DEPENDS
+    .${LIBC_TARGET_OS}.popen
+)
+
+add_entrypoint_object(
+  pclose
+  ALIAS
+  DEPENDS
+    .${LIBC_TARGET_OS}.pclose
+)
+
 add_entrypoint_object(
   tmpfile
   ALIAS
diff --git a/libc/src/stdio/linux/CMakeLists.txt b/libc/src/stdio/linux/CMakeLists.txt
index d782ee65866442..6fc5e7b7940a33 100644
--- a/libc/src/stdio/linux/CMakeLists.txt
+++ b/libc/src/stdio/linux/CMakeLists.txt
@@ -48,6 +48,49 @@ add_entrypoint_object(
     libc.src.__support.File.platform_file
 )
 
+add_entrypoint_object(
+  popen
+  SRCS
+    popen.cpp
+  HDRS
+    ../popen.h
+  DEPENDS
+    libc.hdr.errno_macros
+    libc.hdr.fcntl_macros
+    libc.hdr.types.FILE
+    libc.hdr.types.pid_t
+    libc.hdr.unistd_macros
+    libc.src.__support.common
+    libc.src.__support.error_or
+    libc.src.__support.File.file
+    libc.src.__support.File.linux.popen_file
+    libc.src.__support.File.platform_file
+    libc.src.__support.libc_errno
+    libc.src.__support.macros.config
+    libc.src.__support.OSUtil.linux.shell_fork
+    libc.src.__support.OSUtil.linux.syscall_wrappers.close
+    libc.src.__support.OSUtil.linux.syscall_wrappers.dup2
+    libc.src.__support.OSUtil.linux.syscall_wrappers.fcntl
+    libc.src.__support.OSUtil.linux.syscall_wrappers.pipe2
+)
+
+add_entrypoint_object(
+  pclose
+  SRCS
+    pclose.cpp
+  HDRS
+    ../pclose.h
+  DEPENDS
+    libc.hdr.types.FILE
+    libc.hdr.types.pid_t
+    libc.src.__support.common
+    libc.src.__support.error_or
+    libc.src.__support.File.linux.popen_file
+    libc.src.__support.libc_errno
+    libc.src.__support.macros.config
+    libc.src.__support.OSUtil.linux.shell_fork
+)
+
 add_entrypoint_object(
   stdin
   SRCS
diff --git a/libc/src/stdio/linux/pclose.cpp b/libc/src/stdio/linux/pclose.cpp
new file mode 100644
index 00000000000000..acb4a6a7e8763a
--- /dev/null
+++ b/libc/src/stdio/linux/pclose.cpp
@@ -0,0 +1,53 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 the POSIX pclose function for Linux.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/stdio/pclose.h"
+
+#include "hdr/types/FILE.h"
+#include "hdr/types/pid_t.h"
+#include "src/__support/File/linux/popen_file.h"
+#include "src/__support/OSUtil/linux/shell_fork.h"
+#include "src/__support/common.h"
+#include "src/__support/error_or.h"
+#include "src/__support/libc_errno.h"
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+LLVM_LIBC_FUNCTION(int, pclose, (::FILE * stream)) {
+  // Passing a stream that was not created by popen is undefined behavior.
+  auto *file = reinterpret_cast<PopenFile *>(stream);
+  const pid_t pid = file->get_pid();
+
+  // Closing the stream flushes it and closes the parent's end of the pipe, so
+  // a child reading from it sees EOF. It also frees the stream.
+  // TODO: File::close returns early without closing the fd if flushing fails,
+  // which could leave a child reading from the pipe waiting forever. Fix
+  // File::close to always release the fd in a followup.
+  int close_result = file->close();
+
+  // POSIX requires that pclose not return before the child has terminated, so
+  // wait even if closing failed.
+  ErrorOr<int> wait_result = shell_wait(pid);
+  if (!wait_result) {
+    libc_errno = wait_result.error();
+    return -1;
+  }
+  if (close_result != 0) {
+    libc_errno = close_result;
+    return -1;
+  }
+  return wait_result.value();
+}
+
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/stdio/linux/popen.cpp b/libc/src/stdio/linux/popen.cpp
new file mode 100644
index 00000000000000..ea5461c9870f23
--- /dev/null
+++ b/libc/src/stdio/linux/popen.cpp
@@ -0,0 +1,131 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 the POSIX popen function for Linux.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/stdio/popen.h"
+
+#include "hdr/errno_macros.h"
+#include "hdr/fcntl_macros.h"
+#include "hdr/types/FILE.h"
+#include "hdr/types/pid_t.h"
+#include "hdr/unistd_macros.h"
+#include "src/__support/File/file.h"
+#include "src/__support/File/file_mode.h"
+#include "src/__support/File/linux/file.h"
+#include "src/__support/File/linux/popen_file.h"
+#include "src/__support/OSUtil/linux/shell_fork.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/fcntl.h"
+#include "src/__support/OSUtil/linux/syscall_wrappers/pipe2.h"
+#include "src/__support/common.h"
+#include "src/__support/error_or.h"
+#include "src/__support/libc_errno.h"
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+namespace {
+
+// A pipe only goes one way, so these are the only modes popen accepts.
+constexpr FileMode VALID_POPEN_MODES[] = {FileMode("r"), FileMode("w"),
+                                          FileMode("re"), FileMode("we")};
+
+// TODO: Simplify this with a cpp::find (or similar) helper in
+// src/__support/CPP/algorithm.h once one exists.
+bool is_valid_popen_mode(const FileMode &file_mode) {
+  for (const FileMode &valid_mode : VALID_POPEN_MODES)
+    if (file_mode == valid_mode)
+      return true;
+  return false;
+}
+
+// Clears FD_CLOEXEC on |fd|, leaving its other descriptor flags unchanged.
+ErrorOr<int> clear_close_on_exec(int fd) {
+  ErrorOr<int> flags = linux_syscalls::fcntl(fd, F_GETFD);
+  if (!flags)
+    return flags;
+  return linux_syscalls::fcntl(
+      fd, F_SETFD, reinterpret_cast<void *>(flags.value() & ~FD_CLOEXEC));
+}
+
+// Makes |fd| available as |target_fd| in a process that is about to exec.
+// Both ends of the pipe are created close-on-exec, so if |fd| is already
+// |target_fd| its FD_CLOEXEC flag has to be cleared instead.
+bool move_fd_for_exec(int fd, int target_fd) {
+  if (fd == target_fd)
+    return clear_close_on_exec(fd).has_value();
+  return linux_syscalls::dup2(fd, target_fd).has_value();
+}
+
+} // namespace
+
+LLVM_LIBC_FUNCTION(::FILE *, popen, (const char *command, const char *mode)) {
+  const FileMode file_mode(mode);
+  if (!is_valid_popen_mode(file_mode)) {
+    libc_errno = EINVAL;
+    return nullptr;
+  }
+  const bool read = file_mode.is_read();
+
+  // Both ends start out close-on-exec so that no other child process can
+  // inherit them. The child moves its end onto stdin or stdout before exec.
+  int pipe_fds[2];
+  if (ErrorOr<int> res = linux_syscalls::pipe2(pipe_fds, O_CLOEXEC); !res) {
+    libc_errno = res.error();
+    return nullptr;
+  }
+  const int parent_fd = read ? pipe_fds[0] : pipe_fds[1];
+  const int child_fd = read ? pipe_fds[1] : pipe_fds[0];
+  const int child_target_fd = read ? STDOUT_FILENO : STDIN_FILENO;
+
+  ErrorOr<PopenFile *> new_file =
+      LinuxFile::create_buffered<PopenFile>(parent_fd, file_mode);
+  if (!new_file) {
+    linux_syscalls::close(parent_fd);
+    linux_syscalls::close(child_fd);
+    libc_errno = new_file.error();
+    return nullptr;
+  }
+  PopenFile *file = new_file.value();
+
+  // Hold the popen list lock across the fork so that the child closes exactly
+  // the popen streams which are open in the parent.
+  PopenFile::lock_popen_list();
+  ErrorOr<pid_t> fork_result = shell_fork(command, [&] {
+    PopenFile::close_popen_list_fds_unlocked();
+    return move_fd_for_exec(child_fd, child_target_fd);
+  });
+  linux_syscalls::close(child_fd);
+
+  if (!fork_result) {
+    PopenFile::unlock_popen_list();
+    // The file is not on any list yet, so closing it just closes parent_fd
+    // and frees it.
+    file->close();
+    libc_errno = fork_result.error();
+    return nullptr;
+  }
+
+  file->set_pid(fork_result.value());
+  if (!file_mode.is_close_on_exec()) {
+    // This can only fail if parent_fd is invalid, which it is not.
+    clear_close_on_exec(parent_fd);
+  }
+  PopenFile::add_to_popen_list_unlocked(file);
+  PopenFile::unlock_popen_list();
+
+  File::add_file(file);
+  return reinterpret_cast<::FILE *>(file);
+}
+
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/stdio/pclose.h b/libc/src/stdio/pclose.h
new file mode 100644
index 00000000000000..36b220f2d20ca5
--- /dev/null
+++ b/libc/src/stdio/pclose.h
@@ -0,0 +1,26 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+/// Declaration of the POSIX pclose function.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_STDIO_PCLOSE_H
+#define LLVM_LIBC_SRC_STDIO_PCLOSE_H
+
+#include "hdr/types/FILE.h"
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+int pclose(::FILE *stream);
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_STDIO_PCLOSE_H
diff --git a/libc/src/stdio/popen.h b/libc/src/stdio/popen.h
new file mode 100644
index 00000000000000..52e829c8e97b92
--- /dev/null
+++ b/libc/src/stdio/popen.h
@@ -0,0 +1,26 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+/// Declaration of the POSIX popen function.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_STDIO_POPEN_H
+#define LLVM_LIBC_SRC_STDIO_POPEN_H
+
+#include "hdr/types/FILE.h"
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+::FILE *popen(const char *command, const char *mode);
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_STDIO_POPEN_H
diff --git a/libc/src/stdlib/linux/CMakeLists.txt b/libc/src/stdlib/linux/CMakeLists.txt
index c40fc6a5b84ce7..a6e7b9920b2f99 100644
--- a/libc/src/stdlib/linux/CMakeLists.txt
+++ b/libc/src/stdlib/linux/CMakeLists.txt
@@ -114,21 +114,17 @@ add_entrypoint_object(
   HDRS
     ../system.h
   DEPENDS
-    libc.hdr.errno_macros
     libc.hdr.signal_macros
     libc.hdr.types.pid_t
     libc.hdr.types.sigset_t
     libc.src.__support.common
+    libc.src.__support.error_or
     libc.src.__support.libc_errno
     libc.src.__support.macros.config
-    libc.src.__support.OSUtil.osutil
-    libc.src.__support.OSUtil.linux.syscall_wrappers.execle
-    libc.src.__support.OSUtil.linux.syscall_wrappers.clone3
+    libc.src.__support.OSUtil.linux.shell_fork
     libc.src.__support.OSUtil.linux.syscall_wrappers.rt_sigaction
     libc.src.__support.OSUtil.linux.syscall_wrappers.rt_sigprocmask
-    libc.src.__support.OSUtil.linux.syscall_wrappers.wait4
     libc.src.signal.linux.signal_utils
-    libc.src.unistd.environ
 )
 
 add_entrypoint_object(
diff --git a/libc/src/stdlib/linux/system.cpp b/libc/src/stdlib/linux/system.cpp
index f40e1f8756b155..d0383ab1d06909 100644
--- a/libc/src/stdlib/linux/system.cpp
+++ b/libc/src/stdlib/linux/system.cpp
@@ -12,28 +12,20 @@
 //===----------------------------------------------------------------------===//
 
 #include "src/stdlib/system.h"
-#include "hdr/errno_macros.h"
 #include "hdr/signal_macros.h"
 #include "hdr/types/pid_t.h"
 #include "hdr/types/sigset_t.h"
-#include "src/__support/OSUtil/exit.h"
-#include "src/__support/OSUtil/linux/syscall_wrappers/clone3.h"
-#include "src/__support/OSUtil/linux/syscall_wrappers/execle.h"
+#include "src/__support/OSUtil/linux/shell_fork.h"
 #include "src/__support/OSUtil/linux/syscall_wrappers/rt_sigaction.h"
 #include "src/__support/OSUtil/linux/syscall_wrappers/rt_sigprocmask.h"
-#include "src/__support/OSUtil/linux/syscall_wrappers/wait4.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/signal/linux/signal_utils.h"
-#include "src/unistd/environ.h"
-
-#include <linux/sched.h> // For struct clone_args
 
 namespace LIBC_NAMESPACE_DECL {
 
-const char *SHELL_PATH = "/bin/sh";
-
 LLVM_LIBC_FUNCTION(int, system, (const char *command)) {
   if (command == nullptr)
     return 1;
@@ -69,50 +61,32 @@ LLVM_LIBC_FUNCTION(int, system, (const char *command)) {
     return -1;
   }
 
-  struct clone_args cl_args{};
-  cl_args.exit_signal = SIGCHLD;
-  auto fork_res = linux_syscalls::clone3(&cl_args, sizeof(cl_args));
-  if (!fork_res.has_value()) {
+  auto restore_signals = [&] {
     linux_syscalls::rt_sigaction(SIGINT, &orig_int, nullptr);
     linux_syscalls::rt_sigaction(SIGQUIT, &orig_quit, nullptr);
     linux_syscalls::rt_sigprocmask(SIG_SETMASK, &orig_mask, nullptr);
+  };
+
+  // The child restores the original signal state before running the shell.
+  ErrorOr<pid_t> fork_res = shell_fork(command, [&] {
+    restore_signals();
+    return true;
+  });
+  if (!fork_res.has_value()) {
+    restore_signals();
     libc_errno = fork_res.error();
     return -1;
   }
 
-  pid_t pid = fork_res.value();
-  if (pid == 0) {
-    linux_syscalls::rt_sigaction(SIGINT, &orig_int, nullptr);
-    linux_syscalls::rt_sigaction(SIGQUIT, &orig_quit, nullptr);
-    linux_syscalls::rt_sigprocmask(SIG_SETMASK, &orig_mask, nullptr);
-
-    // Error checking isn't helpful since this is the forked process, so we
-    // can't set errno. All we can meaningfully do is exit with status 127.
-    linux_syscalls::execle(SHELL_PATH, "sh", "-c", command, 0, environ);
+  ErrorOr<int> wait_res = shell_wait(fork_res.value());
 
-    internal::exit(127);
-  }
+  restore_signals();
 
-  int status = 0;
-  int wait_ret = 0;
-  while (true) {
-    if (auto wait_res = linux_syscalls::wait4(pid, &status, 0, nullptr);
-        !wait_res.has_value()) {
-      if (wait_res.error() == EINTR)
-        continue;
-      wait_ret = -1;
-      libc_errno = wait_res.error();
-      break;
-    }
-    wait_ret = status;
-    break;
+  if (!wait_res.has_value()) {
+    libc_errno = wait_res.error();
+    return -1;
   }
-
-  linux_syscalls::rt_sigaction(SIGINT, &orig_int, nullptr);
-  linux_syscalls::rt_sigaction(SIGQUIT, &orig_quit, nullptr);
-  linux_syscalls::rt_sigprocmask(SIG_SETMASK, &orig_mask, nullptr);
-
-  return wait_ret;
+  return wait_res.value();
 }
 
 } // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/unistd/linux/CMakeLists.txt b/libc/src/unistd/linux/CMakeLists.txt
index fb77b8b738e614..f85acff74b6490 100644
--- a/libc/src/unistd/linux/CMakeLists.txt
+++ b/libc/src/unistd/linux/CMakeLists.txt
@@ -607,9 +607,10 @@ add_entrypoint_object(
   HDRS
     ../pipe2.h
   DEPENDS
-    libc.include.unistd
-    libc.include.sys_syscall
-    libc.src.__support.OSUtil.osutil
+    libc.src.__support.OSUtil.linux.syscall_wrappers.pipe2
+    libc.src.__support.common
+    libc.src.__support.libc_errno
+    libc.src.__support.macros.config
     libc.src.errno.errno
 )
 
diff --git a/libc/src/unistd/linux/pipe2.cpp b/libc/src/unistd/linux/pipe2.cpp
index baf78ff55be7b1..e12309449917fc 100644
--- a/libc/src/unistd/linux/pipe2.cpp
+++ b/libc/src/unistd/linux/pipe2.cpp
@@ -1,30 +1,32 @@
-//===-- Linux implementation of pipe --------------------------------------===//
+//===----------------------------------------------------------------------===//
 //
 // 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 pipe2 for Linux.
+///
+//===----------------------------------------------------------------------===//
 
 #include "src/unistd/pipe2.h"
 
-#include "src/__support/OSUtil/syscall.h" // For internal syscall function.
+#include "src/__support/OSUtil/linux/syscall_wrappers/pipe2.h"
 #include "src/__support/common.h"
 #include "src/__support/libc_errno.h"
 #include "src/__support/macros/config.h"
-#include <sys/syscall.h> // For syscall numbers.
 
 namespace LIBC_NAMESPACE_DECL {
 
 LLVM_LIBC_FUNCTION(int, pipe2, (int pipefd[2], int flags)) {
-  int ret = LIBC_NAMESPACE::syscall_impl<int>(
-      SYS_pipe2, reinterpret_cast<long>(pipefd), flags);
-  if (ret < 0) {
-    libc_errno = -ret;
+  ErrorOr<int> result = linux_syscalls::pipe2(pipefd, flags);
+  if (!result.has_value()) {
+    libc_errno = result.error();
     return -1;
   }
-  LIBC_MSAN_UNPOISON(pipefd, sizeof(int) * 2);
-  return ret;
+  return result.value();
 }
 
 } // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/test/src/stdio/CMakeLists.txt b/libc/test/src/stdio/CMakeLists.txt
index 40d91e799b99ab..b5b16abf34ae3c 100644
--- a/libc/test/src/stdio/CMakeLists.txt
+++ b/libc/test/src/stdio/CMakeLists.txt
@@ -605,6 +605,29 @@ if(${LIBC_TARGET_OS} STREQUAL "linux")
       libc.test.UnitTest.ErrnoCheckingTest
       libc.test.UnitTest.ErrnoSetterMatcher
   )
+
+  add_libc_test(
+    popen_pclose_test
+    SUITE
+      libc_stdio_unittests
+    SRCS
+      popen_pclose_test.cpp
+    DEPENDS
+      libc.hdr.errno_macros
+      libc.hdr.fcntl_macros
+      libc.hdr.sys_wait_macros
+      libc.hdr.types.FILE
+      libc.src.fcntl.fcntl
+      libc.src.stdio.fgets
+      libc.src.stdio.fileno
+      libc.src.stdio.fputs
+      libc.src.stdio.pclose
+      libc.src.stdio.popen
+      libc.src.stdio.snprintf
+      libc.src.sys.wait.wait
+      libc.test.UnitTest.ErrnoCheckingTest
+      libc.test.UnitTest.ErrnoSetterMatcher
+  )
 endif()
 
 add_libc_test(
diff --git a/libc/test/src/stdio/popen_pclose_test.cpp b/libc/test/src/stdio/popen_pclose_test.cpp
new file mode 100644
index 00000000000000..f901e574fe30a4
--- /dev/null
+++ b/libc/test/src/stdio/popen_pclose_test.cpp
@@ -0,0 +1,158 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+/// Unit tests for popen and pclose.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/stdio/popen.h"
+
+#include "hdr/errno_macros.h"
+#include "hdr/fcntl_macros.h"
+#include "hdr/sys_wait_macros.h"
+#include "hdr/types/FILE.h"
+#include "src/fcntl/fcntl.h"
+#include "src/stdio/fgets.h"
+#include "src/stdio/fileno.h"
+#include "src/stdio/fputs.h"
+#include "src/stdio/pclose.h"
+#include "src/stdio/snprintf.h"
+#include "src/sys/wait/wait.h"
+#include "test/UnitTest/ErrnoCheckingTest.h"
+#include "test/UnitTest/ErrnoSetterMatcher.h"
+#include "test/UnitTest/Test.h"
+
+using LIBC_NAMESPACE::testing::ErrnoSetterMatcher::Fails;
+using LlvmLibcPopenTest = LIBC_NAMESPACE::testing::ErrnoCheckingTest;
+
+TEST_F(LlvmLibcPopenTest, ReadMode) {
+  ::FILE *stream = LIBC_NAMESPACE::popen("echo hello", "r");
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_NE(stream, nullptr);
+
+  char buf[16];
+  ASSERT_NE(LIBC_NAMESPACE::fgets(buf, sizeof(buf), stream), nullptr);
+  ASSERT_STREQ(buf, "hello\n");
+
+  int status = LIBC_NAMESPACE::pclose(stream);
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_TRUE(WIFEXITED(status));
+  ASSERT_EQ(WEXITSTATUS(status), 0);
+}
+
+TEST_F(LlvmLibcPopenTest, WriteMode) {
+  // The command exits with 0 only if it reads "hello" from its stdin.
+  ::FILE *stream = LIBC_NAMESPACE::popen("read x; test \"$x\" = hello", "w");
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_NE(stream, nullptr);
+
+  ASSERT_GE(LIBC_NAMESPACE::fputs("hello\n", stream), 0);
+
+  int status = LIBC_NAMESPACE::pclose(stream);
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_TRUE(WIFEXITED(status));
+  ASSERT_EQ(WEXITSTATUS(status), 0);
+}
+
+TEST_F(LlvmLibcPopenTest, ExitStatus) {
+  ::FILE *stream = LIBC_NAMESPACE::popen("exit 42", "r");
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_NE(stream, nullptr);
+
+  int status = LIBC_NAMESPACE::pclose(stream);
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_TRUE(WIFEXITED(status));
+  ASSERT_EQ(WEXITSTATUS(status), 42);
+}
+
+TEST_F(LlvmLibcPopenTest, InvalidMode) {
+  ::FILE *stream = LIBC_NAMESPACE::popen("true", "x");
+  ASSERT_ERRNO_EQ(EINVAL);
+  ASSERT_EQ(stream, nullptr);
+}
+
+TEST_F(LlvmLibcPopenTest, PreviousStreamsClosedInChild) {
+  // Keep a popen stream open while a second command is started.
+  ::FILE *first = LIBC_NAMESPACE::popen("cat >/dev/null", "w");
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_NE(first, nullptr);
+  int first_fd = LIBC_NAMESPACE::fileno(first);
+  ASSERT_GE(first_fd, 0);
+
+  // The second command reports whether the first stream's fd is open in its
+  // own process. Redirecting to a closed fd fails with EBADF.
+  char command[128];
+  ASSERT_GT(LIBC_NAMESPACE::snprintf(
+                command, sizeof(command),
+                "{ true >&%d; } 2>/dev/null && echo open || echo closed",
+                first_fd),
+            0);
+  ::FILE *second = LIBC_NAMESPACE::popen(command, "r");
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_NE(second, nullptr);
+
+  char buf[16];
+  ASSERT_NE(LIBC_NAMESPACE::fgets(buf, sizeof(buf), second), nullptr);
+  ASSERT_STREQ(buf, "closed\n");
+
+  int status = LIBC_NAMESPACE::pclose(second);
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_TRUE(WIFEXITED(status));
+  ASSERT_EQ(WEXITSTATUS(status), 0);
+
+  status = LIBC_NAMESPACE::pclose(first);
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_TRUE(WIFEXITED(status));
+  ASSERT_EQ(WEXITSTATUS(status), 0);
+}
+
+TEST_F(LlvmLibcPopenTest, StatusUnavailable) {
+  ::FILE *stream = LIBC_NAMESPACE::popen("true", "r");
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_NE(stream, nullptr);
+
+  // Reap the child before pclose can, which makes its status unavailable.
+  int status = 0;
+  ASSERT_GT(LIBC_NAMESPACE::wait(&status), 0);
+  ASSERT_TRUE(WIFEXITED(status));
+
+  ASSERT_THAT(LIBC_NAMESPACE::pclose(stream), Fails(ECHILD));
+}
+
+TEST_F(LlvmLibcPopenTest, CloseOnExecMode) {
+  ::FILE *stream = LIBC_NAMESPACE::popen("true", "re");
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_NE(stream, nullptr);
+
+  int fd_flags = LIBC_NAMESPACE::fcntl(LIBC_NAMESPACE::fileno(stream), F_GETFD);
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_GE(fd_flags, 0);
+  ASSERT_NE(fd_flags & FD_CLOEXEC, 0);
+
+  int status = LIBC_NAMESPACE::pclose(stream);
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_TRUE(WIFEXITED(status));
+  ASSERT_EQ(WEXITSTATUS(status), 0);
+}
+
+TEST_F(LlvmLibcPopenTest, NoCloseOnExecByDefault) {
+  ::FILE *stream = LIBC_NAMESPACE::popen("true", "r");
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_NE(stream, nullptr);
+
+  int fd_flags = LIBC_NAMESPACE::fcntl(LIBC_NAMESPACE::fileno(stream), F_GETFD);
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_GE(fd_flags, 0);
+  ASSERT_EQ(fd_flags & FD_CLOEXEC, 0);
+
+  int status = LIBC_NAMESPACE::pclose(stream);
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_TRUE(WIFEXITED(status));
+  ASSERT_EQ(WEXITSTATUS(status), 0);
+}
diff --git a/utils/bazel/llvm-project-overlay/libc/BUILD.bazel b/utils/bazel/llvm-project-overlay/libc/BUILD.bazel
index 131288e8d43feb..6585af1a6c563b 100644
--- a/utils/bazel/llvm-project-overlay/libc/BUILD.bazel
+++ b/utils/bazel/llvm-project-overlay/libc/BUILD.bazel
@@ -2368,6 +2368,7 @@ libc_support_library(
         ":__support_alloc_checker",
         ":__support_common",
         ":__support_cpp_new",
+        ":__support_cpp_type_traits",
         ":__support_error_or",
         ":__support_file_file",
         ":__support_libc_errno",



More information about the libc-commits mailing list