[libc-commits] [libc] [libc][realpath] Implement symbolic path resolution (PR #204467)

Jackson Stogel via libc-commits libc-commits at lists.llvm.org
Mon Jun 29 15:45:59 PDT 2026


https://github.com/jtstogel updated https://github.com/llvm/llvm-project/pull/204467

>From 5a867eb371bfc52da10103ff436191d9a96314d4 Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at google.com>
Date: Tue, 16 Jun 2026 19:43:30 -0700
Subject: [PATCH 01/10] [libc][realpath] Implement symbolic path resolution

This is an incremental implementation of realpath. It performs no syscalls. A real implementation would perform getcwd/statx/readlinkat to actually resolve symlinks / valid that paths exist / etc.

For testing purposes, I've added the entrypoint when `LLVM_LIBC_ENABLE_EXPERIMENTAL_ENTRYPOINTS` is enabled.
---
 libc/config/linux/x86_64/entrypoints.txt |   4 +
 libc/include/stdlib.yaml                 |   7 +
 libc/src/stdlib/CMakeLists.txt           |  15 +-
 libc/src/stdlib/linux/CMakeLists.txt     |  18 ++
 libc/src/stdlib/linux/realpath.cpp       | 217 +++++++++++++++++++++++
 libc/src/stdlib/realpath.h               |  25 +++
 libc/test/src/stdlib/CMakeLists.txt      |  15 ++
 libc/test/src/stdlib/realpath_test.cpp   | 109 ++++++++++++
 8 files changed, 406 insertions(+), 4 deletions(-)
 create mode 100644 libc/src/stdlib/linux/realpath.cpp
 create mode 100644 libc/src/stdlib/realpath.h
 create mode 100644 libc/test/src/stdlib/realpath_test.cpp

diff --git a/libc/config/linux/x86_64/entrypoints.txt b/libc/config/linux/x86_64/entrypoints.txt
index ce88a6749d9dc..78e88bf1a352f 100644
--- a/libc/config/linux/x86_64/entrypoints.txt
+++ b/libc/config/linux/x86_64/entrypoints.txt
@@ -1512,6 +1512,10 @@ if(LLVM_LIBC_FULL_BUILD)
 endif()
 
 if(LLVM_LIBC_ENABLE_EXPERIMENTAL_ENTRYPOINTS)
+  list(APPEND TARGET_LIBC_ENTRYPOINTS
+    libc.src.stdlib.realpath
+  )
+
   if(LLVM_LIBC_FULL_BUILD)
     list(APPEND TARGET_LIBC_ENTRYPOINTS
       # regex.h entrypoints
diff --git a/libc/include/stdlib.yaml b/libc/include/stdlib.yaml
index bf77d5d850ea0..a9b7f2f73ad57 100644
--- a/libc/include/stdlib.yaml
+++ b/libc/include/stdlib.yaml
@@ -216,6 +216,13 @@ functions:
     return_type: int
     arguments:
       - type: void
+  - name: realpath
+    standards:
+      - posix
+    return_type: char *
+    arguments:
+      - type: const char *__restrict
+      - type: char *__restrict
   - name: srand
     standards:
       - stdc
diff --git a/libc/src/stdlib/CMakeLists.txt b/libc/src/stdlib/CMakeLists.txt
index 4593288881c7d..2bb515bbf6c24 100644
--- a/libc/src/stdlib/CMakeLists.txt
+++ b/libc/src/stdlib/CMakeLists.txt
@@ -585,6 +585,17 @@ if(NOT LIBC_TARGET_OS_IS_BAREMETAL AND NOT LIBC_TARGET_OS_IS_GPU)
   endif()
 endif()
 
+if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${LIBC_TARGET_OS})
+  add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/${LIBC_TARGET_OS})
+endif()
+
+add_entrypoint_object(
+  realpath
+  ALIAS
+  DEPENDS
+    .${LIBC_TARGET_OS}.realpath
+)
+
 if(NOT LLVM_LIBC_FULL_BUILD)
   return()
 endif()
@@ -666,10 +677,6 @@ add_entrypoint_object(
     libc.src.__support.str_to_integer
 )
 
