path-util: add flavour of path_startswith() that leaves a leading slash in place
authorLennart Poettering <lennart@poettering.net>
Mon, 19 May 2025 10:58:52 +0000 (12:58 +0200)
committerTobias Deiminger <tobias.deiminger@linutronix.de>
Mon, 27 Apr 2026 19:48:55 +0000 (21:48 +0200)
(cherry picked from commit ee19edbb9f3455db3f750089082f3e5a925e3a0c)

Origin: backport, https://github.com/systemd/systemd/commit/20021e7686426052e3a7505425d7e12085feb2a6

Gbp-Pq: Name CVE-2026-29111-1.patch

src/basic/fs-util.c
src/basic/mkdir.c
src/basic/path-util.c
src/basic/path-util.h
src/test/test-path-util.c

index d71c07c78cb4ee352fcf8079bd2c096f8e7a4dbe..9950ff3e244784735065baa17cc24816dafd1b2f 100644 (file)
@@ -67,7 +67,7 @@ int rmdir_parents(const char *path, const char *stop) {
                 assert(*slash == '/');
                 *slash = '\0';
 
-                if (path_startswith_full(stop, p, /* accept_dot_dot= */ false))
+                if (path_startswith_full(stop, p, /* flags= */ 0))
                         return 0;
 
                 if (rmdir(p) < 0 && errno != ENOENT)
index c8ff342d0f7e45156f796bde8cc8d03ebe301f49..478cb693b259723b55560e136c4fa8b1102500dc 100644 (file)
@@ -99,7 +99,7 @@ int mkdir_parents_internal(const char *prefix, const char *path, mode_t mode, ui
         assert(_mkdirat != mkdirat);
 
         if (prefix) {
-                p = path_startswith_full(path, prefix, /* accept_dot_dot= */ false);
+                p = path_startswith_full(path, prefix, /* flags= */ 0);
                 if (!p)
                         return -ENOTDIR;
         } else
@@ -144,7 +144,7 @@ int mkdir_parents_internal(const char *prefix, const char *path, mode_t mode, ui
 
                 s[n] = '\0';
 
-                if (!prefix || !path_startswith_full(prefix, path, /* accept_dot_dot= */ false)) {
+                if (!prefix || !path_startswith_full(prefix, path, /* flags= */ 0)) {
                         r = mkdir_safe_internal(path, mode, uid, gid, flags | MKDIR_IGNORE_EXISTING, _mkdirat);
                         if (r < 0 && r != -EEXIST)
                                 return r;
index 72c0d6bad80e376bfc43f3ef7545098f6f2dff99..0c2d08c69af2b53f8ab155fd3394fa7b4aac349f 100644 (file)
@@ -399,8 +399,8 @@ char* path_simplify_full(char *path, PathSimplifyFlags flags) {
         return path;
 }
 
-char* path_startswith_full(const char *path, const char *prefix, bool accept_dot_dot) {
-        assert(path);
+char* path_startswith_full(const char *original_path, const char *prefix, PathStartWithFlags flags) {
+        assert(original_path);
         assert(prefix);
 
         /* Returns a pointer to the start of the first component after the parts matched by
@@ -413,28 +413,45 @@ char* path_startswith_full(const char *path, const char *prefix, bool accept_dot
          * Returns NULL otherwise.
          */
 
+        const char *path = original_path;
+
         if ((path[0] == '/') != (prefix[0] == '/'))
                 return NULL;
 
         for (;;) {
                 const char *p, *q;
-                int r, k;
+                int m, n;
 
-                r = path_find_first_component(&path, accept_dot_dot, &p);
-                if (r < 0)
+                m = path_find_first_component(&path, FLAGS_SET(flags, PATH_STARTSWITH_ACCEPT_DOT_DOT), &p);
+                if (m < 0)
                         return NULL;
 
-                k = path_find_first_component(&prefix, accept_dot_dot, &q);
-                if (k < 0)
+                n = path_find_first_component(&prefix, FLAGS_SET(flags, PATH_STARTSWITH_ACCEPT_DOT_DOT), &q);
+                if (n < 0)
                         return NULL;
 
-                if (k == 0)
-                        return (char*) (p ?: path);
+                if (n == 0) {
+                        if (!p)
+                                p = path;
+
+                        if (FLAGS_SET(flags, PATH_STARTSWITH_RETURN_LEADING_SLASH)) {
+
+                                if (p <= original_path)
+                                        return NULL;
+
+                                p--;
+
+                                if (*p != '/')
+                                        return NULL;
+                        }
+
+                        return (char*) p;
+                }
 
-                if (r != k)
+                if (m != n)
                         return NULL;
 
-                if (!strneq(p, q, r))
+                if (!strneq(p, q, m))
                         return NULL;
         }
 }
index 09604ba4b069f39ef1a0d3db01ebdbb956928312..e083a91aee800b272527abad23a821a8181a4876 100644 (file)
@@ -62,9 +62,15 @@ int safe_getcwd(char **ret);
 int path_make_absolute_cwd(const char *p, char **ret);
 int path_make_relative(const char *from, const char *to, char **ret);
 int path_make_relative_parent(const char *from_child, const char *to, char **ret);
-char* path_startswith_full(const char *path, const char *prefix, bool accept_dot_dot) _pure_;
+
+typedef enum PathStartWithFlags {
+        PATH_STARTSWITH_ACCEPT_DOT_DOT       = 1U << 0,
+        PATH_STARTSWITH_RETURN_LEADING_SLASH = 1U << 1,
+} PathStartWithFlags;
+
+char* path_startswith_full(const char *path, const char *prefix, PathStartWithFlags flags) _pure_;
 static inline char* path_startswith(const char *path, const char *prefix) {
-        return path_startswith_full(path, prefix, true);
+        return path_startswith_full(path, prefix, PATH_STARTSWITH_ACCEPT_DOT_DOT);
 }
 int path_compare(const char *a, const char *b) _pure_;
 
index 7ab9f20b317b57e48c632cfe5ab0078b464816c7..6e495e6b084110ef018400af603619d25caedcb1 100644 (file)
@@ -671,6 +671,22 @@ TEST(path_startswith) {
         test_path_startswith_one("/foo/bar/barfoo/", "/fo", NULL, NULL);
 }
 
+static void test_path_startswith_return_leading_slash_one(const char *path, const char *prefix, const char *expected) {
+        const char *p;
+
+        log_debug("/* %s(%s, %s) */", __func__, path, prefix);
+
+        p = path_startswith_full(path, prefix, PATH_STARTSWITH_RETURN_LEADING_SLASH);
+        assert_se(streq_ptr(p, expected));
+}
+
+TEST(path_startswith_return_leading_slash) {
+        test_path_startswith_return_leading_slash_one("/foo/bar", "/", "/foo/bar");
+        test_path_startswith_return_leading_slash_one("/foo/bar", "/foo", "/bar");
+        test_path_startswith_return_leading_slash_one("/foo/bar", "/foo/bar", NULL);
+        test_path_startswith_return_leading_slash_one("/foo/bar/", "/foo/bar", "/");
+}
+
 static void test_prefix_root_one(const char *r, const char *p, const char *expected) {
         _cleanup_free_ char *s = NULL;
         const char *t;