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

This PR contains an incremental implementation of `realpath` that just
simplifies path traversals (e.g. `"/a/./../b"` -> `"/b"`). The complete
implementation would perform `getcwd`/`statx`/`readlinkat` to actually
resolve symlinks, validate paths, etc. This PR aims to add as much
functionality/error handling as possible without making any syscalls.

This implementation lives in `libc/src/stdlib/linux/`, since follow-on
PRs will rely on linux-specific syscalls. For testing purposes, I've
added the entrypoint for x86-64 linux when
`LLVM_LIBC_ENABLE_EXPERIMENTAL_ENTRYPOINTS` is enabled.

Follow-on PRs to add: path validation (e.g.
paths must exist), support for cwd-relative paths, and symlink
resolution.
This commit is contained in:
Jackson Stogel authored and GitHub committed 2026-07-01 15:58:08 -07:00
1 parent 4ce874e05a
commit 88a3488c8a
17 files changed
+561 -24

No files matched your search

+4
View File
@@ -1526,6 +1526,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
+7
View File
@@ -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
+1
View File
@@ -90,6 +90,7 @@ add_header_library(
libc.hdr.func.realloc
libc.src.__support.common
libc.src.__support.integer_to_string
libc.src.__support.macros.null_check
libc.src.string.memory_utils.inline_memcpy
libc.src.string.memory_utils.inline_memset
libc.src.string.string_utils
+37 -9
View File
@@ -15,6 +15,7 @@
#include "src/__support/CPP/string_view.h"
#include "src/__support/integer_to_string.h" // IntegerToString
#include "src/__support/macros/config.h"
#include "src/__support/macros/null_check.h"
#include "src/string/memory_utils/inline_memcpy.h"
#include "src/string/memory_utils/inline_memset.h"
#include "src/string/string_utils.h" // string_length
@@ -23,6 +24,16 @@
namespace LIBC_NAMESPACE_DECL {
namespace cpp {
namespace {
char *realloc_or_die(char *ptr, size_t size) {
void *new_ptr = ::realloc(ptr, size);
// Out of memory: this is not handled in current implementation, so crash.
LIBC_CRASH_ON_NULLPTR(new_ptr);
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
@@ -76,6 +87,11 @@ public:
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_;
@@ -133,15 +149,9 @@ public:
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 +163,24 @@ public:
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);
+19 -10
View File
@@ -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()
@@ -44,6 +44,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
+45
View File
@@ -0,0 +1,45 @@
//===----------------------------------------------------------------------===//
//
// 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 = '/';
// Component pointing to the current directory.
constexpr cpp::string_view CURRENT_DIR_COMPONENT = ".";
// Component pointing to the parent directory.
constexpr cpp::string_view PARENT_DIR_COMPONENT = "..";
// 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
+23
View File
@@ -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.h unavailable on this platfrom
#endif
#endif // LLVM_LIBC_SRC___SUPPORT_OSUTIL_PATH_H
+11 -4
View File
@@ -596,6 +596,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()
@@ -677,10 +688,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
+21
View File
@@ -9,6 +9,27 @@ 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.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
libc.src.__support.libc_errno
libc.src.__support.macros.config
libc.src.__support.OSUtil.path
libc.src.string.memory_utils.inline_memcpy
)
add_entrypoint_object(
setenv
SRCS
+201
View File
@@ -0,0 +1,201 @@
//===----------------------------------------------------------------------===//
//
// 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 "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"
#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 {
// 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; }
cpp::optional<Error> 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::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);
}
// 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 (cpp::optional<Error> err = push_raw(path::SEPARATOR); err)
return err;
}
return push_raw(component);
}
// 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:
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 cpp::nullopt;
}
cpp::optional<Error> push_raw(char c) {
return push_raw(cpp::string_view(&c, 1));
}
cpp::string path_;
};
// 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::SEPARATOR);
if (component_start == cpp::string_view::npos) {
view_ = "";
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);
}
view_ = view_.substr(component_end);
return path.substr(component_start, component_end - component_start);
}
private:
cpp::string_view view_;
};
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 == path::CURRENT_DIR_COMPONENT)
continue;
if (component == path::PARENT_DIR_COMPONENT) {
resolved_path.set_to_parent();
continue;
}
if (cpp::optional<Error> err = resolved_path.push_component(component); err)
return err;
}
return cpp::nullopt;
}
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 (!path::is_absolute(path)) {
if (cpp::optional<Error> err = resolved_path.set_to_cwd(); err)
return *err;
}
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);
return resolved_path_buf;
}
return resolved_path.release();
}
} // 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
+25
View File
@@ -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
@@ -169,6 +169,7 @@ add_libc_test(
SRCS
string_test.cpp
DEPENDS
libc.hdr.func.free
libc.src.__support.CPP.string
libc.src.__support.CPP.string_view
)
+28 -1
View File
@@ -6,6 +6,7 @@
//
//===----------------------------------------------------------------------===//
#include "hdr/func/free.h"
#include "src/__support/CPP/string.h"
#include "test/UnitTest/Test.h"
@@ -77,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());
}
@@ -135,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);
@@ -186,6 +194,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.
+17
View File
@@ -377,6 +377,23 @@ 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.hdr.types.size_t
libc.src.__support.CPP.string
libc.src.__support.macros.config
libc.src.__support.OSUtil.path
libc.src.stdlib.realpath
libc.test.UnitTest.ErrnoCheckingTest
)
add_libc_test(
memalignment_test
SUITE
+111
View File
@@ -0,0 +1,111 @@
//===----------------------------------------------------------------------===//
//
// 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 "hdr/types/size_t.h"
#include "src/__support/CPP/string.h"
#include "src/__support/OSUtil/path.h"
#include "src/__support/macros/config.h"
#include "src/stdlib/realpath.h"
#include "test/UnitTest/ErrnoCheckingTest.h"
namespace cpp = LIBC_NAMESPACE::cpp;
namespace path = LIBC_NAMESPACE::path;
using LIBC_NAMESPACE::testing::ErrnoCheckingTest;
// This test assumes the following values, so fail early if they mismatch.
static_assert(path::SEPARATOR == '/');
static_assert(path::CURRENT_DIR_COMPONENT == ".");
static_assert(path::PARENT_DIR_COMPONENT == "..");
class LlvmLibcRealpathTest : public LIBC_NAMESPACE::testing::ErrnoCheckingTest {
public:
char *realpath_buffered(const char *path) {
return LIBC_NAMESPACE::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, OkIfPathArgIsExactlyMaxSize) {
// PATH_MAX counts null terminator, so construct a path of size PATH_MAX-1.
cpp::string s(PATH_MAX - 1, '/');
for (size_t i = 1; i < s.size(); i += 2)
s[i] = '.';
ASSERT_STREQ(realpath_buffered(s), "/");
}
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_STREQ(realpath_buffered(s), s.c_str());
}
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);
}
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);
}
@@ -1129,6 +1129,7 @@ libc_support_library(
":__support_cpp_string_view",
":__support_integer_to_string",
":__support_macros_config",
":__support_macros_null_check",
":func_free",
":func_malloc",
":func_realloc",