[libc-commits] [libc] [libc][realpath] Validate path components (PR #209370)
Jackson Stogel via libc-commits
libc-commits at lists.llvm.org
Fri Jul 17 15:50:15 PDT 2026
https://github.com/jtstogel updated https://github.com/llvm/llvm-project/pull/209370
>From 850d1fde46d1363e1fa0e16665239edfe2d1b9e3 Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Fri, 10 Jul 2026 16:26:05 -0700
Subject: [PATCH] [libc][realpath] Validate path components.
This PR updates `realpath` to validate paths and return `ENOTDIR` or `ENOENT` according to https://pubs.opengroup.org/onlinepubs/9699919799/functions/realpath.html.
This PR also bumps the memory limit in `HermeticTestUtils.cpp`. The realpath unit tests allocate quite a few strings. Since memory in hermetic tests is never free'd, the unit test quickly reaches the limit.
---
libc/src/stdlib/linux/CMakeLists.txt | 5 +++
libc/src/stdlib/linux/realpath.cpp | 34 ++++++++++++++++++
libc/test/src/stdlib/realpath_test.cpp | 49 ++++++++++++++++++++++++++
3 files changed, 88 insertions(+)
diff --git a/libc/src/stdlib/linux/CMakeLists.txt b/libc/src/stdlib/linux/CMakeLists.txt
index 5c91de4135c84..e71e3bf3e4ddf 100644
--- a/libc/src/stdlib/linux/CMakeLists.txt
+++ b/libc/src/stdlib/linux/CMakeLists.txt
@@ -17,7 +17,10 @@ add_entrypoint_object(
../realpath.h
DEPENDS
libc.hdr.errno_macros
+ libc.hdr.fcntl_macros
libc.hdr.limits_macros
+ libc.hdr.sys_stat_macros
+ libc.hdr.types.mode_t
libc.hdr.types.size_t
libc.src.__support.common
libc.src.__support.CPP.optional
@@ -26,7 +29,9 @@ add_entrypoint_object(
libc.src.__support.error_or
libc.src.__support.libc_errno
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.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 b91033f9b9f60..4c6a1e14e8e47 100644
--- a/libc/src/stdlib/linux/realpath.cpp
+++ b/libc/src/stdlib/linux/realpath.cpp
@@ -13,12 +13,17 @@
#include "src/stdlib/realpath.h"
#include "hdr/errno_macros.h"
+#include "hdr/fcntl_macros.h"
#include "hdr/limits_macros.h"
+#include "hdr/sys_stat_macros.h"
+#include "hdr/types/mode_t.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/linux/stat/kernel_statx_types.h"
#include "src/__support/OSUtil/linux/syscall_wrappers/getcwd.h"
+#include "src/__support/OSUtil/linux/syscall_wrappers/statx.h"
#include "src/__support/OSUtil/path.h"
#include "src/__support/common.h"
#include "src/__support/error_or.h"
@@ -82,6 +87,8 @@ class ResolvedPath {
// Must be free'd by the caller.
char *release() { return path_.release_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);
@@ -151,6 +158,20 @@ class PendingPath {
cpp::string_view view_;
};
+ErrorOr<mode_t> read_file_type(const char *path) {
+ internal::kernel_statx_buf buf;
+ ErrorOr<int> ret =
+ linux_syscalls::statx(AT_FDCWD, path, AT_SYMLINK_NOFOLLOW,
+ internal::KERNEL_STATX_TYPE_MASK, &buf);
+ if (!ret)
+ return Error(ret.error());
+
+ if (!(buf.stx_mask & internal::KERNEL_STATX_TYPE_MASK))
+ return Error(EIO);
+
+ return static_cast<mode_t>(buf.stx_mode);
+}
+
cpp::optional<Error> resolve_path(PendingPath &pending_path,
ResolvedPath &resolved_path) {
while (!pending_path.empty()) {
@@ -165,6 +186,19 @@ cpp::optional<Error> resolve_path(PendingPath &pending_path,
if (cpp::optional<Error> err = resolved_path.push_component(component); err)
return err;
+
+ ErrorOr<mode_t> mode = read_file_type(resolved_path.c_str());
+ if (!mode)
+ return Error(mode.error());
+
+ // TODO: Resolve symbolic links.
+ if (S_ISLNK(*mode))
+ return Error(ENOSYS);
+
+ // If the path is not a directory, but there is more to resolve, then error.
+ // For example, realpath("/path/to/file.txt/") give ENOTDIR.
+ if (!S_ISDIR(*mode) && !pending_path.empty())
+ return Error(ENOTDIR);
}
return cpp::nullopt;
diff --git a/libc/test/src/stdlib/realpath_test.cpp b/libc/test/src/stdlib/realpath_test.cpp
index 5e4a8931acc04..760dc5da05593 100644
--- a/libc/test/src/stdlib/realpath_test.cpp
+++ b/libc/test/src/stdlib/realpath_test.cpp
@@ -373,6 +373,55 @@ TEST_F(LlvmLibcRealpathTest, AllocatesResultWhenBufferIsNull) {
::free(result);
}
+TEST_F(LlvmLibcRealpathTest, ErrorsWithNotDirWhenFileIsInPath) {
+ TestDir test_dir;
+ ASSERT_TRUE(create_test_dir("ErrorsWithNotDirWhenFileIsInPath", test_dir));
+
+ ASSERT_THAT(test_dir.touch("file"), Succeeds());
+
+ ASSERT_EQ(realpath_buffered(test_dir.absolute_path("file/.")), nullptr);
+ ASSERT_ERRNO_EQ(ENOTDIR);
+
+ ASSERT_EQ(realpath_buffered(test_dir.absolute_path("file/")), nullptr);
+ ASSERT_ERRNO_EQ(ENOTDIR);
+}
+
+TEST_F(LlvmLibcRealpathTest, FileAtEndOfPathIsOk) {
+ TestDir test_dir;
+ ASSERT_TRUE(create_test_dir("FileAtEndOfPathIsOk", test_dir));
+
+ ASSERT_THAT(test_dir.mkdir("a"), Succeeds());
+ ASSERT_THAT(test_dir.touch("a/file"), Succeeds());
+
+ ASSERT_STREQ(realpath_buffered(test_dir.absolute_path("a/file")),
+ test_dir.absolute_path("a/file").c_str());
+}
+
+TEST_F(LlvmLibcRealpathTest, ErrorsWithNoEntWhenComponentDoesNotExist) {
+ TestDir test_dir;
+ ASSERT_TRUE(
+ create_test_dir("ErrorsWithNoEntWhenComponentDoesNotExist", test_dir));
+
+ // A missing directory should give ENOENT.
+ ASSERT_STREQ(realpath_buffered(test_dir.absolute_path("a/b")), nullptr);
+ ASSERT_ERRNO_EQ(ENOENT);
+
+ // Should fail if the final compnent doesn't exist.
+ ASSERT_STREQ(realpath_buffered(test_dir.absolute_path("a")), nullptr);
+ ASSERT_ERRNO_EQ(ENOENT);
+}
+
+TEST_F(LlvmLibcRealpathTest, ErrorsWithNoAccesWhenDirectoryNotSearchable) {
+ TestDir test_dir;
+ ASSERT_TRUE(
+ create_test_dir("ErrorsWithNoAccesWhenDirectoryNotSearchable", test_dir));
+
+ ASSERT_THAT(test_dir.mkdir("a", /* mode= */ 0644), Succeeds());
+
+ ASSERT_STREQ(realpath_buffered(test_dir.absolute_path("a/b")), nullptr);
+ ASSERT_ERRNO_EQ(EACCES);
+}
+
TEST_F(LlvmLibcRealpathTest, RelativePathResolvesToCurrentWorkingDir) {
TestDir test_dir;
ASSERT_TRUE(
More information about the libc-commits
mailing list