From 1633ceaff25535de9419d992dd4753c6cc221796 Mon Sep 17 00:00:00 2001 From: Sergei Zimmerman Date: Tue, 14 Oct 2025 02:33:38 +0300 Subject: [PATCH] libutil: Ensure that CanonPath does not contain NUL bytes This, alongside the other invariants of the CanonPath is important to uphold. std::filesystem happily crashes on NUL bytes in the constructor, as we've seen with `path:%00` prior to c436b7a32afaf01d62f828697ddf5c49d4f8678c. Best to stay clear of NUL bytes when we're talking about syscalls, especially on Unix where strings are null terminated. Very nice to have if we decide to switch over to pascal-style strings. --- src/libutil-tests/canon-path.cc | 9 +++++++++ src/libutil/canon-path.cc | 19 +++++++++++++++++++ src/libutil/include/nix/util/canon-path.hh | 10 ++++++---- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/libutil-tests/canon-path.cc b/src/libutil-tests/canon-path.cc index 971a9cc96..aae9285c4 100644 --- a/src/libutil-tests/canon-path.cc +++ b/src/libutil-tests/canon-path.cc @@ -42,6 +42,15 @@ TEST(CanonPath, basic) } } +TEST(CanonPath, nullBytes) +{ + std::string s = "/hello/world"; + s[8] = '\0'; + ASSERT_THROW(CanonPath("/").push(std::string(1, '\0')), BadCanonPath); + ASSERT_THROW(CanonPath(std::string_view(s)), BadCanonPath); + ASSERT_THROW(CanonPath(s, CanonPath::root), BadCanonPath); +} + TEST(CanonPath, from_existing) { CanonPath p0("foo//bar/"); diff --git a/src/libutil/canon-path.cc b/src/libutil/canon-path.cc index 3b4777ef7..22ca3e066 100644 --- a/src/libutil/canon-path.cc +++ b/src/libutil/canon-path.cc @@ -3,6 +3,8 @@ #include "nix/util/file-path-impl.hh" #include "nix/util/strings-inline.hh" +#include + namespace nix { const CanonPath CanonPath::root = CanonPath("/"); @@ -12,14 +14,30 @@ static std::string absPathPure(std::string_view path) return canonPathInner(path, [](auto &, auto &) {}); } +static void ensureNoNullBytes(std::string_view s) +{ + if (std::memchr(s.data(), '\0', s.size())) [[unlikely]] { + using namespace std::string_view_literals; + auto str = replaceStrings(std::string(s), "\0"sv, "␀"sv); + throw BadCanonPath("path segment '%s' must not contain null (\\0) bytes", str); + } +} + CanonPath::CanonPath(std::string_view raw) : path(absPathPure(concatStrings("/", raw))) +{ + ensureNoNullBytes(raw); +} + +CanonPath::CanonPath(const char * raw) + : path(absPathPure(concatStrings("/", raw))) { } CanonPath::CanonPath(std::string_view raw, const CanonPath & root) : path(absPathPure(raw.size() > 0 && raw[0] == '/' ? raw : concatStrings(root.abs(), "/", raw))) { + ensureNoNullBytes(raw); } CanonPath::CanonPath(const std::vector & elems) @@ -80,6 +98,7 @@ void CanonPath::push(std::string_view c) { assert(c.find('/') == c.npos); assert(c != "." && c != ".."); + ensureNoNullBytes(c); if (!isRoot()) path += '/'; path += c; diff --git a/src/libutil/include/nix/util/canon-path.hh b/src/libutil/include/nix/util/canon-path.hh index a9c173d71..b9b2fff25 100644 --- a/src/libutil/include/nix/util/canon-path.hh +++ b/src/libutil/include/nix/util/canon-path.hh @@ -1,6 +1,7 @@ #pragma once ///@file +#include "nix/util/error.hh" #include #include #include @@ -12,6 +13,8 @@ namespace nix { +MakeError(BadCanonPath, Error); + /** * A canonical representation of a path. It ensures the following: * @@ -23,6 +26,8 @@ namespace nix { * * - There are no components equal to '.' or '..'. * + * - It does not contain NUL bytes. + * * `CanonPath` are "virtual" Nix paths for abstract file system objects; * they are always Unix-style paths, regardless of what OS Nix is * running on. The `/` root doesn't denote the ambient host file system @@ -51,10 +56,7 @@ public: */ CanonPath(std::string_view raw); - explicit CanonPath(const char * raw) - : CanonPath(std::string_view(raw)) - { - } + explicit CanonPath(const char * raw); struct unchecked_t {};