[libcxx-commits] [libcxx] [libc++][filesystem] Report an error for canonical("") (PR #215031)

Yordan Vásquez via libcxx-commits libcxx-commits at lists.llvm.org
Sat Aug 8 16:05:38 PDT 2026


https://github.com/vyordan updated https://github.com/llvm/llvm-project/pull/215031

>From b598d1453e4cfe92d2e7ca0bc5498f9daacffb9c Mon Sep 17 00:00:00 2001
From: vyordan <vyordangiovani at gmail.com>
Date: Sat, 8 Aug 2026 16:50:22 -0600
Subject: [PATCH] [libc++][filesystem] Report an error for canonical("")

canonical(p) requires !exists(p) to be an error per [fs.op.canonical].
An empty path never refers to an existing file (exists(path()) is
always false, matching how a POSIX stat("") call fails with ENOENT),
so canonical("") must report an error instead of silently returning
the current directory.

The current implementation absolutizes p before checking whether it
resolves to something real: __do_absolute(p) computes cwd / p, and
appending an empty path via operator/= yields cwd itself (with a
trailing separator), which does exist. This hides the required error,
since no information survives about whether the original argument was
empty. This is a corner case unique to the empty path: no other
relative path can "disappear" when composed with the cwd.

Fix this at the source by checking orig_p.empty() in __canonical
before absolutizing, using the original, unmodified argument.

absolute() is unaffected by design: [fs.op.absolute] explicitly
discourages treating !exists(p) as an error, since it is a purely
lexical composition (current_path() / p). This matches the existing
absolute.pass.cpp expectation that absolute("") == current_path() / "".

weakly_canonical() is also unaffected in its observable behavior:
[fs.op.weakly_canonical] never requires existence either. It used
__canonical("", ec) internally as a shortcut relying on the old
(buggy) behavior to resolve to the current directory; both internal
call sites are updated to call __current_path() directly instead,
producing the exact same result without depending on the
now-corrected canonical(""). Since relative() and proximate() are
built solely on top of weakly_canonical(), they require no changes
either.

Fixes #77170
---
 libcxx/docs/ReleaseNotes/22.rst               |  7 +++++
 libcxx/src/filesystem/operations.cpp          | 22 ++++++++++++--
 .../fs.op.canonical/canonical.pass.cpp        | 30 +++++++++++++++++++
 3 files changed, 57 insertions(+), 2 deletions(-)

diff --git a/libcxx/docs/ReleaseNotes/22.rst b/libcxx/docs/ReleaseNotes/22.rst
index 9f9927095a61f..97852fed4b4fe 100644
--- a/libcxx/docs/ReleaseNotes/22.rst
+++ b/libcxx/docs/ReleaseNotes/22.rst
@@ -128,6 +128,13 @@ Potentially breaking changes
   ``_LIBCPP_ABI_NO_ITERATOR_BASES``. If you are using this flag and care about ABI stability, you should set
   ``_LIBCPP_ABI_NO_REVERSE_ITERATOR_SECOND_MEMBER`` as well.
 
+- ``std::filesystem::canonical`` now reports an error (throwing ``filesystem_error``, or setting the
+  ``error_code`` for the overload that takes one) when called with an empty path, as required by
+  ``[fs.op.canonical]`` ("``!exists(p)`` is an error"), instead of silently returning the current
+  directory. ``std::filesystem::absolute``, ``std::filesystem::weakly_canonical``,
+  ``std::filesystem::relative`` and ``std::filesystem::proximate`` are unaffected and keep resolving an
+  empty path to the current directory, as required by the Standard. (`Github <https://llvm.org/PR77170>`__)
+
 Announcements About Future Releases
 -----------------------------------
 
diff --git a/libcxx/src/filesystem/operations.cpp b/libcxx/src/filesystem/operations.cpp
index 3ff62c102d0ad..0cee6a1f4a038 100644
--- a/libcxx/src/filesystem/operations.cpp
+++ b/libcxx/src/filesystem/operations.cpp
@@ -99,6 +99,15 @@ path __canonical(path const& orig_p, error_code* ec) {
   path cwd;
   ErrorHandler<path> err("canonical", ec, &orig_p, &cwd);
 
+  // canonical() requires the path to exist ([fs.op.canonical]: "!exists(p) is
+  // an error"). An empty path never refers to an existing file -- exists("")
+  // is always false, since status("") would fail the same way a POSIX
+  // stat("") call fails with ENOENT. Absolutizing an empty path below would
+  // otherwise silently produce the (existing) current directory and hide
+  // this required error.
+  if (orig_p.empty())
+    return err.report(errc::no_such_file_or_directory);
+
   path p = __do_absolute(orig_p, &cwd, ec);
 #if (defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112) || defined(_WIN32)
   std::unique_ptr<path::value_type, decltype(&::free)> hold(detail::realpath(p.c_str(), nullptr), &::free);
@@ -1042,8 +1051,12 @@ path __temp_directory_path(error_code* ec) {
 path __weakly_canonical(const path& p, error_code* ec) {
   ErrorHandler<path> err("weakly_canonical", ec, &p);
 
+  // weakly_canonical() never requires its argument to exist ([fs.op.weakly.canonical]),
+  // so an empty path -- which has no leading elements at all -- resolves to
+  // the current directory directly. This must not go through __canonical(""),
+  // which is now required to report an error for an empty path.
   if (p.empty())
-    return __canonical("", ec);
+    return __current_path(ec);
 
   path result;
   path tmp;
@@ -1069,7 +1082,12 @@ path __weakly_canonical(const path& p, error_code* ec) {
     --PP;
   }
   if (PP.State_ == PathParser::PS_BeforeBegin) {
-    result = __canonical("", &m_ec);
+    // None of the elements of p exist (or p was relative with no root and
+    // none of its ancestors exist either); the "leading elements of p that
+    // exist" are therefore empty, and canonical() of that degenerate,
+    // zero-element path is the current directory. As above, this must not
+    // be routed through __canonical(""), which now reports an error.
+    result = __current_path(&m_ec);
     if (m_ec) {
       return err.report(m_ec);
     }
diff --git a/libcxx/test/std/input.output/filesystems/fs.op.funcs/fs.op.canonical/canonical.pass.cpp b/libcxx/test/std/input.output/filesystems/fs.op.funcs/fs.op.canonical/canonical.pass.cpp
index 5323f3bc8337f..081aabb24ea93 100644
--- a/libcxx/test/std/input.output/filesystems/fs.op.funcs/fs.op.canonical/canonical.pass.cpp
+++ b/libcxx/test/std/input.output/filesystems/fs.op.funcs/fs.op.canonical/canonical.pass.cpp
@@ -92,6 +92,35 @@ static void test_dne_path()
     }
 }
 
+// https://llvm.org/PR77170
+// Per [fs.op.canonical], "!exists(p) is an error", and an empty path never
+// refers to an existing file (exists(path()) is always false). canonical("")
+// must therefore report an error instead of silently returning the current
+// directory.
+static void test_empty_path() {
+  std::error_code ec = GetTestEC();
+  {
+    const path ret = canonical(path{}, ec);
+    assert(ec != GetTestEC());
+    assert(ec);
+    assert(ret == path{});
+  }
+  {
+    TEST_THROWS_TYPE(filesystem_error, canonical(path{}));
+  }
+
+  ec = GetTestEC();
+  {
+    const path ret = canonical("", ec);
+    assert(ec != GetTestEC());
+    assert(ec);
+    assert(ret == path{});
+  }
+  {
+    TEST_THROWS_TYPE(filesystem_error, canonical(""));
+  }
+}
+
 static void test_exception_contains_paths()
 {
 #ifndef TEST_HAS_NO_EXCEPTIONS
@@ -121,6 +150,7 @@ int main(int, char**) {
     signature_test();
     test_canonical();
     test_dne_path();
+    test_empty_path();
     test_exception_contains_paths();
 
     return 0;



More information about the libcxx-commits mailing list