[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