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

Yordan Vásquez via libcxx-commits libcxx-commits at lists.llvm.org
Sun Aug 9 13:14:35 PDT 2026


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

>From ea8882e9991d9f1037f9aa58114a4d20a9d67501 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/24.rst               |  3 ++
 libcxx/src/filesystem/operations.cpp          | 12 ++++++--
 .../fs.op.canonical/canonical.pass.cpp        | 30 +++++++++++++++++++
 3 files changed, 43 insertions(+), 2 deletions(-)

diff --git a/libcxx/docs/ReleaseNotes/24.rst b/libcxx/docs/ReleaseNotes/24.rst
index e9a36ae94392f..5204861f374e0 100644
--- a/libcxx/docs/ReleaseNotes/24.rst
+++ b/libcxx/docs/ReleaseNotes/24.rst
@@ -67,6 +67,9 @@ Potentially breaking changes
 - Unused objects of containers are now diagnosed. To ease the transition ``_LIBCPP_DISABLE_UNUSED_STRUCT_WARNINGS`` can
   be defined as an escape hatch. This escape hatch will be removed in LLVM 24.
 
+- ``std::filesystem::canonical`` now reports an error when called with an empty path, per ``[fs.op.canonical]``.
+  (`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..367b6c0ace12b 100644
--- a/libcxx/src/filesystem/operations.cpp
+++ b/libcxx/src/filesystem/operations.cpp
@@ -99,6 +99,12 @@ path __canonical(path const& orig_p, error_code* ec) {
   path cwd;
   ErrorHandler<path> err("canonical", ec, &orig_p, &cwd);
 
+  // canonical() requires !exists(p) to be an error ([fs.op.canonical]). An empty
+  // path never exists, but __do_absolute("") below would resolve to the (existing)
+  // current directory and hide that error, so it must be checked here first.
+  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 +1048,9 @@ 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]).
   if (p.empty())
-    return __canonical("", ec);
+    return __current_path(ec);
 
   path result;
   path tmp;
@@ -1069,7 +1076,8 @@ path __weakly_canonical(const path& p, error_code* ec) {
     --PP;
   }
   if (PP.State_ == PathParser::PS_BeforeBegin) {
-    result = __canonical("", &m_ec);
+    // No element of p exists; canonical() of that degenerate case is the current directory.
+    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