-if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${LIBC_TARGET_OS})
-  add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/${LIBC_TARGET_OS})
-endif()
-
 add_entrypoint_object(
   setenv
   ALIAS
diff --git a/libc/src/stdlib/linux/CMakeLists.txt b/libc/src/stdlib/linux/CMakeLists.txt
index aedcd3f11bf38..2ea3cbeb06cb8 100644
--- a/libc/src/stdlib/linux/CMakeLists.txt
+++ b/libc/src/stdlib/linux/CMakeLists.txt
@@ -9,6 +9,24 @@ add_header_library(
     libc.src.signal.linux.signal_utils
 )
 
+add_entrypoint_object(
+  realpath
+  SRCS
+    realpath.cpp
+  HDRS
+    ../realpath.h
+  DEPENDS
+    libc.hdr.errno_macros
+    libc.hdr.limits_macros
+    libc.src.__support.alloc_checker
+    libc.src.__support.common
+    libc.src.__support.CPP.string_view
+    libc.src.__support.error_or
+    libc.src.__support.libc_errno
+    libc.src.__support.macros.config
+    libc.src.string.memory_utils.inline_memcpy
+)
+
 add_entrypoint_object(
   setenv
   SRCS
diff --git a/libc/src/stdlib/linux/realpath.cpp b/libc/src/stdlib/linux/realpath.cpp
new file mode 100644
index 0000000000000..e635b72dacca6
--- /dev/null
+++ b/libc/src/stdlib/linux/realpath.cpp
@@ -0,0 +1,217 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 POSIX realpath.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/stdlib/realpath.h"
+#include "hdr/errno_macros.h"
+#include "hdr/limits_macros.h"
+#include "src/__support/CPP/string_view.h"
+#include "src/__support/alloc-checker.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/string/memory_utils/inline_memcpy.h"
+
+namespace LIBC_NAMESPACE_DECL {
+namespace {
+
+// Separator character for POSIX paths.
+constexpr char PATH_SEP = '/';
+
+// Dummy struct to represent success in `ErrorOr` when no value is needed.
+struct Ok {};
+
+// Whether a path is absolute.
+bool is_absolute(cpp::string_view path) { return path.starts_with(PATH_SEP); }
+
+// Container for a fully resolved, canonical path.
+//
+// The contained path is always in its canonical form. It is:
+// - Absolute
+// - Symlink-free
+// - Without a trailing separator
+// - Devoid of path traversals like "." or ".."
+class ResolvedPath {
+public:
+  ResolvedPath() { set_to_root(); }
+
+  void set_to_root() {
+    buf_[0] = PATH_SEP;
+    size_ = 1;
+  }
+
+  bool is_root() const { return size_ == 1; }
+
+  ErrorOr<Ok> set_to_cwd() { return Error(ENOSYS); }
+
+  void set_to_parent() {
+    size_t sep_index = view().find_last_of(PATH_SEP);
+
+    // Ensure we maintain the root separator.
+    size_ = sep_index == 0 ? 1 : sep_index;
+  }
+
+  // Adds a single component to the end of this path.
+  ErrorOr<Ok> push_component(cpp::string_view component) {
+    if (component.size() > NAME_MAX)
+      return Error(ENAMETOOLONG);
+
+    if (!is_root()) {
+      if (ErrorOr<Ok> res = push_raw(PATH_SEP); !res)
+        return res;
+    }
+
+    return push_raw(component);
+  }
+
+  cpp::string_view view() const { return cpp::string_view(buf_, size_); }
+
+private:
+  ErrorOr<Ok> push_raw(cpp::string_view value) {
+    if (value.size() > sizeof(buf_) - size_)
+      return Error(ENAMETOOLONG);
+
+    inline_memcpy(buf_ + size_, value.data(), value.size());
+    size_ += value.size();
+    return Ok{};
+  }
+
+  ErrorOr<Ok> push_raw(char value) {
+    return push_raw(cpp::string_view(&value, 1));
+  }
+
+  // Current size of the path stored in `buf_`.
+  size_t size_;
+
+  // `PATH_MAX` includes a null-terminator in its count,
+  // so use `PATH_MAX - 1` here as `ResolvedPath` is not null-terminated.
+  char buf_[PATH_MAX - 1];
+};
+
+// A view over path components yet to be processed by realpath.
+//
+// When `realpath("./a/../b")` is called, the input path can be viewed as
+// a stack of components, where components closest to the root are at the top.
+// For example:
+//
+//   ```
+//   PendingPath p("./a/..");
+//   assert(p.advance_component() == ".");
+//   assert(p.advance_component() == "a");
+//   assert(p.advance_component() == "..");
+//   assert(p.empty());
+//   ```
+class PendingPath {
+public:
+  explicit PendingPath(cpp::string_view path) : view_(path) {}
+
+  // Whether all path components have been consumed.
+  bool empty() const { return view_.empty(); }
+
+  // Takes the next path component,
+  // starting with the component closest to the root.
+  cpp::string_view advance_component() {
+    const cpp::string_view path = view_;
+
+    const size_t component_start = path.find_first_not_of(PATH_SEP);
+    if (component_start == cpp::string_view::npos) {
+      view_ = "";
+      return "";
+    }
+
+    const size_t component_end =
+        path.find_first_of(PATH_SEP, /* From = */ component_start);
+    if (component_end == cpp::string_view::npos) {
+      view_ = "";
+      return path.substr(component_start);
+    }
+
+    view_ = view_.substr(component_end);
+    return path.substr(component_start, component_end - component_start);
+  }
+
+private:
+  cpp::string_view view_;
+};
+
+ErrorOr<char *> copy_or_allocate_cstr(char *dst, cpp::string_view src) {
+  if (dst == nullptr) {
+    AllocChecker ac;
+    dst = new (ac) char[src.size() + 1];
+    if (!ac)
+      return Error(ENOMEM);
+  }
+  inline_memcpy(dst, src.data(), src.size());
+  dst[src.size()] = '\0';
+  return dst;
+}
+
+ErrorOr<Ok> resolve_path(PendingPath &pending_path,
+                         ResolvedPath &resolved_path) {
+  while (!pending_path.empty()) {
+    cpp::string_view component = pending_path.advance_component();
+    if (component.empty() || component == ".")
+      continue;
+
+    if (component == "..") {
+      resolved_path.set_to_parent();
+      continue;
+    }
+
+    if (ErrorOr<Ok> res = resolved_path.push_component(component); !res)
+      return res;
+  }
+
+  return Ok{};
+}
+
+ErrorOr<char *> realpath_impl(const char *__restrict path_cstr,
+                              char *__restrict resolved_path_buf) {
+  if (path_cstr == nullptr)
+    return Error(EINVAL);
+
+  cpp::string_view path(path_cstr);
+  if (path.size() == 0)
+    return Error(ENOENT);
+
+  if (path.size() >= PATH_MAX)
+    return Error(ENAMETOOLONG);
+
+  PendingPath pending_path(path);
+
+  ResolvedPath resolved_path;
+  if (!is_absolute(path)) {
+    if (ErrorOr<Ok> res = resolved_path.set_to_cwd(); !res)
+      return Error(res.error());
+  }
+
+  if (ErrorOr<Ok> res = resolve_path(pending_path, resolved_path); !res)
+    return Error(res.error());
+
+  return copy_or_allocate_cstr(resolved_path_buf, resolved_path.view());
+}
+
+} // namespace
+
+LLVM_LIBC_FUNCTION(char *, realpath,
+                   (const char *__restrict path,
+                    char *__restrict resolved_path)) {
+  ErrorOr<char *> res = realpath_impl(path, resolved_path);
+  if (!res) {
+    libc_errno = res.error();
+    return nullptr;
+  }
+  return *res;
+}
+
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/stdlib/realpath.h b/libc/src/stdlib/realpath.h
new file mode 100644
index 0000000000000..ddc8700ffc197
--- /dev/null
+++ b/libc/src/stdlib/realpath.h
@@ -0,0 +1,25 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 realpath function.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_STDLIB_REALPATH_H
+#define LLVM_LIBC_SRC_STDLIB_REALPATH_H
+
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+char *realpath(const char *__restrict path, char *__restrict resolved_path);
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_STDLIB_REALPATH_H
diff --git a/libc/test/src/stdlib/CMakeLists.txt b/libc/test/src/stdlib/CMakeLists.txt
index 95e5a3ccc2bb5..6b48d86d6ae50 100644
--- a/libc/test/src/stdlib/CMakeLists.txt
+++ b/libc/test/src/stdlib/CMakeLists.txt
@@ -376,6 +376,21 @@ add_libc_test(
     libc.src.stdlib.srand
 )
 
+add_libc_test(
+  realpath_test
+  SUITE
+    libc-stdlib-tests
+  SRCS
+    realpath_test.cpp
+  DEPENDS
+    libc.hdr.errno_macros
+    libc.hdr.limits_macros
+    libc.src.__support.CPP.string
+    libc.src.__support.macros.config
+    libc.src.stdlib.realpath
+    libc.test.UnitTest.ErrnoCheckingTest
+)
+
 add_libc_test(
   memalignment_test
   SUITE
diff --git a/libc/test/src/stdlib/realpath_test.cpp b/libc/test/src/stdlib/realpath_test.cpp
new file mode 100644
index 0000000000000..604121e321dd0
--- /dev/null
+++ b/libc/test/src/stdlib/realpath_test.cpp
@@ -0,0 +1,109 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 realpath.
+///
+//===----------------------------------------------------------------------===//
+
+#include "hdr/errno_macros.h"
+#include "hdr/limits_macros.h"
+#include "src/__support/CPP/string.h"
+#include "src/__support/macros/config.h"
+#include "src/stdlib/realpath.h"
+#include "test/UnitTest/ErrnoCheckingTest.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+class LlvmLibcRealpathTest : public testing::ErrnoCheckingTest {
+public:
+  char *realpath_buffered(const char *path) { return realpath(path, buf_); }
+
+  char *realpath_buffered(const cpp::string &path) {
+    return realpath_buffered(path.c_str());
+  }
+
+  char buf_[PATH_MAX];
+};
+
+TEST_F(LlvmLibcRealpathTest, ErrorsWithInvalidArgIfNullPath) {
+  ASSERT_EQ(realpath_buffered(nullptr), nullptr);
+  ASSERT_ERRNO_EQ(EINVAL);
+}
+
+TEST_F(LlvmLibcRealpathTest, ErrorsWithNoEntryIfEmptyPath) {
+  ASSERT_EQ(realpath_buffered(""), nullptr);
+  ASSERT_ERRNO_EQ(ENOENT);
+}
+
+TEST_F(LlvmLibcRealpathTest, OkIfPathIsExactlyMaxSize) {
+  // PATH_MAX counts null terminator, so construct a path of size PATH_MAX-1.
+  cpp::string s(PATH_MAX - 1, 'a');
+  for (size_t i = 0; i < s.size(); i += NAME_MAX + 1)
+    s[i] = '/';
+
+  ASSERT_STREQ(realpath_buffered(s), s.c_str());
+}
+
+TEST_F(LlvmLibcRealpathTest, ErrorsWithNameTooLongIfPathExceedsMaxSize) {
+  // PATH_MAX counts null terminator, so construct a path of size PATH_MAX.
+  cpp::string s(PATH_MAX, 'a');
+  for (size_t i = 0; i < s.size(); i += NAME_MAX + 1)
+    s[i] = '/';
+
+  ASSERT_EQ(realpath_buffered(s), nullptr);
+  ASSERT_ERRNO_EQ(ENAMETOOLONG);
+}
+
+TEST_F(LlvmLibcRealpathTest, OkWhenComponentLengthIsExactlyNameMax) {
+  cpp::string s(NAME_MAX + 1, 'a'); // +1 for leading "/"
+  s[0] = '/';
+
+  ASSERT_STREQ(realpath_buffered(s), s.c_str());
+}
+
+TEST_F(LlvmLibcRealpathTest, ErrorsIfComponentExceedsNameMax) {
+  constexpr size_t COMPONENT_SIZE = NAME_MAX + 1;
+  cpp::string s(COMPONENT_SIZE + 1, 'a'); // +1 for leading "/"
+  s[0] = '/';
+
+  ASSERT_EQ(realpath_buffered(s), nullptr);
+  ASSERT_ERRNO_EQ(ENAMETOOLONG);
+}
+
+TEST_F(LlvmLibcRealpathTest, RootResolvesToRoot) {
+  ASSERT_STREQ(realpath_buffered("/"), "/");
+}
+
+TEST_F(LlvmLibcRealpathTest, RootDotDotTraversalStaysAtRoot) {
+  ASSERT_STREQ(realpath_buffered("/.."), "/");
+}
+
+TEST_F(LlvmLibcRealpathTest, SimpleAbsolutePath) {
+  ASSERT_STREQ(realpath_buffered("/a/b/c"), "/a/b/c");
+}
+
+TEST_F(LlvmLibcRealpathTest, DotDotTraversesParent) {
+  ASSERT_STREQ(realpath_buffered("/a/b/.."), "/a");
+}
+
+TEST_F(LlvmLibcRealpathTest, DotTraversalIsNop) {
+  ASSERT_STREQ(realpath_buffered("/a/b/./"), "/a/b");
+}
+
+TEST_F(LlvmLibcRealpathTest, ConsecutiveSeparatorsIgnored) {
+  ASSERT_STREQ(realpath_buffered("////a///..///a//"), "/a");
+}
+
+TEST_F(LlvmLibcRealpathTest, AllocatesResultWhenBufferIsNull) {
+  char *result = LIBC_NAMESPACE::realpath("/a", nullptr);
+  ASSERT_STREQ(result, "/a");
+  ::free(result);
+}
+
+} // namespace LIBC_NAMESPACE_DECL

>From 7267857d21175c444af8dadc6470505c5c1835e4 Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Wed, 17 Jun 2026 19:50:25 -0700
Subject: [PATCH 02/10] Comment on AllocChecker usage for resolved_path

---
 libc/src/stdlib/linux/realpath.cpp | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/libc/src/stdlib/linux/realpath.cpp b/libc/src/stdlib/linux/realpath.cpp
index e635b72dacca6..7b5cde08f5cee 100644
--- a/libc/src/stdlib/linux/realpath.cpp
+++ b/libc/src/stdlib/linux/realpath.cpp
@@ -147,6 +147,8 @@ class PendingPath {
 ErrorOr<char *> copy_or_allocate_cstr(char *dst, cpp::string_view src) {
   if (dst == nullptr) {
     AllocChecker ac;
+    // `dst` is safe to return and let the caller `free()`, since AllocChecker
+    // delegates to malloc, and this value is trivially destructible.
     dst = new (ac) char[src.size() + 1];
     if (!ac)
       return Error(ENOMEM);

>From e85d0364dadb9aa58608fad4bba4248e862e22fe Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Wed, 17 Jun 2026 19:53:54 -0700
Subject: [PATCH 03/10] Include size_t

---
 libc/src/stdlib/linux/CMakeLists.txt   | 1 +
 libc/src/stdlib/linux/realpath.cpp     | 1 +
 libc/test/src/stdlib/CMakeLists.txt    | 1 +
 libc/test/src/stdlib/realpath_test.cpp | 1 +
 4 files changed, 4 insertions(+)

diff --git a/libc/src/stdlib/linux/CMakeLists.txt b/libc/src/stdlib/linux/CMakeLists.txt
index 2ea3cbeb06cb8..71c890a4ecf43 100644
--- a/libc/src/stdlib/linux/CMakeLists.txt
+++ b/libc/src/stdlib/linux/CMakeLists.txt
@@ -19,6 +19,7 @@ add_entrypoint_object(
     libc.hdr.errno_macros
     libc.hdr.limits_macros
     libc.src.__support.alloc_checker
+    libc.hdr.types.size_t
     libc.src.__support.common
     libc.src.__support.CPP.string_view
     libc.src.__support.error_or
diff --git a/libc/src/stdlib/linux/realpath.cpp b/libc/src/stdlib/linux/realpath.cpp
index 7b5cde08f5cee..b5fb2d7f14902 100644
--- a/libc/src/stdlib/linux/realpath.cpp
+++ b/libc/src/stdlib/linux/realpath.cpp
@@ -14,6 +14,7 @@
 #include "src/stdlib/realpath.h"
 #include "hdr/errno_macros.h"
 #include "hdr/limits_macros.h"
+#include "hdr/types/size_t.h"
 #include "src/__support/CPP/string_view.h"
 #include "src/__support/alloc-checker.h"
 #include "src/__support/common.h"
diff --git a/libc/test/src/stdlib/CMakeLists.txt b/libc/test/src/stdlib/CMakeLists.txt
index 6b48d86d6ae50..224e9c4ac3bad 100644
--- a/libc/test/src/stdlib/CMakeLists.txt
+++ b/libc/test/src/stdlib/CMakeLists.txt
@@ -385,6 +385,7 @@ add_libc_test(
   DEPENDS
     libc.hdr.errno_macros
     libc.hdr.limits_macros
+    libc.hdr.types.size_t
     libc.src.__support.CPP.string
     libc.src.__support.macros.config
     libc.src.stdlib.realpath
diff --git a/libc/test/src/stdlib/realpath_test.cpp b/libc/test/src/stdlib/realpath_test.cpp
index 604121e321dd0..62d7f14c53c8d 100644
--- a/libc/test/src/stdlib/realpath_test.cpp
+++ b/libc/test/src/stdlib/realpath_test.cpp
@@ -13,6 +13,7 @@
 
 #include "hdr/errno_macros.h"
 #include "hdr/limits_macros.h"
+#include "hdr/types/size_t.h"
 #include "src/__support/CPP/string.h"
 #include "src/__support/macros/config.h"
 #include "src/stdlib/realpath.h"

>From 2157635c0b8fc5475c439d344bca9e9f08bb46f9 Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Wed, 17 Jun 2026 19:55:33 -0700
Subject: [PATCH 04/10] Don't check against NAME_MAX, we can let the kernel do
 so in statx

---
 libc/src/stdlib/linux/realpath.cpp     |  3 ---
 libc/test/src/stdlib/realpath_test.cpp | 35 +++++++++++---------------
 2 files changed, 14 insertions(+), 24 deletions(-)

diff --git a/libc/src/stdlib/linux/realpath.cpp b/libc/src/stdlib/linux/realpath.cpp
index b5fb2d7f14902..f23b705d0cdbd 100644
--- a/libc/src/stdlib/linux/realpath.cpp
+++ b/libc/src/stdlib/linux/realpath.cpp
@@ -64,9 +64,6 @@ class ResolvedPath {
 
   // Adds a single component to the end of this path.
   ErrorOr<Ok> push_component(cpp::string_view component) {
-    if (component.size() > NAME_MAX)
-      return Error(ENAMETOOLONG);
-
     if (!is_root()) {
       if (ErrorOr<Ok> res = push_raw(PATH_SEP); !res)
         return res;
diff --git a/libc/test/src/stdlib/realpath_test.cpp b/libc/test/src/stdlib/realpath_test.cpp
index 62d7f14c53c8d..515ef37be410d 100644
--- a/libc/test/src/stdlib/realpath_test.cpp
+++ b/libc/test/src/stdlib/realpath_test.cpp
@@ -42,36 +42,29 @@ TEST_F(LlvmLibcRealpathTest, ErrorsWithNoEntryIfEmptyPath) {
   ASSERT_ERRNO_EQ(ENOENT);
 }
 
-TEST_F(LlvmLibcRealpathTest, OkIfPathIsExactlyMaxSize) {
+TEST_F(LlvmLibcRealpathTest, OkIfPathArgIsExactlyMaxSize) {
   // PATH_MAX counts null terminator, so construct a path of size PATH_MAX-1.
-  cpp::string s(PATH_MAX - 1, 'a');
-  for (size_t i = 0; i < s.size(); i += NAME_MAX + 1)
-    s[i] = '/';
+  cpp::string s(PATH_MAX - 1, '/');
+  for (size_t i = 1; i < s.size(); i += 2)
+    s[i] = '.';
 
-  ASSERT_STREQ(realpath_buffered(s), s.c_str());
+  ASSERT_STREQ(realpath_buffered(s), "/");
 }
 
-TEST_F(LlvmLibcRealpathTest, ErrorsWithNameTooLongIfPathExceedsMaxSize) {
-  // PATH_MAX counts null terminator, so construct a path of size PATH_MAX.
-  cpp::string s(PATH_MAX, 'a');
-  for (size_t i = 0; i < s.size(); i += NAME_MAX + 1)
+TEST_F(LlvmLibcRealpathTest, OkIfResolvedPathIsExactlyMaxSize) {
+  // PATH_MAX counts null terminator, so construct a path of size PATH_MAX-1.
+  cpp::string s(PATH_MAX - 1, 'a');
+  for (size_t i = 0; i < s.size() - 1; i += 8)
     s[i] = '/';
 
-  ASSERT_EQ(realpath_buffered(s), nullptr);
-  ASSERT_ERRNO_EQ(ENAMETOOLONG);
-}
-
-TEST_F(LlvmLibcRealpathTest, OkWhenComponentLengthIsExactlyNameMax) {
-  cpp::string s(NAME_MAX + 1, 'a'); // +1 for leading "/"
-  s[0] = '/';
-
   ASSERT_STREQ(realpath_buffered(s), s.c_str());
 }
 
-TEST_F(LlvmLibcRealpathTest, ErrorsIfComponentExceedsNameMax) {
-  constexpr size_t COMPONENT_SIZE = NAME_MAX + 1;
-  cpp::string s(COMPONENT_SIZE + 1, 'a'); // +1 for leading "/"
-  s[0] = '/';
+TEST_F(LlvmLibcRealpathTest, ErrorsWithNameTooLongIfPathArgExceedsMaxSize) {
+  // PATH_MAX counts null terminator, so construct a path of size PATH_MAX.
+  cpp::string s(PATH_MAX, '/');
+  for (size_t i = 1; i < s.size(); i += 2)
+    s[i] = '.';
 
   ASSERT_EQ(realpath_buffered(s), nullptr);
   ASSERT_ERRNO_EQ(ENAMETOOLONG);

>From e3ed1329db69c8f3bd9e1d1b33f50a8df1bf0aff Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Thu, 18 Jun 2026 12:22:12 -0700
Subject: [PATCH 05/10] Simplify is_root.

---
 libc/src/stdlib/linux/realpath.cpp | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

diff --git a/libc/src/stdlib/linux/realpath.cpp b/libc/src/stdlib/linux/realpath.cpp
index f23b705d0cdbd..3213e3c095156 100644
--- a/libc/src/stdlib/linux/realpath.cpp
+++ b/libc/src/stdlib/linux/realpath.cpp
@@ -29,6 +29,9 @@ namespace {
 // Separator character for POSIX paths.
 constexpr char PATH_SEP = '/';
 
+// Separator for POSIX paths, as a `cpp::string_view`.
+constexpr cpp::string_view PATH_SEP_STRING = "/";
+
 // Dummy struct to represent success in `ErrorOr` when no value is needed.
 struct Ok {};
 
@@ -51,7 +54,7 @@ class ResolvedPath {
     size_ = 1;
   }
 
-  bool is_root() const { return size_ == 1; }
+  bool is_root() const { return view() == PATH_SEP_STRING; }
 
   ErrorOr<Ok> set_to_cwd() { return Error(ENOSYS); }
 
@@ -65,7 +68,7 @@ class ResolvedPath {
   // Adds a single component to the end of this path.
   ErrorOr<Ok> push_component(cpp::string_view component) {
     if (!is_root()) {
-      if (ErrorOr<Ok> res = push_raw(PATH_SEP); !res)
+      if (ErrorOr<Ok> res = push_raw(PATH_SEP_STRING); !res)
         return res;
     }
 
@@ -84,10 +87,6 @@ class ResolvedPath {
     return Ok{};
   }
 
-  ErrorOr<Ok> push_raw(char value) {
-    return push_raw(cpp::string_view(&value, 1));
-  }
-
   // Current size of the path stored in `buf_`.
   size_t size_;
 

>From 0d31ab5d2b61065a8bec866e0b54c1c0134960ae Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Tue, 23 Jun 2026 14:23:04 -0700
Subject: [PATCH 06/10] Use cpp::string instead of manual buffer management

---
 libc/src/__support/CPP/string.h             | 44 ++++++++++++---
 libc/src/stdlib/linux/CMakeLists.txt        |  2 +-
 libc/src/stdlib/linux/realpath.cpp          | 61 +++++++++------------
 libc/test/src/__support/CPP/CMakeLists.txt  |  1 +
 libc/test/src/__support/CPP/string_test.cpp | 20 +++++++
 5 files changed, 84 insertions(+), 44 deletions(-)

diff --git a/libc/src/__support/CPP/string.h b/libc/src/__support/CPP/string.h
index 04f0e9538d03e..541dbe6552110 100644
--- a/libc/src/__support/CPP/string.h
+++ b/libc/src/__support/CPP/string.h
@@ -23,6 +23,20 @@
 
 namespace LIBC_NAMESPACE_DECL {
 namespace cpp {
+namespace {
+
+char *realloc_or_die(char *ptr, size_t size) {
+  void *new_ptr = ::realloc(ptr, size);
+  if (new_ptr == nullptr) {
+    // Out of memory: this is not handled in current implementation,
+    // We trap the program and exits.
+    __builtin_trap();
+  }
+
+  return reinterpret_cast<char *>(new_ptr);
+}
+
+} // namespace
 
 // This class mimics std::string but does not intend to be a full fledged
 // implementation. Most notably it does not provide support for character traits
@@ -133,15 +147,9 @@ class string {
     if (new_capacity < SIZE_MAX / 11)
       new_capacity = new_capacity * 11 / 8;
 
-    if (void *Ptr = ::realloc(buffer_ == get_empty_string() ? nullptr : buffer_,
-                              new_capacity)) {
-      buffer_ = static_cast<char *>(Ptr);
-      capacity_ = new_capacity;
-      return;
-    }
-    // Out of memory: this is not handled in current implementation,
-    // We trap the program and exits.
-    __builtin_trap();
+    buffer_ = realloc_or_die(buffer_ == get_empty_string() ? nullptr : buffer_,
+                             new_capacity);
+    capacity_ = new_capacity;
   }
 
   LIBC_INLINE void resize(size_t size) {
@@ -153,6 +161,24 @@ class string {
     set_size_and_add_null_character(size);
   }
 
+  // Releases the backing C-string.
+  //
+  // The returned pointer must be free'd by the caller.
+  // This is a non-standard extension to std::string.
+  LIBC_INLINE char *release_c_str() {
+    if (buffer_ == get_empty_string()) {
+      // Ensure the buffer is heap allocated,
+      // so that it may later be passed to `free`.
+      char *res = realloc_or_die(nullptr, 1);
+      res[0] = '\0';
+      return res;
+    }
+
+    char *res = buffer_;
+    reset_no_deallocate();
+    return res;
+  }
+
   LIBC_INLINE string &operator+=(const string &rhs) {
     const size_t new_size = size_ + rhs.size();
     reserve(new_size);
diff --git a/libc/src/stdlib/linux/CMakeLists.txt b/libc/src/stdlib/linux/CMakeLists.txt
index 71c890a4ecf43..0800145301638 100644
--- a/libc/src/stdlib/linux/CMakeLists.txt
+++ b/libc/src/stdlib/linux/CMakeLists.txt
@@ -18,9 +18,9 @@ add_entrypoint_object(
   DEPENDS
     libc.hdr.errno_macros
     libc.hdr.limits_macros
-    libc.src.__support.alloc_checker
     libc.hdr.types.size_t
     libc.src.__support.common
+    libc.src.__support.CPP.string
     libc.src.__support.CPP.string_view
     libc.src.__support.error_or
     libc.src.__support.libc_errno
diff --git a/libc/src/stdlib/linux/realpath.cpp b/libc/src/stdlib/linux/realpath.cpp
index 3213e3c095156..ef26ad74b52d5 100644
--- a/libc/src/stdlib/linux/realpath.cpp
+++ b/libc/src/stdlib/linux/realpath.cpp
@@ -15,8 +15,8 @@
 #include "hdr/errno_macros.h"
 #include "hdr/limits_macros.h"
 #include "hdr/types/size_t.h"
+#include "src/__support/CPP/string.h"
 #include "src/__support/CPP/string_view.h"
-#include "src/__support/alloc-checker.h"
 #include "src/__support/common.h"
 #include "src/__support/error_or.h"
 #include "src/__support/libc_errno.h"
@@ -49,20 +49,19 @@ class ResolvedPath {
 public:
   ResolvedPath() { set_to_root(); }
 
-  void set_to_root() {
-    buf_[0] = PATH_SEP;
-    size_ = 1;
-  }
+  void set_to_root() { path_ = PATH_SEP_STRING; }
 
-  bool is_root() const { return view() == PATH_SEP_STRING; }
+  bool is_root() const { return path_ == PATH_SEP_STRING; }
 
   ErrorOr<Ok> set_to_cwd() { return Error(ENOSYS); }
 
+  // Removes the trailing path component.
   void set_to_parent() {
-    size_t sep_index = view().find_last_of(PATH_SEP);
+    size_t sep_index = cpp::string_view(path_).find_last_of(PATH_SEP);
 
-    // Ensure we maintain the root separator.
-    size_ = sep_index == 0 ? 1 : sep_index;
+    // Never move past the root separator. For example,
+    // ensures that set_to_parent on "/hello" only resizes to "/".
+    path_.resize(sep_index >= 1 ? sep_index : 1);
   }
 
   // Adds a single component to the end of this path.
@@ -75,24 +74,28 @@ class ResolvedPath {
     return push_raw(component);
   }
 
-  cpp::string_view view() const { return cpp::string_view(buf_, size_); }
+  // Releases ownership of the underlying C-string and resets this path.
+  //
+  // Must be free'd by the caller.
+  char *release() { return path_.release_c_str(); }
+
+  // Copies the content of this path to `dst`.
+  void copy_to(char *dst) {
+    inline_memcpy(dst, path_.c_str(), path_.size() + 1);
+  }
 
 private:
   ErrorOr<Ok> push_raw(cpp::string_view value) {
-    if (value.size() > sizeof(buf_) - size_)
+    // -1 because PATH_MAX includes a null-terminator.
+    size_t remaining_bytes = (PATH_MAX - 1) - path_.size();
+    if (value.size() > remaining_bytes)
       return Error(ENAMETOOLONG);
 
-    inline_memcpy(buf_ + size_, value.data(), value.size());
-    size_ += value.size();
+    path_ += value;
     return Ok{};
   }
 
-  // Current size of the path stored in `buf_`.
-  size_t size_;
-
-  // `PATH_MAX` includes a null-terminator in its count,
-  // so use `PATH_MAX - 1` here as `ResolvedPath` is not null-terminated.
-  char buf_[PATH_MAX - 1];
+  cpp::string path_;
 };
 
 // A view over path components yet to be processed by realpath.
@@ -141,20 +144,6 @@ class PendingPath {
   cpp::string_view view_;
 };
 
-ErrorOr<char *> copy_or_allocate_cstr(char *dst, cpp::string_view src) {
-  if (dst == nullptr) {
-    AllocChecker ac;
-    // `dst` is safe to return and let the caller `free()`, since AllocChecker
-    // delegates to malloc, and this value is trivially destructible.
-    dst = new (ac) char[src.size() + 1];
-    if (!ac)
-      return Error(ENOMEM);
-  }
-  inline_memcpy(dst, src.data(), src.size());
-  dst[src.size()] = '\0';
-  return dst;
-}
-
 ErrorOr<Ok> resolve_path(PendingPath &pending_path,
                          ResolvedPath &resolved_path) {
   while (!pending_path.empty()) {
@@ -197,7 +186,11 @@ ErrorOr<char *> realpath_impl(const char *__restrict path_cstr,
   if (ErrorOr<Ok> res = resolve_path(pending_path, resolved_path); !res)
     return Error(res.error());
 
-  return copy_or_allocate_cstr(resolved_path_buf, resolved_path.view());
+  if (resolved_path_buf != nullptr) {
+    resolved_path.copy_to(resolved_path_buf);
+    return resolved_path_buf;
+  }
+  return resolved_path.release();
 }
 
 } // namespace
diff --git a/libc/test/src/__support/CPP/CMakeLists.txt b/libc/test/src/__support/CPP/CMakeLists.txt
index 1d47455dcd31f..dec097e776efd 100644
--- a/libc/test/src/__support/CPP/CMakeLists.txt
+++ b/libc/test/src/__support/CPP/CMakeLists.txt
@@ -167,6 +167,7 @@ add_libc_test(
   SRCS
     string_test.cpp
   DEPENDS
+  libc.hdr.func.free
   libc.src.__support.CPP.string
   libc.src.__support.CPP.string_view
 )
diff --git a/libc/test/src/__support/CPP/string_test.cpp b/libc/test/src/__support/CPP/string_test.cpp
index e606ab127adc2..5e44a2b11688a 100644
--- a/libc/test/src/__support/CPP/string_test.cpp
+++ b/libc/test/src/__support/CPP/string_test.cpp
@@ -6,6 +6,7 @@
 //
 //===----------------------------------------------------------------------===//
 
+#include "hdr/func/free.h"
 #include "src/__support/CPP/string.h"
 #include "test/UnitTest/Test.h"
 
@@ -186,6 +187,25 @@ TEST(LlvmLibcStringTest, ConcatWithCString) {
   ASSERT_STREQ(("a" + string("b")).c_str(), "ab");
 }
 
+TEST(LlvmLibcStringTest, ReleaseCStrResetsString) {
+  string s("abc");
+
+  char *cstr = s.release_c_str();
+  ASSERT_STREQ(cstr, "abc");
+  ASSERT_STREQ(s.c_str(), "");
+  free(cstr);
+}
+
+TEST(LlvmLibcStringTest, ReleaseCStrOnDefaultString) {
+  string s;
+
+  char *cstr = s.release_c_str();
+  ASSERT_STREQ(cstr, "");
+  ASSERT_STREQ(s.c_str(), "");
+  ASSERT_NE(cstr, s.data()); // Different pointers.
+  free(cstr);
+}
+
 TEST(LlvmLibcStringTest, Comparison) {
   // Here we simply check that comparison of string and string_view have the
   // same semantic.

>From 6b6ce1bf1677069b93c5ecde2a04ae1e65ccc98d Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Fri, 26 Jun 2026 11:39:47 -0700
Subject: [PATCH 07/10] Move generic path functions to OSUtil/path.h

---
 libc/src/__support/CPP/string.h               |  5 +++
 libc/src/__support/OSUtil/CMakeLists.txt      | 29 +++++++++-----
 .../src/__support/OSUtil/linux/CMakeLists.txt |  9 +++++
 libc/src/__support/OSUtil/linux/path.h        | 39 +++++++++++++++++++
 libc/src/__support/OSUtil/path.h              | 23 +++++++++++
 libc/src/stdlib/linux/CMakeLists.txt          |  1 +
 libc/src/stdlib/linux/realpath.cpp            | 29 ++++++--------
 libc/test/src/__support/CPP/string_test.cpp   |  9 ++++-
 8 files changed, 115 insertions(+), 29 deletions(-)
 create mode 100644 libc/src/__support/OSUtil/linux/path.h
 create mode 100644 libc/src/__support/OSUtil/path.h

diff --git a/libc/src/__support/CPP/string.h b/libc/src/__support/CPP/string.h
index 541dbe6552110..823a5f94cb7e7 100644
--- a/libc/src/__support/CPP/string.h
+++ b/libc/src/__support/CPP/string.h
@@ -90,6 +90,11 @@ class string {
     return (*this) += other;
   }
 
+  LIBC_INLINE string &operator=(char other) {
+    resize(0);
+    return (*this) += other;
+  }
+
   LIBC_INLINE string &operator=(string &&other) {
     buffer_ = other.buffer_;
     size_ = other.size_;
diff --git a/libc/src/__support/OSUtil/CMakeLists.txt b/libc/src/__support/OSUtil/CMakeLists.txt
index 94d1042ccbb4a..da5158a850bef 100644
--- a/libc/src/__support/OSUtil/CMakeLists.txt
+++ b/libc/src/__support/OSUtil/CMakeLists.txt
@@ -1,17 +1,26 @@
 if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${LIBC_TARGET_OS})
   return()
 endif()
-
 add_subdirectory(${LIBC_TARGET_OS})
+
 set(target_os_util libc.src.__support.OSUtil.${LIBC_TARGET_OS}.${LIBC_TARGET_OS}_util)
-if(NOT TARGET ${target_os_util})
-  return()
+if(TARGET ${target_os_util})
+  add_object_library(
+    osutil
+    ALIAS
+      ${target_os_util}
+    DEPENDS
+      ${target_os_util}
+  )
 endif()
 
-add_object_library(
-  osutil
-  ALIAS
-    ${target_os_util}
-  DEPENDS
-    ${target_os_util}
-)
+set(target_path libc.src.__support.OSUtil.${LIBC_TARGET_OS}.path)
+if(TARGET ${target_path})
+  add_header_library(
+    path
+    HDRS
+      path.h
+    DEPENDS
+      ${target_path}
+  )
+endif()
diff --git a/libc/src/__support/OSUtil/linux/CMakeLists.txt b/libc/src/__support/OSUtil/linux/CMakeLists.txt
index 525e62c7ca23b..3dc8c31212e30 100644
--- a/libc/src/__support/OSUtil/linux/CMakeLists.txt
+++ b/libc/src/__support/OSUtil/linux/CMakeLists.txt
@@ -43,6 +43,15 @@ add_header_library(
     libc.src.__support.threads.callonce
 )
 
+add_header_library(
+  path
+  HDRS
+    path.h
+  DEPENDS
+    libc.src.__support.common
+    libc.src.__support.CPP.string_view
+)
+
 add_header_library(
   sysinfo
   HDRS
diff --git a/libc/src/__support/OSUtil/linux/path.h b/libc/src/__support/OSUtil/linux/path.h
new file mode 100644
index 0000000000000..955751c308793
--- /dev/null
+++ b/libc/src/__support/OSUtil/linux/path.h
@@ -0,0 +1,39 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 linux path helpers.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC___SUPPORT_OSUTIL_LINUX_PATH_H
+#define LLVM_LIBC_SRC___SUPPORT_OSUTIL_LINUX_PATH_H
+
+#include "src/__support/CPP/string_view.h"
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+namespace path {
+
+// Separator character for linux paths.
+constexpr char SEPARATOR = '/';
+
+// Whether `path` is absolute.
+LIBC_INLINE constexpr bool is_absolute(cpp::string_view path) {
+  return path.starts_with(SEPARATOR);
+}
+
+// Whether `path` is absolute.
+LIBC_INLINE constexpr bool is_root(cpp::string_view path) {
+  return path.size() == 1 && path[0] == SEPARATOR;
+}
+
+} // namespace path
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC___SUPPORT_OSUTIL_LINUX_PATH_H
diff --git a/libc/src/__support/OSUtil/path.h b/libc/src/__support/OSUtil/path.h
new file mode 100644
index 0000000000000..ba7cf6d0cbc81
--- /dev/null
+++ b/libc/src/__support/OSUtil/path.h
@@ -0,0 +1,23 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 path helpers.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC___SUPPORT_OSUTIL_PATH_H
+#define LLVM_LIBC_SRC___SUPPORT_OSUTIL_PATH_H
+
+#if defined(__linux__)
+#include "src/__support/OSUtil/linux/path.h"
+#else
+#error Path unavailable on this platfrom
+#endif
+
+#endif // LLVM_LIBC_SRC___SUPPORT_OSUTIL_PATH_H
diff --git a/libc/src/stdlib/linux/CMakeLists.txt b/libc/src/stdlib/linux/CMakeLists.txt
index 0800145301638..b287fc3a1a8f6 100644
--- a/libc/src/stdlib/linux/CMakeLists.txt
+++ b/libc/src/stdlib/linux/CMakeLists.txt
@@ -25,6 +25,7 @@ add_entrypoint_object(
     libc.src.__support.error_or
     libc.src.__support.libc_errno
     libc.src.__support.macros.config
+    libc.src.__support.OSUtil.path
     libc.src.string.memory_utils.inline_memcpy
 )
 
diff --git a/libc/src/stdlib/linux/realpath.cpp b/libc/src/stdlib/linux/realpath.cpp
index ef26ad74b52d5..d6c1a148eb35f 100644
--- a/libc/src/stdlib/linux/realpath.cpp
+++ b/libc/src/stdlib/linux/realpath.cpp
@@ -17,6 +17,7 @@
 #include "hdr/types/size_t.h"
 #include "src/__support/CPP/string.h"
 #include "src/__support/CPP/string_view.h"
+#include "src/__support/OSUtil/path.h"
 #include "src/__support/common.h"
 #include "src/__support/error_or.h"
 #include "src/__support/libc_errno.h"
@@ -26,18 +27,9 @@
 namespace LIBC_NAMESPACE_DECL {
 namespace {
 
-// Separator character for POSIX paths.
-constexpr char PATH_SEP = '/';
-
-// Separator for POSIX paths, as a `cpp::string_view`.
-constexpr cpp::string_view PATH_SEP_STRING = "/";
-
 // Dummy struct to represent success in `ErrorOr` when no value is needed.
 struct Ok {};
 
-// Whether a path is absolute.
-bool is_absolute(cpp::string_view path) { return path.starts_with(PATH_SEP); }
-
 // Container for a fully resolved, canonical path.
 //
 // The contained path is always in its canonical form. It is:
@@ -49,15 +41,13 @@ class ResolvedPath {
 public:
   ResolvedPath() { set_to_root(); }
 
-  void set_to_root() { path_ = PATH_SEP_STRING; }
-
-  bool is_root() const { return path_ == PATH_SEP_STRING; }
+  void set_to_root() { path_ = path::SEPARATOR; }
 
   ErrorOr<Ok> set_to_cwd() { return Error(ENOSYS); }
 
   // Removes the trailing path component.
   void set_to_parent() {
-    size_t sep_index = cpp::string_view(path_).find_last_of(PATH_SEP);
+    size_t sep_index = cpp::string_view(path_).find_last_of(path::SEPARATOR);
 
     // Never move past the root separator. For example,
     // ensures that set_to_parent on "/hello" only resizes to "/".
@@ -66,8 +56,8 @@ class ResolvedPath {
 
   // Adds a single component to the end of this path.
   ErrorOr<Ok> push_component(cpp::string_view component) {
-    if (!is_root()) {
-      if (ErrorOr<Ok> res = push_raw(PATH_SEP_STRING); !res)
+    if (!path::is_root(path_)) {
+      if (ErrorOr<Ok> res = push_raw(path::SEPARATOR); !res)
         return res;
     }
 
@@ -95,6 +85,9 @@ class ResolvedPath {
     return Ok{};
   }
 
+  // Adds a single component to the end of this path.
+  ErrorOr<Ok> push_raw(char c) { return push_raw(cpp::string_view(&c, 1)); }
+
   cpp::string path_;
 };
 
@@ -123,14 +116,14 @@ class PendingPath {
   cpp::string_view advance_component() {
     const cpp::string_view path = view_;
 
-    const size_t component_start = path.find_first_not_of(PATH_SEP);
+    const size_t component_start = path.find_first_not_of(path::SEPARATOR);
     if (component_start == cpp::string_view::npos) {
       view_ = "";
       return "";
     }
 
     const size_t component_end =
-        path.find_first_of(PATH_SEP, /* From = */ component_start);
+        path.find_first_of(path::SEPARATOR, /* From = */ component_start);
     if (component_end == cpp::string_view::npos) {
       view_ = "";
       return path.substr(component_start);
@@ -178,7 +171,7 @@ ErrorOr<char *> realpath_impl(const char *__restrict path_cstr,
   PendingPath pending_path(path);
 
   ResolvedPath resolved_path;
-  if (!is_absolute(path)) {
+  if (!path::is_absolute(path)) {
     if (ErrorOr<Ok> res = resolved_path.set_to_cwd(); !res)
       return Error(res.error());
   }
diff --git a/libc/test/src/__support/CPP/string_test.cpp b/libc/test/src/__support/CPP/string_test.cpp
index 5e44a2b11688a..cc7c47c55e1b6 100644
--- a/libc/test/src/__support/CPP/string_test.cpp
+++ b/libc/test/src/__support/CPP/string_test.cpp
@@ -78,7 +78,7 @@ TEST(LlvmLibcStringTest, InitializeRepeatedChar) {
   ASSERT_EQ(string_view(s), string_view("1111"));
 }
 
-TEST(LlvmLibcStringTest, InitializeZeorChar) {
+TEST(LlvmLibcStringTest, InitializeZeroChar) {
   const string s(0, '1');
   ASSERT_TRUE(s.empty());
 }
@@ -136,6 +136,13 @@ TEST(LlvmLibcStringTest, StringViewAssign) {
   ASSERT_EQ(s.back(), 'b');
 }
 
+TEST(LlvmLibcStringTest, CharacterAssign) {
+  string s("abc");
+  s = 'a';
+  ASSERT_EQ(s.size(), size_t(1));
+  ASSERT_STREQ(s.c_str(), "a");
+}
+
 TEST(LlvmLibcStringTest, Concat) {
   const char *const str = "abc";
   string a(str);

>From 40bdbe7b04818f6f62285d4c50d562c3a82730d9 Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Fri, 26 Jun 2026 17:02:15 -0700
Subject: [PATCH 08/10] Better error message

---
 libc/src/__support/OSUtil/path.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/libc/src/__support/OSUtil/path.h b/libc/src/__support/OSUtil/path.h
index ba7cf6d0cbc81..7d3d3d6c19b96 100644
--- a/libc/src/__support/OSUtil/path.h
+++ b/libc/src/__support/OSUtil/path.h
@@ -17,7 +17,7 @@
 #if defined(__linux__)
 #include "src/__support/OSUtil/linux/path.h"
 #else
-#error Path unavailable on this platfrom
+#error path.h unavailable on this platfrom
 #endif
 
 #endif // LLVM_LIBC_SRC___SUPPORT_OSUTIL_PATH_H

>From 167a9e6bc6796c52676abd25328e01d4c52e0737 Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Fri, 26 Jun 2026 17:03:43 -0700
Subject: [PATCH 09/10] Fix comment

---
 libc/src/stdlib/linux/realpath.cpp | 1 -
 1 file changed, 1 deletion(-)

diff --git a/libc/src/stdlib/linux/realpath.cpp b/libc/src/stdlib/linux/realpath.cpp
index d6c1a148eb35f..e4e8db3263ad4 100644
--- a/libc/src/stdlib/linux/realpath.cpp
+++ b/libc/src/stdlib/linux/realpath.cpp
@@ -85,7 +85,6 @@ class ResolvedPath {
     return Ok{};
   }
 
-  // Adds a single component to the end of this path.
   ErrorOr<Ok> push_raw(char c) { return push_raw(cpp::string_view(&c, 1)); }
 
   cpp::string path_;

>From e63859f1b0f8ca730dc314bfe030251db3ffc219 Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Mon, 29 Jun 2026 15:31:29 -0700
Subject: [PATCH 10/10] Remove Ok monotype for error handling.

---
 libc/src/stdlib/linux/CMakeLists.txt |  1 +
 libc/src/stdlib/linux/realpath.cpp   | 38 ++++++++++++++--------------
 2 files changed, 20 insertions(+), 19 deletions(-)

diff --git a/libc/src/stdlib/linux/CMakeLists.txt b/libc/src/stdlib/linux/CMakeLists.txt
index b287fc3a1a8f6..a796e54da2fce 100644
--- a/libc/src/stdlib/linux/CMakeLists.txt
+++ b/libc/src/stdlib/linux/CMakeLists.txt
@@ -20,6 +20,7 @@ add_entrypoint_object(
     libc.hdr.limits_macros
     libc.hdr.types.size_t
     libc.src.__support.common
+    libc.src.__support.CPP.optional
     libc.src.__support.CPP.string
     libc.src.__support.CPP.string_view
     libc.src.__support.error_or
diff --git a/libc/src/stdlib/linux/realpath.cpp b/libc/src/stdlib/linux/realpath.cpp
index e4e8db3263ad4..2410816532d4e 100644
--- a/libc/src/stdlib/linux/realpath.cpp
+++ b/libc/src/stdlib/linux/realpath.cpp
@@ -15,6 +15,7 @@
 #include "hdr/errno_macros.h"
 #include "hdr/limits_macros.h"
 #include "hdr/types/size_t.h"
+#include "src/__support/CPP/optional.h"
 #include "src/__support/CPP/string.h"
 #include "src/__support/CPP/string_view.h"
 #include "src/__support/OSUtil/path.h"
@@ -27,9 +28,6 @@
 namespace LIBC_NAMESPACE_DECL {
 namespace {
 
-// Dummy struct to represent success in `ErrorOr` when no value is needed.
-struct Ok {};
-
 // Container for a fully resolved, canonical path.
 //
 // The contained path is always in its canonical form. It is:
@@ -43,7 +41,7 @@ class ResolvedPath {
 
   void set_to_root() { path_ = path::SEPARATOR; }
 
-  ErrorOr<Ok> set_to_cwd() { return Error(ENOSYS); }
+  cpp::optional<Error> set_to_cwd() { return Error(ENOSYS); }
 
   // Removes the trailing path component.
   void set_to_parent() {
@@ -55,10 +53,10 @@ class ResolvedPath {
   }
 
   // Adds a single component to the end of this path.
-  ErrorOr<Ok> push_component(cpp::string_view component) {
+  cpp::optional<Error> push_component(cpp::string_view component) {
     if (!path::is_root(path_)) {
-      if (ErrorOr<Ok> res = push_raw(path::SEPARATOR); !res)
-        return res;
+      if (cpp::optional<Error> err = push_raw(path::SEPARATOR); err)
+        return err;
     }
 
     return push_raw(component);
@@ -75,17 +73,19 @@ class ResolvedPath {
   }
 
 private:
-  ErrorOr<Ok> push_raw(cpp::string_view value) {
+  cpp::optional<Error> push_raw(cpp::string_view value) {
     // -1 because PATH_MAX includes a null-terminator.
     size_t remaining_bytes = (PATH_MAX - 1) - path_.size();
     if (value.size() > remaining_bytes)
       return Error(ENAMETOOLONG);
 
     path_ += value;
-    return Ok{};
+    return cpp::nullopt;
   }
 
-  ErrorOr<Ok> push_raw(char c) { return push_raw(cpp::string_view(&c, 1)); }
+  cpp::optional<Error> push_raw(char c) {
+    return push_raw(cpp::string_view(&c, 1));
+  }
 
   cpp::string path_;
 };
@@ -136,8 +136,8 @@ class PendingPath {
   cpp::string_view view_;
 };
 
-ErrorOr<Ok> resolve_path(PendingPath &pending_path,
-                         ResolvedPath &resolved_path) {
+cpp::optional<Error> resolve_path(PendingPath &pending_path,
+                                  ResolvedPath &resolved_path) {
   while (!pending_path.empty()) {
     cpp::string_view component = pending_path.advance_component();
     if (component.empty() || component == ".")
@@ -148,11 +148,11 @@ ErrorOr<Ok> resolve_path(PendingPath &pending_path,
       continue;
     }
 
-    if (ErrorOr<Ok> res = resolved_path.push_component(component); !res)
-      return res;
+    if (cpp::optional<Error> err = resolved_path.push_component(component); err)
+      return err;
   }
 
-  return Ok{};
+  return cpp::nullopt;
 }
 
 ErrorOr<char *> realpath_impl(const char *__restrict path_cstr,
@@ -171,12 +171,12 @@ ErrorOr<char *> realpath_impl(const char *__restrict path_cstr,
 
   ResolvedPath resolved_path;
   if (!path::is_absolute(path)) {
-    if (ErrorOr<Ok> res = resolved_path.set_to_cwd(); !res)
-      return Error(res.error());
+    if (cpp::optional<Error> err = resolved_path.set_to_cwd(); err)
+      return *err;
   }
 
-  if (ErrorOr<Ok> res = resolve_path(pending_path, resolved_path); !res)
-    return Error(res.error());
+  if (cpp::optional<Error> err = resolve_path(pending_path, resolved_path); err)
+    return *err;
 
   if (resolved_path_buf != nullptr) {
     resolved_path.copy_to(resolved_path_buf);



More information about the libc-commits mailing list