[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