[libc-commits] [libc] [llvm] [libc][realpath] Follow symlinks (PR #212164)

Jackson Stogel via libc-commits libc-commits at lists.llvm.org
Sun Jul 26 19:23:21 PDT 2026


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

>From 517d15a6e9559db52b5a376ab2e5b9f6121ca055 Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Sun, 26 Jul 2026 11:35:09 -0700
Subject: [PATCH 1/2] [libc][cpp::string] Implement replace

---
 libc/src/__support/CPP/CMakeLists.txt         |   3 +
 libc/src/__support/CPP/string.h               |  66 ++++++++++-
 libc/test/src/__support/CPP/string_test.cpp   | 111 ++++++++++++++++++
 .../llvm-project-overlay/libc/BUILD.bazel     |   3 +
 4 files changed, 177 insertions(+), 6 deletions(-)

diff --git a/libc/src/__support/CPP/CMakeLists.txt b/libc/src/__support/CPP/CMakeLists.txt
index 6ae9b44437e52..5b89257418732 100644
--- a/libc/src/__support/CPP/CMakeLists.txt
+++ b/libc/src/__support/CPP/CMakeLists.txt
@@ -84,12 +84,15 @@ add_header_library(
   HDRS
     string.h
   DEPENDS
+    .algorithm
     .string_view
     libc.hdr.func.free
     libc.hdr.func.malloc
     libc.hdr.func.realloc
+    libc.hdr.stdint_proxy
     libc.src.__support.common
     libc.src.__support.integer_to_string
+    libc.src.__support.libc_assert
     libc.src.__support.macros.null_check
     libc.src.string.memory_utils.inline_memcpy
     libc.src.string.memory_utils.inline_memmove
diff --git a/libc/src/__support/CPP/string.h b/libc/src/__support/CPP/string.h
index 4b66f679c2ada..a82ab45487d16 100644
--- a/libc/src/__support/CPP/string.h
+++ b/libc/src/__support/CPP/string.h
@@ -12,8 +12,11 @@
 #include "hdr/func/free.h"
 #include "hdr/func/malloc.h"
 #include "hdr/func/realloc.h"
+#include "hdr/stdint_proxy.h"
+#include "src/__support/CPP/algorithm.h"
 #include "src/__support/CPP/string_view.h"
 #include "src/__support/integer_to_string.h" // IntegerToString
+#include "src/__support/libc_assert.h"
 #include "src/__support/macros/config.h"
 #include "src/__support/macros/null_check.h"
 #include "src/string/memory_utils/inline_memcpy.h"
@@ -36,6 +39,17 @@ char *realloc_or_die(char *ptr, size_t size) {
 
 char *malloc_or_die(size_t size) { return realloc_or_die(nullptr, size); }
 
+// Returns whether the address of a is less than or equal to the address of b.
+LIBC_INLINE bool ptr_le(const char *a, const char *b) {
+  return reinterpret_cast<uintptr_t>(a) <= reinterpret_cast<uintptr_t>(b);
+}
+
+// Whether the memory spanned by a and b have any overlap.
+LIBC_INLINE bool memory_overlaps(cpp::string_view a, cpp::string_view b) {
+  return !(ptr_le(b.data() + b.size(), a.data()) ||
+           ptr_le(a.data() + a.size(), b.data()));
+}
+
 } // namespace
 
 // This class mimics std::string but does not intend to be a full fledged
@@ -94,17 +108,21 @@ class string {
     return new_capacity;
   }
 
-  // Replaces the current buffer with a new larger one containing the first
-  // keep_prefix_size bytes of the current buffer concatenated with new_data.
+  // Replaces the current buffer with a new larger one that is the concatenation
+  // the first keep_prefix_size bytes of the current string, new_data, and the
+  // last keep_suffix_size bytes of the current string.
   LIBC_INLINE void grow_and_replace(size_t keep_prefix_size,
-                                    cpp::string_view new_data) {
-    size_t new_size = keep_prefix_size + new_data.size();
+                                    cpp::string_view new_data,
+                                    size_t keep_suffix_size) {
+    size_t new_size = keep_prefix_size + new_data.size() + keep_suffix_size;
     size_t new_capacity = amortized_capacity(new_size);
     char *new_buffer = malloc_or_die(new_capacity);
 
     inline_memcpy(new_buffer, buffer_, keep_prefix_size);
     inline_memcpy(new_buffer + keep_prefix_size, new_data.data(),
                   new_data.size());
+    inline_memcpy(new_buffer + keep_prefix_size + new_data.size(),
+                  buffer_ + size_ - keep_suffix_size, keep_suffix_size);
 
     move_assign_from_buffer(new_buffer, new_capacity, new_size);
     set_size_and_add_null_character(new_size);
@@ -138,7 +156,8 @@ class string {
     }
 
     if (capacity() < view.size()) {
-      grow_and_replace(/* keep_prefix_size= */ 0, view);
+      grow_and_replace(/* keep_prefix_size= */ 0, view,
+                       /* keep_suffix_size= */ 0);
       return *this;
     }
 
@@ -257,7 +276,8 @@ class string {
       return *this;
 
     if (capacity() - size_ < view.size()) {
-      grow_and_replace(/* keep_prefix_size= */ size_, view);
+      grow_and_replace(/* keep_prefix_size= */ size_, view,
+                       /* keep_suffix_size= */ 0);
       return *this;
     }
 
@@ -272,6 +292,40 @@ class string {
   LIBC_INLINE string &operator+=(const char c) {
     return append(string_view(&c, 1));
   }
+
+  // Replaces the span [pos, pos + count) in this string with `str`.
+  LIBC_INLINE string &replace(size_t pos, size_t count, string_view str) {
+    LIBC_ASSERT(pos <= size_); // Out of bounds.
+
+    count = min(count, size_ - pos);
+    size_t new_size = str.size() + size_ - count;
+
+    if (new_size > capacity()) {
+      grow_and_replace(/* keep_prefix_size= */ pos, str, size_ - pos - count);
+      return *this;
+    }
+
+    // If the input references a section of this string that will be edited,
+    // fall back to a slow approach. libc++ implements this efficiently via
+    // pointer arithmetic. If self-referential replace is used frequently,
+    // this can be updated to avoid the extra temporary.
+    bool has_overlap =
+        memory_overlaps(str, string_view(buffer_ + pos, size_ - pos));
+    string tmp;
+    if (LIBC_UNLIKELY(has_overlap)) {
+      // Save str in a temp string, then update the view to reference it.
+      tmp = str;
+      str = tmp;
+    }
+
+    if (str.size() != count)
+      inline_memmove(buffer_ + pos + str.size(), buffer_ + pos + count,
+                     size_ - pos - count);
+
+    inline_memcpy(buffer_ + pos, str.data(), str.size());
+    set_size_and_add_null_character(new_size);
+    return *this;
+  }
 };
 
 LIBC_INLINE bool operator==(const string &lhs, const string &rhs) {
diff --git a/libc/test/src/__support/CPP/string_test.cpp b/libc/test/src/__support/CPP/string_test.cpp
index 5896dbfff2f97..6eb6475a11e2d 100644
--- a/libc/test/src/__support/CPP/string_test.cpp
+++ b/libc/test/src/__support/CPP/string_test.cpp
@@ -339,3 +339,114 @@ TEST(LlvmLibcStringTest, SelfAppendAtCapacityTest) {
   ASSERT_EQ(s, expected);
   ASSERT_GT(cap_after_append, cap_before_append);
 }
+
+TEST(LlvmLibcStringTest, ReplaceWithSmallerString) {
+  string s("Hello world");
+
+  s.replace(3, 7, "orl"); // Replace "lo worl" with "orl"
+
+  EXPECT_STREQ(s.c_str(), "Helorld");
+}
+
+TEST(LlvmLibcStringTest, ReplaceWithSmallerSelfReferentialString) {
+  string s("Hello world");
+  string_view view = string_view(s).substr(7, 3); // "orl"
+
+  s.replace(3, 7, view); // Replace "lo worl" with "orl"
+
+  EXPECT_STREQ(s.c_str(), "Helorld");
+}
+
+TEST(LlvmLibcStringTest, ReplaceWithEqualLengthString) {
+  string s("Hello world");
+
+  s.replace(6, 3, " wo"); // Replace "wor" with " wo"
+
+  EXPECT_STREQ(s.c_str(), "Hello  wold");
+}
+
+TEST(LlvmLibcStringTest, ReplaceWithEqualLengthSelfReferentialString) {
+  string s("Hello world");
+  string_view view = string_view(s).substr(5, 3); // " wo"
+
+  s.replace(6, 3, view); // Replace "wor" with " wo"
+
+  EXPECT_STREQ(s.c_str(), "Hello  wold");
+}
+
+TEST(LlvmLibcStringTest, ReplaceWithLongerStringNoGrowth) {
+  string s("Hello world");
+  s.reserve(32); // Reserve enough space to where replace will not grow.
+  size_t capacity_before = s.capacity();
+
+  s.replace(6, 3, " worl"); // Replace "wor" with " worl"
+
+  EXPECT_STREQ(s.c_str(), "Hello  worlld");
+  EXPECT_EQ(s.capacity(), capacity_before);
+}
+
+TEST(LlvmLibcStringTest, ReplaceWithLongerSelfReferentialStringNoGrowth) {
+  string s = "Hello world";
+  s.reserve(32); // Reserve enough space to where replace will not grow.
+  size_t capacity_before = s.capacity();
+  string_view view = string_view(s).substr(5, 5); // " worl"
+
+  s.replace(6, 3, view); // Replace "wor" with " worl"
+
+  EXPECT_STREQ(s.c_str(), "Hello  worlld");
+  EXPECT_EQ(s.capacity(), capacity_before);
+}
+
+TEST(LlvmLibcStringTest, ReplaceWithLongerStringTriggeringGrow) {
+  string s("Hello placeholder world");
+  string_view placeholder = "placeholder";
+  size_t capacity_before = s.capacity();
+  string insert(s.capacity() - s.size() + placeholder.size() + 1, 'a');
+
+  s.replace(6, placeholder.size(), insert); // Replace "placeholder"
+
+  string expected = "Hello ";
+  expected += insert;
+  expected += " world";
+  EXPECT_EQ(s, expected);
+  EXPECT_GT(s.capacity(), capacity_before);
+}
+
+TEST(LlvmLibcStringTest, ReplaceFromEndOfStringNoGrowth) {
+  string s;
+  s.reserve(32);
+  size_t capacity_before = s.capacity();
+
+  s.replace(0, 0, "Hello world");
+
+  EXPECT_STREQ(s.c_str(), "Hello world");
+  EXPECT_EQ(s.capacity(), capacity_before);
+}
+
+TEST(LlvmLibcStringTest, ReplaceFromEndOfStringTriggeringGrowth) {
+  string s;
+  size_t capacity_before = s.capacity();
+
+  s.replace(0, 0, "Hello world");
+
+  EXPECT_STREQ(s.c_str(), "Hello world");
+  EXPECT_GT(s.capacity(), capacity_before);
+}
+
+TEST(LlvmLibcStringTest, ReplaceWithEmptyString) {
+  string s = "Hello world";
+  s.replace(2, 7, "");
+  EXPECT_STREQ(s.c_str(), "Held");
+}
+
+TEST(LlvmLibcStringTest, ReplaceAtEndOfString) {
+  string s = "Hello ";
+  s.replace(6, 0, "world");
+  EXPECT_STREQ(s.c_str(), "Hello world");
+}
+
+TEST(LlvmLibcStringTest, ReplaceWithLargeCountClampsSize) {
+  string s = "Hello world";
+  s.replace(0, 100, "goodbye");
+  EXPECT_STREQ(s.c_str(), "goodbye");
+}
diff --git a/utils/bazel/llvm-project-overlay/libc/BUILD.bazel b/utils/bazel/llvm-project-overlay/libc/BUILD.bazel
index bee6f7aa5766c..af389fb24f76d 100644
--- a/utils/bazel/llvm-project-overlay/libc/BUILD.bazel
+++ b/utils/bazel/llvm-project-overlay/libc/BUILD.bazel
@@ -1205,13 +1205,16 @@ libc_support_library(
     hdrs = ["src/__support/CPP/string.h"],
     deps = [
         ":__support_common",
+        ":__support_cpp_algorithm",
         ":__support_cpp_string_view",
         ":__support_integer_to_string",
+        ":__support_libc_assert",
         ":__support_macros_config",
         ":__support_macros_null_check",
         ":func_free",
         ":func_malloc",
         ":func_realloc",
+        ":hdr_stdint_proxy",
         ":string_memory_utils",
         ":string_utils",
     ],

>From 01b8b7cd0199a1b990b1509188ac6106ed45787e Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Sat, 25 Jul 2026 20:18:03 -0700
Subject: [PATCH 2/2] [libc][realpath] Follow symlinks

This commit updates realpath to call readlinkat on symlinks during path resolution. In order to do, it updates `PendingPath` to store a `cpp::string` that is prepended with a symlink target when resolved. This could be more efficient by using a `PATH_MAX` buffer and storing the path at the end of the buffer, but using a `cpp::string` avoids the pointer arithmetic and manual memory management that comes with that approach.
---
 libc/src/stdlib/linux/CMakeLists.txt   |   1 +
 libc/src/stdlib/linux/realpath.cpp     | 143 +++++++++++++++++++------
 libc/test/src/stdlib/CMakeLists.txt    |   1 +
 libc/test/src/stdlib/realpath_test.cpp | 123 +++++++++++++++++++++
 4 files changed, 235 insertions(+), 33 deletions(-)

diff --git a/libc/src/stdlib/linux/CMakeLists.txt b/libc/src/stdlib/linux/CMakeLists.txt
index e71e3bf3e4ddf..2580b086ee0c5 100644
--- a/libc/src/stdlib/linux/CMakeLists.txt
+++ b/libc/src/stdlib/linux/CMakeLists.txt
@@ -31,6 +31,7 @@ add_entrypoint_object(
     libc.src.__support.macros.config
     libc.src.__support.OSUtil.linux.stat.kernel_statx_types
     libc.src.__support.OSUtil.linux.syscall_wrappers.getcwd
+    libc.src.__support.OSUtil.linux.syscall_wrappers.readlink
     libc.src.__support.OSUtil.linux.syscall_wrappers.statx
     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 c7cccc9e4297d..614788a2a0188 100644
--- a/libc/src/stdlib/linux/realpath.cpp
+++ b/libc/src/stdlib/linux/realpath.cpp
@@ -23,6 +23,7 @@
 #include "src/__support/CPP/string_view.h"
 #include "src/__support/OSUtil/linux/stat/kernel_statx_types.h"
 #include "src/__support/OSUtil/linux/syscall_wrappers/getcwd.h"
+#include "src/__support/OSUtil/linux/syscall_wrappers/readlink.h"
 #include "src/__support/OSUtil/linux/syscall_wrappers/statx.h"
 #include "src/__support/OSUtil/path.h"
 #include "src/__support/common.h"
@@ -34,18 +35,27 @@
 namespace LIBC_NAMESPACE_DECL {
 namespace {
 
+#ifdef SYMLOOP_MAX
+constexpr size_t MAX_SYMLINK_FOLLOWS = SYMLOOP_MAX;
+#else
+// Maximum number of symlinks that may be followed during path resolution.
+// This is a large, arbitrary value consistent with other libc implementations.
+// Must be at least _POSIX_SYMLOOP_MAX (8). Ideally should be read from sysconf
+// so that it respects limits set by the system libc in overlay mode.
+constexpr size_t MAX_SYMLINK_FOLLOWS = 40;
+#endif
+
 // 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() { path_ = path::SEPARATOR; }
+  void set_to_root() { path = path::SEPARATOR; }
 
   cpp::optional<Error> set_to_cwd() {
     char buf[PATH_MAX];
@@ -59,22 +69,22 @@ class ResolvedPath {
     if (*ret <= 0)
       return Error(EIO);
 
-    path_ = cpp::string_view(buf, *ret - 1);
+    path = cpp::string_view(buf, *ret - 1);
     return cpp::nullopt;
   }
 
   // Removes the trailing path component.
   void set_to_parent() {
-    size_t sep_index = cpp::string_view(path_).find_last_of(path::SEPARATOR);
+    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 "/".
-    path_.resize(sep_index >= 1 ? sep_index : 1);
+    path.resize(sep_index >= 1 ? sep_index : 1);
   }
 
   // Adds a single component to the end of this path.
   cpp::optional<Error> push_component(cpp::string_view component) {
-    if (!path::is_root(path_)) {
+    if (!path::is_root(path)) {
       if (cpp::optional<Error> err = push_raw(path::SEPARATOR); err)
         return err;
     }
@@ -85,23 +95,21 @@ class ResolvedPath {
   // 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(); }
+  char *release() { return path.release_c_str(); }
 
-  const char *c_str() const { return path_.c_str(); }
+  const char *c_str() const { return path.c_str(); }
 
   // Copies the content of this path to `dst`.
-  void copy_to(char *dst) {
-    inline_memcpy(dst, path_.c_str(), path_.size() + 1);
-  }
+  void copy_to(char *dst) { inline_memcpy(dst, path.c_str(), path.size() + 1); }
 
 private:
   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();
+    size_t remaining_bytes = (PATH_MAX - 1) - path.size();
     if (value.size() > remaining_bytes)
       return Error(ENAMETOOLONG);
 
-    path_ += value;
+    path += value;
     return cpp::nullopt;
   }
 
@@ -109,7 +117,7 @@ class ResolvedPath {
     return push_raw(cpp::string_view(&c, 1));
   }
 
-  cpp::string path_;
+  cpp::string path;
 };
 
 // A view over path components yet to be processed by realpath.
@@ -122,40 +130,87 @@ class ResolvedPath {
 //   PendingPath p("./a/..");
 //   assert(p.advance_component() == ".");
 //   assert(p.advance_component() == "a");
+//
+//   p.prepend("b/c");
+//   assert(p.advance_component() == "b");
+//   assert(p.advance_component() == "c");
 //   assert(p.advance_component() == "..");
 //   assert(p.empty());
 //   ```
 class PendingPath {
 public:
-  explicit PendingPath(cpp::string_view path) : view_(path) {}
+  PendingPath() {}
 
   // Whether all path components have been consumed.
-  bool empty() const { return view_.empty(); }
+  bool empty() const { return cursor >= path.size(); }
 
   // Takes the next path component,
   // starting with the component closest to the root.
   cpp::string_view advance_component() {
-    const cpp::string_view path = view_;
+    cpp::string_view view = path;
 
-    const size_t component_start = path.find_first_not_of(path::SEPARATOR);
+    const size_t component_start =
+        view.find_first_not_of(path::SEPARATOR, /* From= */ cursor);
     if (component_start == cpp::string_view::npos) {
-      view_ = "";
+      cursor = path.size();
       return "";
     }
 
-    const size_t component_end =
-        path.find_first_of(path::SEPARATOR, /* From = */ component_start);
-    if (component_end == cpp::string_view::npos) {
-      view_ = "";
-      return path.substr(component_start);
+    size_t component_end =
+        view.find_first_of(path::SEPARATOR, /* From = */ component_start);
+    if (component_end == cpp::string_view::npos)
+      component_end = path.size();
+
+    cursor = component_end;
+    return view.substr(component_start, component_end - component_start);
+  }
+
+  // Prepends other_path to this path.
+  cpp::optional<Error> prepend(cpp::string_view other_path) {
+    size_t new_size = other_path.size() + path.size() - cursor;
+    if (new_size >= PATH_MAX)
+      return Error(ENAMETOOLONG);
+
+    if (other_path.size() <= cursor) {
+      // If the string to prepend fits in the unused prefix of path,
+      // just slot it in directly and move the cursor back.
+      size_t start = cursor - other_path.size();
+      path.replace(start, other_path.size(), other_path);
+      cursor = start;
+    } else {
+      path.replace(0, cursor, other_path);
+      cursor = 0;
     }
+    return cpp::nullopt;
+  }
+
+private:
+  cpp::string path;
+  size_t cursor = 0;
+};
 
-    view_ = view_.substr(component_end);
-    return path.substr(component_start, component_end - component_start);
+// A buffer for calls to `readlink`.
+class ReadlinkBuffer {
+public:
+  // Calls readlink and returns a view into this buffer.
+  // The view is only valid until the next mutating call to ReadlinkBuffer.
+  ErrorOr<cpp::string_view> readlink(const char *path) {
+    ErrorOr<ssize_t> bytes_written =
+        linux_syscalls::readlink(path, buffer, sizeof(buffer));
+    if (!bytes_written)
+      return Error(bytes_written.error());
+    if (*bytes_written <= 0)
+      return Error(EIO); // Should not be possible, but check to guard underflow
+
+    cpp::string_view target(buffer, static_cast<size_t>(*bytes_written));
+    if (target.size() >= sizeof(buffer))
+      return Error(ENAMETOOLONG);
+
+    return target;
   }
 
 private:
-  cpp::string_view view_;
+  char buffer[PATH_MAX];
 };
 
 ErrorOr<mode_t> read_file_type(const char *path) {
@@ -174,6 +229,8 @@ ErrorOr<mode_t> read_file_type(const char *path) {
 
 cpp::optional<Error> resolve_path(PendingPath &pending_path,
                                   ResolvedPath &resolved_path) {
+  size_t symlinks_followed = 0;
+
   while (!pending_path.empty()) {
     cpp::string_view component = pending_path.advance_component();
     if (component.empty() || component == path::CURRENT_DIR_COMPONENT)
@@ -191,12 +248,30 @@ cpp::optional<Error> resolve_path(PendingPath &pending_path,
     if (!mode)
       return Error(mode.error());
 
-    // TODO: Resolve symbolic links.
-    if (S_ISLNK(*mode))
-      return Error(ENOSYS);
+    if (S_ISLNK(*mode)) {
+      if (symlinks_followed >= MAX_SYMLINK_FOLLOWS)
+        return Error(ELOOP);
+      symlinks_followed += 1;
+
+      ReadlinkBuffer buf;
+      ErrorOr<cpp::string_view> target = buf.readlink(resolved_path.c_str());
+      if (!target)
+        return Error(target.error());
+
+      if (cpp::optional<Error> err = pending_path.prepend(*target); err)
+        return err;
+
+      // Since the last component of resolved_path was a link, remove it.
+      resolved_path.set_to_parent();
+
+      if (path::is_absolute(*target))
+        resolved_path.set_to_root();
+
+      continue;
+    }
 
-    // If the path is not a directory, but there is more to resolve, then error.
-    // For example, realpath("/path/to/file.txt/") should give ENOTDIR.
+    // If the path is not a directory, but there are directory traversals, then
+    // we should error. e.g. realpath("/path/to/file.txt/") should give ENOTDIR.
     if (!S_ISDIR(*mode) && !pending_path.empty())
       return Error(ENOTDIR);
   }
@@ -216,7 +291,9 @@ ErrorOr<char *> realpath_impl(const char *__restrict path_cstr,
   if (path.size() >= PATH_MAX)
     return Error(ENAMETOOLONG);
 
-  PendingPath pending_path(path);
+  PendingPath pending_path;
+  if (cpp::optional<Error> err = pending_path.prepend(path); err)
+    return Error(*err);
 
   ResolvedPath resolved_path;
   if (!path::is_absolute(path)) {
diff --git a/libc/test/src/stdlib/CMakeLists.txt b/libc/test/src/stdlib/CMakeLists.txt
index 4a0d7a6abd910..2e47893f8b540 100644
--- a/libc/test/src/stdlib/CMakeLists.txt
+++ b/libc/test/src/stdlib/CMakeLists.txt
@@ -405,6 +405,7 @@ add_libc_test(
     libc.src.unistd.close
     libc.src.unistd.getcwd
     libc.src.unistd.getpid
+    libc.src.unistd.symlinkat
     libc.src.unistd.unlinkat
     libc.test.UnitTest.ErrnoCheckingTest
     libc.test.UnitTest.ErrnoSetterMatcher
diff --git a/libc/test/src/stdlib/realpath_test.cpp b/libc/test/src/stdlib/realpath_test.cpp
index f7dc440a26278..539aae794b652 100644
--- a/libc/test/src/stdlib/realpath_test.cpp
+++ b/libc/test/src/stdlib/realpath_test.cpp
@@ -32,6 +32,7 @@
 #include "src/unistd/close.h"
 #include "src/unistd/getcwd.h"
 #include "src/unistd/getpid.h"
+#include "src/unistd/symlinkat.h"
 #include "src/unistd/unlinkat.h"
 #include "test/UnitTest/ErrnoCheckingTest.h"
 #include "test/UnitTest/ErrnoSetterMatcher.h"
@@ -160,6 +161,21 @@ class TestDir {
       return -1;
     return LIBC_NAMESPACE::close(newfd);
   }
+
+  // Creates a symlink relative to TestDir. Returns zero on success.
+  [[nodiscard]] int symlink(const char *target_path,
+                            const char *relative_path) {
+    char *path = LIBC_NAMESPACE::strdup(relative_path);
+    if (path == nullptr)
+      return -1;
+
+    if (!files.push_back(path)) {
+      tlog << "Not enough space in TestDir::files_\n";
+      return -1;
+    }
+
+    return LIBC_NAMESPACE::symlinkat(target_path, fd, path);
+  }
 };
 
 cpp::string unique_id() {
@@ -485,3 +501,110 @@ TEST_F(LlvmLibcRealpathTest, RelativeRealpathRejectsPathExceedingMaxSize) {
   ASSERT_EQ(realpath_buffered("."), nullptr);
   ASSERT_ERRNO_EQ(ENAMETOOLONG);
 }
+
+TEST_F(LlvmLibcRealpathTest, AbsoluteSymlinkResolves) {
+  TestDir test_dir;
+  ASSERT_TRUE(create_test_dir("AbsoluteSymlinkResolves", test_dir));
+
+  ASSERT_THAT(test_dir.mkdir("a"), Succeeds());
+  ASSERT_THAT(test_dir.touch("a/file"), Succeeds());
+
+  cpp::string absolute_target = test_dir.absolute_path("a/file");
+  ASSERT_THAT(test_dir.symlink(absolute_target.c_str(), "link"), Succeeds());
+
+  ASSERT_STREQ(realpath_buffered(test_dir.absolute_path("link")),
+               absolute_target.c_str());
+}
+
+TEST_F(LlvmLibcRealpathTest, RelativeSymlinkResolves) {
+  TestDir test_dir;
+  ASSERT_TRUE(create_test_dir("RelativeSymlinkResolves", test_dir));
+
+  ASSERT_THAT(test_dir.mkdir("a"), Succeeds());
+  ASSERT_THAT(test_dir.touch("a/file"), Succeeds());
+
+  ASSERT_THAT(test_dir.symlink("a/file", "link"), Succeeds());
+
+  ASSERT_STREQ(realpath_buffered(test_dir.absolute_path("link")),
+               test_dir.absolute_path("a/file").c_str());
+}
+
+TEST_F(LlvmLibcRealpathTest, SymlinkWithinDirectoryTraversalResolves) {
+  TestDir test_dir;
+  ASSERT_TRUE(
+      create_test_dir("SymlinkWithinDirectoryTraversalResolves", test_dir));
+
+  ASSERT_THAT(test_dir.mkdir("a"), Succeeds());
+  ASSERT_THAT(test_dir.mkdir("a/b"), Succeeds());
+  ASSERT_THAT(test_dir.touch("a/b/c"), Succeeds());
+  ASSERT_THAT(test_dir.symlink("a/b", "link"), Succeeds());
+
+  ASSERT_STREQ(realpath_buffered(test_dir.absolute_path("link/c")),
+               test_dir.absolute_path("a/b/c").c_str());
+}
+
+TEST_F(LlvmLibcRealpathTest, MultipleSymlinkResolutions) {
+  TestDir test_dir;
+  ASSERT_TRUE(create_test_dir("MultipleSymlinkResolutions", test_dir));
+
+  ASSERT_THAT(test_dir.symlink("b", "a"), Succeeds());
+  ASSERT_THAT(test_dir.symlink("c", "b"), Succeeds());
+  ASSERT_THAT(test_dir.symlink("d", "c"), Succeeds());
+  ASSERT_THAT(test_dir.touch("d"), Succeeds());
+
+  ASSERT_STREQ(realpath_buffered(test_dir.absolute_path("a")),
+               test_dir.absolute_path("d").c_str());
+}
+
+TEST_F(LlvmLibcRealpathTest, SymlinkLoop) {
+  TestDir test_dir;
+  ASSERT_TRUE(create_test_dir("SymlinkLoop", test_dir));
+
+  ASSERT_THAT(test_dir.symlink("a", "b"), Succeeds());
+  ASSERT_THAT(test_dir.symlink("b", "a"), Succeeds());
+
+  ASSERT_EQ(realpath_buffered(test_dir.absolute_path("a")), nullptr);
+  ASSERT_ERRNO_EQ(ELOOP);
+}
+
+TEST_F(LlvmLibcRealpathTest, LongSymlinkErrorsWithNameTooLong) {
+  TestDir test_dir;
+  ASSERT_TRUE(create_test_dir("LongSymlinkErrorsWithNameTooLong", test_dir));
+
+  cpp::string target(PATH_MAX - 1, 'a');
+  for (size_t i = 0; i < target.size(); i += NAME_MAX)
+    target[i] = '/';
+
+  ASSERT_THAT(test_dir.symlink(target.c_str(), "link"), Succeeds());
+
+  // The link resolves to a maximum length path,
+  // so adding anything to the end means the intermediary path is too long.
+  ASSERT_EQ(realpath_buffered(test_dir.absolute_path("link/long")), nullptr);
+  ASSERT_ERRNO_EQ(ENAMETOOLONG);
+}
+
+TEST_F(LlvmLibcRealpathTest, ErrorsWithNotDirWhenLinkTargetHasTrailingSep) {
+  TestDir test_dir;
+  ASSERT_TRUE(create_test_dir("ErrorsWithNotDirWhenLinkTargetHasTrailingSep",
+                              test_dir));
+
+  ASSERT_THAT(test_dir.touch("file"), Succeeds());
+  // The trailing slash in the symlink target should be respected.
+  ASSERT_THAT(test_dir.symlink("file/", "link"), Succeeds());
+
+  ASSERT_EQ(realpath_buffered(test_dir.absolute_path("link")), nullptr);
+  ASSERT_ERRNO_EQ(ENOTDIR);
+}
+
+TEST_F(LlvmLibcRealpathTest, ErrorsWithNotDirWhenPathWithLinkHasTrailingSep) {
+  TestDir test_dir;
+  ASSERT_TRUE(create_test_dir("ErrorsWithNotDirWhenPathWithLinkHasTrailingSep",
+                              test_dir));
+
+  ASSERT_THAT(test_dir.touch("file"), Succeeds());
+  ASSERT_THAT(test_dir.symlink("file", "link"), Succeeds());
+
+  // The trailing slash in "link/" should be respected.
+  ASSERT_EQ(realpath_buffered(test_dir.absolute_path("link/")), nullptr);
+  ASSERT_ERRNO_EQ(ENOTDIR);
+}



More information about the libc-commits mailing list