From d7ca77f10800afc2485fe162869cf23b4d0c90f7 Mon Sep 17 00:00:00 2001
From: agnostic-apollo <agnosticapollo@gmail.com>
Date: Thu, 28 Mar 2024 18:46:02 +0100
Subject: [PATCH] Fix bugs in File.getCanonicalPath()

1. If path length is `>= PATH_MAX` (4096) characters, return error.
2. If any path component length is `> NAME_MAX` (255) characters, return
   error.
3. If path has consecutive path separators e.g. `//`, remove them before
   processing and returning.
4. Ensure leading double dot `..` path components are removed since a
   canonical path returned by canonicalize() must not contain them.
---
 .../unix/native/libjava/canonicalize_md.c     | 102 +++++++++++++++---
 1 file changed, 87 insertions(+), 15 deletions(-)

diff --git a/src/java.base/unix/native/libjava/canonicalize_md.c b/src/java.base/unix/native/libjava/canonicalize_md.c
index 2bb896bf32d7..7210f305282f 100644
--- a/src/java.base/unix/native/libjava/canonicalize_md.c
+++ b/src/java.base/unix/native/libjava/canonicalize_md.c
@@ -33,6 +33,7 @@
 #include <sys/stat.h>
 #include <errno.h>
 #include <limits.h>
+#include <unistd.h>
 #if !defined(_ALLBSD_SOURCE)
 #include <alloca.h>
 #endif
@@ -43,6 +44,38 @@
          defined in the java.io.File class */
 
 
+/* Remove consecutive duplicate path separators `//` and the trailing
+  path separator if not rootfs `/`. */
+
+char* removeDupSeparator(char *path)
+{
+    if (path == NULL || *path == '\0') {
+        return NULL;
+    }
+
+    char *in = path;
+    char *out = path;
+    char prevChar = 0;
+    int n = 0;
+    for (; *in != '\0'; in++) {
+        // Remove duplicate path separators
+        if (!(*in == '/' && prevChar == '/')) {
+            *(out++) = *in;
+            n++;
+        }
+        prevChar = *in;
+    }
+    *out = '\0';
+
+    // Remove the trailing path separator, except when path equals `/`
+    if (prevChar == '/' && n > 1) {
+        *(--out) = '\0';
+    }
+
+    return path;
+}
+
+
 /* Check the given name sequence to see if it can be further collapsed.
    Return zero if not, otherwise return the number of names in the sequence. */
 
@@ -62,7 +95,9 @@ collapsible(char *names)
         n++;
         while (*p) {
             if (*p == '/') {
-                p++;
+                while (*p && *p == '/') {
+                    p++;
+                }
                 break;
             }
             p++;
@@ -85,7 +120,9 @@ splitNames(char *names, char **ix)
         ix[i++] = p++;
         while (*p) {
             if (*p == '/') {
-                *p++ = '\0';
+                while (*p && *p == '/') {
+                    *p++ = '\0';
+                }
                 break;
             }
             p++;
@@ -128,11 +165,13 @@ joinNames(char *names, int nc, char **ix)
 static void
 collapse(char *path)
 {
+    // Remove consecutive duplicate path separators '/' regardless of if single or double dot components exist
+    removeDupSeparator(path);
+
     char *names = (path[0] == '/') ? path + 1 : path; /* Preserve first '/' */
     int nc;
     char **ix;
     int i, j;
-    char *p, *q;
 
     nc = collapsible(names);
     if (nc < 2) return;         /* Nothing to do */
@@ -145,7 +184,7 @@ collapse(char *path)
         /* Find next occurrence of "." or ".." */
         do {
             char *p = ix[i];
-            if (p[0] == '.') {
+            if (p != NULL && p[0] == '.') {
                 if (p[1] == '\0') {
                     dots = 1;
                     break;
@@ -166,14 +205,14 @@ collapse(char *path)
             ix[i] = 0;
         }
         else {
-            /* If there is a preceding name, remove both that name and this
-               instance of ".."; otherwise, leave the ".." as is */
+            /* Remove this instance of ".." and any preceding name if one exists */
             for (j = i - 1; j >= 0; j--) {
                 if (ix[j]) break;
             }
+
+            ix[i] = 0;
             if (j < 0) continue;
             ix[j] = 0;
-            ix[i] = 0;
         }
         /* i will be incremented at the top of the loop */
     }
@@ -195,7 +234,7 @@ JDK_Canonicalize(const char *orig, char *out, int len)
         return -1;
     }
 
-    if (strlen(orig) > PATH_MAX) {
+    if (strlen(orig) >= PATH_MAX) {
         errno = ENAMETOOLONG;
         return -1;
     }
@@ -206,15 +245,20 @@ JDK_Canonicalize(const char *orig, char *out, int len)
         collapse(out);
         return 0;
     } else {
+        if (errno == EINVAL || errno == ELOOP || errno == ENAMETOOLONG || errno == ENOMEM) {
+            return -1;
+        }
+
         /* Something's bogus in the original path, so remove names from the end
            until either some subpath works or we run out of names */
         char *p, *end, *r = NULL;
-        char path[PATH_MAX + 1];
+        char path[PATH_MAX];
 
-        // strlen(orig) <= PATH_MAX, see above
-        strncpy(path, orig, PATH_MAX);
-        // append null for == case
-        path[PATH_MAX] = '\0';
+        strncpy(path, orig, sizeof(path));
+        if (path[PATH_MAX - 1] != '\0') {
+            errno = ENAMETOOLONG;
+            return -1;
+         }
         end = path + strlen(path);
 
         for (p = end; p > path;) {
@@ -242,6 +286,7 @@ JDK_Canonicalize(const char *orig, char *out, int len)
             }
         }
 
+        size_t nameMax;
         if (r != NULL) {
             /* Append unresolved subpath to resolved subpath */
             int rn = strlen(r);
@@ -250,18 +295,45 @@ JDK_Canonicalize(const char *orig, char *out, int len)
                 errno = ENAMETOOLONG;
                 return -1;
             }
+            nameMax = pathconf(r, _PC_NAME_MAX);
+
             if ((rn > 0) && (r[rn - 1] == '/') && (*p == '/')) {
                 /* Avoid duplicate slashes */
                 p++;
             }
             strcpy(r + rn, p);
             collapse(r);
-            return 0;
         } else {
             /* Nothing resolved, so just return the original path */
+            nameMax = pathconf("/", _PC_NAME_MAX);
             strcpy(out, path);
             collapse(out);
-            return 0;
         }
+
+        // Ensure resolve path length is "< PATH_MAX" and collapse() did not overwrite
+        // terminating null byte
+        char resolvedPath[PATH_MAX];
+        strncpy(resolvedPath, out, sizeof(resolvedPath));
+        if (resolvedPath[PATH_MAX - 1] != '\0') {
+            errno = ENAMETOOLONG;
+            return -1;
+        }
+
+        // Ensure resolve path does not contain any components who length is "> NAME_MAX"
+        // If pathconf call failed with -1 or returned 0 in case of permission denial
+        if (nameMax < 1) {
+            nameMax = NAME_MAX;
+        }
+
+        char *component;
+        char *rest = resolvedPath;
+        while ((component = strtok_r(rest, "/", &rest))) {
+            if (strlen(component) > nameMax) {
+                errno = ENAMETOOLONG;
+                return -1;
+            }
+        }
+
+        return 0;
     }
 }
-- 
2.44.0

