8188122: Path length limits on Windows leads to obscure class loading failures
authorccheung
Tue, 21 Nov 2017 09:49:52 -0800
changeset 48016 df39ee817323
parent 48015 d5bb6637c785
child 48017 24065d1e139c
8188122: Path length limits on Windows leads to obscure class loading failures Summary: Used the unicode version of windows API's to handled long paths and avoid using the stat() function. Reviewed-by: stuefe, iklam
src/hotspot/os/windows/os_windows.cpp
src/hotspot/share/classfile/classLoader.cpp
test/hotspot/jtreg/runtime/LoadClass/LongBCP.java
test/hotspot/jtreg/runtime/LoadClass/test-classes/Hello.java
--- a/src/hotspot/os/windows/os_windows.cpp	Tue Nov 21 09:20:56 2017 -0800
+++ b/src/hotspot/os/windows/os_windows.cpp	Tue Nov 21 09:49:52 2017 -0800
@@ -4061,41 +4061,116 @@
   }
 }
 
+// combine the high and low DWORD into a ULONGLONG
+static ULONGLONG make_double_word(DWORD high_word, DWORD low_word) {
+  ULONGLONG value = high_word;
+  value <<= sizeof(high_word) * 8;
+  value |= low_word;
+  return value;
+}
+
+// Transfers data from WIN32_FILE_ATTRIBUTE_DATA structure to struct stat
+static void file_attribute_data_to_stat(struct stat* sbuf, WIN32_FILE_ATTRIBUTE_DATA file_data) {
+  ::memset((void*)sbuf, 0, sizeof(struct stat));
+  sbuf->st_size = (_off_t)make_double_word(file_data.nFileSizeHigh, file_data.nFileSizeLow);
+  sbuf->st_mtime = make_double_word(file_data.ftLastWriteTime.dwHighDateTime,
+                                  file_data.ftLastWriteTime.dwLowDateTime);
+  sbuf->st_ctime = make_double_word(file_data.ftCreationTime.dwHighDateTime,
+                                  file_data.ftCreationTime.dwLowDateTime);
+  sbuf->st_atime = make_double_word(file_data.ftLastAccessTime.dwHighDateTime,
+                                  file_data.ftLastAccessTime.dwLowDateTime);
+  if ((file_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
+    sbuf->st_mode |= S_IFDIR;
+  } else {
+    sbuf->st_mode |= S_IFREG;
+  }
+}
+
+// The following function is adapted from java.base/windows/native/libjava/canonicalize_md.c
+// Creates an UNC path from a single byte path. Return buffer is
+// allocated in C heap and needs to be freed by the caller.
+// Returns NULL on error.
+static wchar_t* create_unc_path(const char* path, errno_t &err) {
+  wchar_t* wpath = NULL;
+  size_t converted_chars = 0;
+  size_t path_len = strlen(path) + 1; // includes the terminating NULL
+  if (path[0] == '\\' && path[1] == '\\') {
+    if (path[2] == '?' && path[3] == '\\'){
+      // if it already has a \\?\ don't do the prefix
+      wpath = (wchar_t*)os::malloc(path_len * sizeof(wchar_t), mtInternal);
+      if (wpath != NULL) {
+        err = ::mbstowcs_s(&converted_chars, wpath, path_len, path, path_len);
+      } else {
+        err = ENOMEM;
+      }
+    } else {
+      // only UNC pathname includes double slashes here
+      wpath = (wchar_t*)os::malloc((path_len + 7) * sizeof(wchar_t), mtInternal);
+      if (wpath != NULL) {
+        ::wcscpy(wpath, L"\\\\?\\UNC\0");
+        err = ::mbstowcs_s(&converted_chars, &wpath[7], path_len, path, path_len);
+      } else {
+        err = ENOMEM;
+      }
+    }
+  } else {
+    wpath = (wchar_t*)os::malloc((path_len + 4) * sizeof(wchar_t), mtInternal);
+    if (wpath != NULL) {
+      ::wcscpy(wpath, L"\\\\?\\\0");
+      err = ::mbstowcs_s(&converted_chars, &wpath[4], path_len, path, path_len);
+    } else {
+      err = ENOMEM;
+    }
+  }
+  return wpath;
+}
+
+static void destroy_unc_path(wchar_t* wpath) {
+  os::free(wpath);
+}
 
 int os::stat(const char *path, struct stat *sbuf) {
-  char pathbuf[MAX_PATH];
-  if (strlen(path) > MAX_PATH - 1) {
-    errno = ENAMETOOLONG;
+  char* pathbuf = (char*)os::strdup(path, mtInternal);
+  if (pathbuf == NULL) {
+    errno = ENOMEM;
     return -1;
   }
-  os::native_path(strcpy(pathbuf, path));
-  int ret = ::stat(pathbuf, sbuf);
-  if (sbuf != NULL && UseUTCFileTimestamp) {
-    // Fix for 6539723.  st_mtime returned from stat() is dependent on
-    // the system timezone and so can return different values for the
-    // same file if/when daylight savings time changes.  This adjustment
-    // makes sure the same timestamp is returned regardless of the TZ.
-    //
-    // See:
-    // http://msdn.microsoft.com/library/
-    //   default.asp?url=/library/en-us/sysinfo/base/
-    //   time_zone_information_str.asp
-    // and
-    // http://msdn.microsoft.com/library/default.asp?url=
-    //   /library/en-us/sysinfo/base/settimezoneinformation.asp
-    //
-    // NOTE: there is a insidious bug here:  If the timezone is changed
-    // after the call to stat() but before 'GetTimeZoneInformation()', then
-    // the adjustment we do here will be wrong and we'll return the wrong
-    // value (which will likely end up creating an invalid class data
-    // archive).  Absent a better API for this, or some time zone locking
-    // mechanism, we'll have to live with this risk.
-    TIME_ZONE_INFORMATION tz;
-    DWORD tzid = GetTimeZoneInformation(&tz);
-    int daylightBias =
-      (tzid == TIME_ZONE_ID_DAYLIGHT) ?  tz.DaylightBias : tz.StandardBias;
-    sbuf->st_mtime += (tz.Bias + daylightBias) * 60;
-  }
+  os::native_path(pathbuf);
+  int ret;
+  WIN32_FILE_ATTRIBUTE_DATA file_data;
+  // Not using stat() to avoid the problem described in JDK-6539723
+  if (strlen(path) < MAX_PATH) {
+    BOOL bret = ::GetFileAttributesExA(pathbuf, GetFileExInfoStandard, &file_data);
+    if (!bret) {
+      errno = ::GetLastError();
+      ret = -1;
+    }
+    else {
+      file_attribute_data_to_stat(sbuf, file_data);
+      ret = 0;
+    }
+  } else {
+    errno_t err = ERROR_SUCCESS;
+    wchar_t* wpath = create_unc_path(pathbuf, err);
+    if (err != ERROR_SUCCESS) {
+      if (wpath != NULL) {
+        destroy_unc_path(wpath);
+      }
+      os::free(pathbuf);
+      errno = err;
+      return -1;
+    }
+    BOOL bret = ::GetFileAttributesExW(wpath, GetFileExInfoStandard, &file_data);
+    if (!bret) {
+      errno = ::GetLastError();
+      ret = -1;
+    } else {
+      file_attribute_data_to_stat(sbuf, file_data);
+      ret = 0;
+    }
+    destroy_unc_path(wpath);
+  }
+  os::free(pathbuf);
   return ret;
 }
 
@@ -4208,14 +4283,34 @@
 // from src/windows/hpi/src/sys_api_md.c
 
 int os::open(const char *path, int oflag, int mode) {
-  char pathbuf[MAX_PATH];
-
-  if (strlen(path) > MAX_PATH - 1) {
-    errno = ENAMETOOLONG;
+  char* pathbuf = (char*)os::strdup(path, mtInternal);
+  if (pathbuf == NULL) {
+    errno = ENOMEM;
     return -1;
   }
-  os::native_path(strcpy(pathbuf, path));
-  return ::open(pathbuf, oflag | O_BINARY | O_NOINHERIT, mode);
+  os::native_path(pathbuf);
+  int ret;
+  if (strlen(path) < MAX_PATH) {
+    ret = ::open(pathbuf, oflag | O_BINARY | O_NOINHERIT, mode);
+  } else {
+    errno_t err = ERROR_SUCCESS;
+    wchar_t* wpath = create_unc_path(pathbuf, err);
+    if (err != ERROR_SUCCESS) {
+      if (wpath != NULL) {
+        destroy_unc_path(wpath);
+      }
+      os::free(pathbuf);
+      errno = err;
+      return -1;
+    }
+    ret = ::_wopen(wpath, oflag | O_BINARY | O_NOINHERIT, mode);
+    if (ret == -1) {
+      errno = ::GetLastError();
+    }
+    destroy_unc_path(wpath);
+  }
+  os::free(pathbuf);
+  return ret;
 }
 
 FILE* os::open(int fd, const char* mode) {
--- a/src/hotspot/share/classfile/classLoader.cpp	Tue Nov 21 09:20:56 2017 -0800
+++ b/src/hotspot/share/classfile/classLoader.cpp	Tue Nov 21 09:49:52 2017 -0800
@@ -262,11 +262,11 @@
 
 ClassFileStream* ClassPathDirEntry::open_stream(const char* name, TRAPS) {
   // construct full path name
-  char* path = NEW_RESOURCE_ARRAY_IN_THREAD(THREAD, char, JVM_MAXPATHLEN);
-  if (jio_snprintf(path, JVM_MAXPATHLEN, "%s%s%s", _dir, os::file_separator(), name) == -1) {
-    FREE_RESOURCE_ARRAY(char, path, JVM_MAXPATHLEN);
-    return NULL;
-  }
+  assert((_dir != NULL) && (name != NULL), "sanity");
+  size_t path_len = strlen(_dir) + strlen(name) + strlen(os::file_separator()) + 1;
+  char* path = NEW_RESOURCE_ARRAY_IN_THREAD(THREAD, char, path_len);
+  int len = jio_snprintf(path, path_len, "%s%s%s", _dir, os::file_separator(), name);
+  assert(len == (int)(path_len - 1), "sanity");
   // check if file exists
   struct stat st;
   if (os::stat(path, &st) == 0) {
@@ -291,7 +291,7 @@
         if (UsePerfData) {
           ClassLoader::perf_sys_classfile_bytes_read()->inc(num_read);
         }
-        FREE_RESOURCE_ARRAY(char, path, JVM_MAXPATHLEN);
+        FREE_RESOURCE_ARRAY(char, path, path_len);
         // Resource allocated
         return new ClassFileStream(buffer,
                                    st.st_size,
@@ -300,7 +300,7 @@
       }
     }
   }
-  FREE_RESOURCE_ARRAY(char, path, JVM_MAXPATHLEN);
+  FREE_RESOURCE_ARRAY(char, path, path_len);
   return NULL;
 }
 
@@ -381,9 +381,13 @@
 
     if (is_multi_ver) {
       int n;
-      char* entry_name = NEW_RESOURCE_ARRAY_IN_THREAD(THREAD, char, JVM_MAXPATHLEN);
+      const char* version_entry = "META-INF/versions/";
+      // 10 is the max length of a decimal 32-bit non-negative number
+      // 2 includes the '/' and trailing zero
+      size_t entry_name_len = strlen(version_entry) + 10 + strlen(name) + 2;
+      char* entry_name = NEW_RESOURCE_ARRAY_IN_THREAD(THREAD, char, entry_name_len);
       if (version > 0) {
-        n = jio_snprintf(entry_name, JVM_MAXPATHLEN, "META-INF/versions/%d/%s", version, name);
+        n = jio_snprintf(entry_name, entry_name_len, "%s%d/%s", version_entry, version, name);
         entry_name[n] = '\0';
         buffer = open_entry((const char*)entry_name, filesize, false, CHECK_NULL);
         if (buffer == NULL) {
@@ -392,7 +396,7 @@
       }
       if (buffer == NULL) {
         for (int i = cur_ver; i >= base_version; i--) {
-          n = jio_snprintf(entry_name, JVM_MAXPATHLEN, "META-INF/versions/%d/%s", i, name);
+          n = jio_snprintf(entry_name, entry_name_len, "%s%d/%s", version_entry, i, name);
           entry_name[n] = '\0';
           buffer = open_entry((const char*)entry_name, filesize, false, CHECK_NULL);
           if (buffer != NULL) {
@@ -400,7 +404,7 @@
           }
         }
       }
-      FREE_RESOURCE_ARRAY(char, entry_name, JVM_MAXPATHLEN);
+      FREE_RESOURCE_ARRAY(char, entry_name, entry_name_len);
     }
   }
   return buffer;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/hotspot/jtreg/runtime/LoadClass/LongBCP.java	Tue Nov 21 09:49:52 2017 -0800
@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary JVM should be able to handle full path (directory path plus
+ *          class name) or directory path longer than MAX_PATH specified
+ *          in -Xbootclasspath/a on windows.
+ * @library /test/lib
+ * @modules java.base/jdk.internal.misc
+ *          java.management
+ * @run main LongBCP
+ */
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import jdk.test.lib.Platform;
+import jdk.test.lib.compiler.CompilerUtils;
+import jdk.test.lib.process.ProcessTools;
+import jdk.test.lib.process.OutputAnalyzer;
+
+public class LongBCP {
+
+    private static final int MAX_PATH = 260;
+
+    public static void main(String args[]) throws Exception {
+        Path sourceDir = Paths.get(System.getProperty("test.src"), "test-classes");
+        Path classDir = Paths.get(System.getProperty("test.classes"));
+        Path destDir = classDir;
+
+        // create a sub-path so that the destDir length is almost MAX_PATH
+        // so that the full path (with the class name) will exceed MAX_PATH
+        int subDirLen = MAX_PATH - classDir.toString().length() - 2;
+        if (subDirLen > 0) {
+            char[] chars = new char[subDirLen];
+            Arrays.fill(chars, 'x');
+            String subPath = new String(chars);
+            destDir = Paths.get(System.getProperty("test.classes"), subPath);
+        }
+
+        CompilerUtils.compile(sourceDir, destDir);
+
+        String bootCP = "-Xbootclasspath/a:" + destDir.toString();
+        ProcessBuilder pb = ProcessTools.createJavaProcessBuilder(
+            bootCP, "Hello");
+
+        OutputAnalyzer output = new OutputAnalyzer(pb.start());
+        output.shouldContain("Hello World")
+              .shouldHaveExitValue(0);
+
+        // increase the length of destDir to slightly over MAX_PATH
+        destDir = Paths.get(destDir.toString(), "xxxxx");
+        CompilerUtils.compile(sourceDir, destDir);
+
+        bootCP = "-Xbootclasspath/a:" + destDir.toString();
+        pb = ProcessTools.createJavaProcessBuilder(
+            bootCP, "Hello");
+
+        output = new OutputAnalyzer(pb.start());
+        output.shouldContain("Hello World")
+              .shouldHaveExitValue(0);
+
+        // relative path tests
+        // We currently cannot handle relative path specified in the
+        // -Xbootclasspath/a on windows.
+        //
+        // relative path length within the 256 limit
+        char[] chars = new char[255];
+        Arrays.fill(chars, 'y');
+        String subPath = new String(chars);
+        destDir = Paths.get(".", subPath);
+
+        CompilerUtils.compile(sourceDir, destDir);
+
+        bootCP = "-Xbootclasspath/a:" + destDir.toString();
+        pb = ProcessTools.createJavaProcessBuilder(
+            bootCP, "Hello");
+
+        output = new OutputAnalyzer(pb.start());
+        if (!Platform.isWindows()) {
+            output.shouldContain("Hello World")
+                  .shouldHaveExitValue(0);
+        } else {
+            output.shouldContain("Could not find or load main class Hello")
+                  .shouldHaveExitValue(1);
+        }
+
+        // total relative path length exceeds MAX_PATH
+        destDir = Paths.get(destDir.toString(), "yyyyyyyy");
+
+        CompilerUtils.compile(sourceDir, destDir);
+
+        bootCP = "-Xbootclasspath/a:" + destDir.toString();
+        pb = ProcessTools.createJavaProcessBuilder(
+            bootCP, "Hello");
+
+        output = new OutputAnalyzer(pb.start());
+        if (!Platform.isWindows()) {
+            output.shouldContain("Hello World")
+                  .shouldHaveExitValue(0);
+        } else {
+            output.shouldContain("Could not find or load main class Hello")
+                  .shouldHaveExitValue(1);
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/hotspot/jtreg/runtime/LoadClass/test-classes/Hello.java	Tue Nov 21 09:49:52 2017 -0800
@@ -0,0 +1,10 @@
+/*
+ * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
+ * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
+ */
+
+public class Hello {
+  public static void main(String args[]) {
+    System.out.println("Hello World");
+  }
+}