8199807: AppCDS performs overly restrictive path matching check.
authorjiangli
Mon, 21 May 2018 15:15:58 -0400
changeset 50199 83d8b3a25f25
parent 50198 db3c6bb7f856
child 50200 9ce050c4711b
8199807: AppCDS performs overly restrictive path matching check. 8203377: Cleanup the usage of os::file_name_strcmp() in SharedPathsMiscInfo::check(). Summary: Relax CDS/AppCDS path check for modules image. Reviewed-by: iklam, ccheung
src/hotspot/os/aix/os_aix.inline.hpp
src/hotspot/os/bsd/os_bsd.inline.hpp
src/hotspot/os/linux/os_linux.inline.hpp
src/hotspot/os/solaris/os_solaris.inline.hpp
src/hotspot/os/windows/os_windows.inline.hpp
src/hotspot/share/classfile/sharedPathsMiscInfo.cpp
src/hotspot/share/memory/filemap.cpp
src/hotspot/share/memory/filemap.hpp
src/hotspot/share/runtime/os.hpp
test/hotspot/jtreg/runtime/appcds/BootClassPathMismatch.java
test/hotspot/jtreg/runtime/appcds/MoveJDKTest.java
test/hotspot/jtreg/runtime/appcds/jigsaw/modulepath/MainModuleOnly.java
--- a/src/hotspot/os/aix/os_aix.inline.hpp	Mon May 21 11:43:57 2018 -0700
+++ b/src/hotspot/os/aix/os_aix.inline.hpp	Mon May 21 15:15:58 2018 -0400
@@ -1,6 +1,6 @@
 /*
- * Copyright (c) 1999, 2016, Oracle and/or its affiliates. All rights reserved.
- * Copyright (c) 2012, 2016 SAP SE. All rights reserved.
+ * Copyright (c) 1999, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2018 SAP SE. 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
@@ -36,9 +36,9 @@
 #include <sys/ioctl.h>
 #include <netdb.h>
 
-// File names are case-sensitive on windows only.
-inline int os::file_name_strcmp(const char* s1, const char* s2) {
-  return strcmp(s1, s2);
+// File names are case-insensitive on windows only.
+inline int os::file_name_strncmp(const char* s1, const char* s2, size_t num) {
+  return strncmp(s1, s2, num);
 }
 
 inline bool os::obsolete_option(const JavaVMOption *option) {
--- a/src/hotspot/os/bsd/os_bsd.inline.hpp	Mon May 21 11:43:57 2018 -0700
+++ b/src/hotspot/os/bsd/os_bsd.inline.hpp	Mon May 21 15:15:58 2018 -0400
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1999, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1999, 2018, 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
@@ -34,9 +34,9 @@
 #include <poll.h>
 #include <netdb.h>
 
-// File names are case-sensitive on windows only
-inline int os::file_name_strcmp(const char* s1, const char* s2) {
-  return strcmp(s1, s2);
+// File names are case-insensitive on windows only
+inline int os::file_name_strncmp(const char* s1, const char* s2, size_t num) {
+  return strncmp(s1, s2, num);
 }
 
 inline bool os::obsolete_option(const JavaVMOption *option) {
--- a/src/hotspot/os/linux/os_linux.inline.hpp	Mon May 21 11:43:57 2018 -0700
+++ b/src/hotspot/os/linux/os_linux.inline.hpp	Mon May 21 15:15:58 2018 -0400
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1999, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1999, 2018, 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
@@ -34,9 +34,9 @@
 #include <poll.h>
 #include <netdb.h>
 
-// File names are case-sensitive on windows only
-inline int os::file_name_strcmp(const char* s1, const char* s2) {
-  return strcmp(s1, s2);
+// File names are case-insensitive on windows only
+inline int os::file_name_strncmp(const char* s1, const char* s2, size_t num) {
+  return strncmp(s1, s2, num);
 }
 
 inline bool os::obsolete_option(const JavaVMOption *option) {
--- a/src/hotspot/os/solaris/os_solaris.inline.hpp	Mon May 21 11:43:57 2018 -0700
+++ b/src/hotspot/os/solaris/os_solaris.inline.hpp	Mon May 21 15:15:58 2018 -0400
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1997, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2018, 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
@@ -37,9 +37,9 @@
 #include <netdb.h>
 #include <setjmp.h>
 
-// File names are case-sensitive on windows only
-inline int os::file_name_strcmp(const char* s1, const char* s2) {
-  return strcmp(s1, s2);
+// File names are case-insensitive on windows only
+inline int os::file_name_strncmp(const char* s1, const char* s2, size_t num) {
+  return strncmp(s1, s2, num);
 }
 
 inline bool os::uses_stack_guard_pages() {
--- a/src/hotspot/os/windows/os_windows.inline.hpp	Mon May 21 11:43:57 2018 -0700
+++ b/src/hotspot/os/windows/os_windows.inline.hpp	Mon May 21 15:15:58 2018 -0400
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1997, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2018, 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
@@ -33,8 +33,8 @@
 inline const int os::default_file_open_flags() { return O_BINARY | O_NOINHERIT;}
 
 // File names are case-insensitive on windows only
-inline int os::file_name_strcmp(const char* s, const char* t) {
-  return _stricmp(s, t);
+inline int os::file_name_strncmp(const char* s, const char* t, size_t num) {
+  return _strnicmp(s, t, num);
 }
 
 inline void  os::dll_unload(void *lib) {
--- a/src/hotspot/share/classfile/sharedPathsMiscInfo.cpp	Mon May 21 11:43:57 2018 -0700
+++ b/src/hotspot/share/classfile/sharedPathsMiscInfo.cpp	Mon May 21 15:15:58 2018 -0400
@@ -28,6 +28,7 @@
 #include "logging/log.hpp"
 #include "logging/logStream.hpp"
 #include "memory/allocation.inline.hpp"
+#include "memory/filemap.hpp"
 #include "memory/metaspaceShared.hpp"
 #include "memory/resourceArea.hpp"
 #include "runtime/arguments.hpp"
@@ -140,12 +141,80 @@
   return true;
 }
 
+char* skip_first_path_entry(const char* path) {
+  size_t path_sep_len = strlen(os::path_separator());
+  char* p = strstr((char*)path, os::path_separator());
+  if (p != NULL) {
+    debug_only( {
+      size_t image_name_len = strlen(MODULES_IMAGE_NAME);
+      assert(strncmp(p - image_name_len, MODULES_IMAGE_NAME, image_name_len) == 0,
+             "first entry must be the modules image");
+    } );
+    p += path_sep_len;
+  } else {
+    debug_only( {
+      assert(ClassLoader::string_ends_with(path, MODULES_IMAGE_NAME),
+             "first entry must be the modules image");
+    } );
+  }
+  return p;
+}
+
 bool SharedPathsMiscInfo::check(jint type, const char* path) {
+  assert(UseSharedSpaces, "runtime only");
   switch (type) {
   case BOOT_PATH:
-    // In the future we should perform the check based on the content of the mapped archive.
-    if (os::file_name_strcmp(path, Arguments::get_sysclasspath()) != 0) {
-      return fail("[BOOT classpath mismatch, actual =", Arguments::get_sysclasspath());
+    {
+      //
+      // - Archive contains boot classes only - relaxed boot path check:
+      //   Extra path elements appended to the boot path at runtime are allowed.
+      //
+      // - Archive contains application or platform classes - strict boot path check:
+      //   Validate the entire runtime boot path, which must be compactible
+      //   with the dump time boot path. Appending boot path at runtime is not
+      //   allowed.
+      //
+
+      // The first entry in boot path is the modules_image (guaranteed by
+      // ClassLoader::setup_boot_search_path()). Skip the first entry. The
+      // path of the runtime modules_image may be different from the dump
+      // time path (e.g. the JDK image is copied to a different location
+      // after generating the shared archive), which is acceptable. For most
+      // common cases, the dump time boot path might contain modules_image only.
+      char* runtime_boot_path = Arguments::get_sysclasspath();
+      char* rp = skip_first_path_entry(runtime_boot_path);
+      char* dp = skip_first_path_entry(path);
+
+      bool relaxed_check = !FileMapInfo::current_info()->header()->has_platform_or_app_classes();
+      if (dp == NULL && rp == NULL) {
+        break;   // ok, both runtime and dump time boot paths have modules_images only
+      } else if (dp == NULL && rp != NULL && relaxed_check) {
+        break;   // ok, relaxed check, runtime has extra boot append path entries
+      } else if (dp != NULL && rp != NULL) {
+        size_t num;
+        size_t dp_len = strlen(dp);
+        size_t rp_len = strlen(rp);
+        if (rp_len >= dp_len) {
+          if (relaxed_check) {
+            // only check the leading entries in the runtime boot path, up to
+            // the length of the dump time boot path
+            num = dp_len;
+          } else {
+            // check the full runtime boot path, must match with dump time
+            num = rp_len;
+          }
+
+          if (os::file_name_strncmp(dp, rp, num) == 0) {
+            // make sure it is the end of an entry in the runtime boot path
+            if (rp[dp_len] == '\0' || rp[dp_len] == os::path_separator()[0]) {
+              break; // ok, runtime and dump time paths match
+            }
+          }
+        }
+      }
+
+      // The paths are different
+      return fail("[BOOT classpath mismatch, actual =", runtime_boot_path);
     }
     break;
   case NON_EXIST:
@@ -160,7 +229,6 @@
     break;
   case APP_PATH:
     {
-      // Prefix is OK: E.g., dump with -cp foo.jar, but run with -cp foo.jar:bar.jar
       size_t len = strlen(path);
       const char *appcp = Arguments::get_appclasspath();
       assert(appcp != NULL, "NULL app classpath");
@@ -168,16 +236,8 @@
       if (appcp_len < len) {
         return fail("Run time APP classpath is shorter than the one at dump time: ", appcp);
       }
-      ResourceMark rm;
-      char* tmp_path;
-      if (len == appcp_len) {
-        tmp_path = (char*)appcp;
-      } else {
-        tmp_path = NEW_RESOURCE_ARRAY(char, len + 1);
-        strncpy(tmp_path, appcp, len);
-        tmp_path[len] = 0;
-      }
-      if (os::file_name_strcmp(path, tmp_path) != 0) {
+      // Prefix is OK: E.g., dump with -cp foo.jar, but run with -cp foo.jar:bar.jar.
+      if (os::file_name_strncmp(path, appcp, len) != 0) {
         return fail("[APP classpath mismatch, actual: -Djava.class.path=", appcp);
       }
       if (appcp[len] != '\0' && appcp[len] != os::path_separator()[0]) {
--- a/src/hotspot/share/memory/filemap.cpp	Mon May 21 11:43:57 2018 -0700
+++ b/src/hotspot/share/memory/filemap.cpp	Mon May 21 15:15:58 2018 -0400
@@ -205,17 +205,23 @@
   _has_platform_or_app_classes = ClassLoaderExt::has_platform_or_app_classes();
 }
 
-void SharedClassPathEntry::init(const char* name, TRAPS) {
+void SharedClassPathEntry::init(const char* name, bool is_modules_image, TRAPS) {
+  assert(DumpSharedSpaces, "dump time only");
   _timestamp = 0;
   _filesize  = 0;
 
   struct stat st;
   if (os::stat(name, &st) == 0) {
     if ((st.st_mode & S_IFMT) == S_IFDIR) {
-      _is_dir = true;
+      _type = dir_entry;
     } else {
-      _is_dir = false;
-      _timestamp = st.st_mtime;
+      // The timestamp of the modules_image is not checked at runtime.
+      if (is_modules_image) {
+        _type = modules_image_entry;
+      } else {
+        _type = jar_entry;
+        _timestamp = st.st_mtime;
+      }
       _filesize = st.st_size;
     }
   } else {
@@ -236,7 +242,18 @@
   assert(UseSharedSpaces, "runtime only");
 
   struct stat st;
-  const char* name = this->name();
+  const char* name;
+
+  // In order to validate the runtime modules image file size against the archived
+  // size information, we need to obtain the runtime modules image path. The recorded
+  // dump time modules image path in the archive may be different from the runtime path
+  // if the JDK image has beed moved after generating the archive.
+  if (is_modules_image()) {
+    name = ClassLoader::get_jrt_entry()->name();
+  } else {
+    name = this->name();
+  }
+
   bool ok = true;
   log_info(class, path)("checking shared classpath entry: %s", name);
   if (os::stat(name, &st) != 0 && is_class_path) {
@@ -251,18 +268,16 @@
       FileMapInfo::fail_continue("directory is not empty: %s", name);
       ok = false;
     }
-  } else if (is_jar_or_bootimage()) {
-    if (_timestamp != st.st_mtime ||
-        _filesize != st.st_size) {
-      ok = false;
-      if (PrintSharedArchiveAndExit) {
-        FileMapInfo::fail_continue(_timestamp != st.st_mtime ?
-                                   "Timestamp mismatch" :
-                                   "File size mismatch");
-      } else {
-        FileMapInfo::fail_continue("A jar/jimage file is not the one used while building"
-                                   " the shared archive file: %s", name);
-      }
+  } else if ((has_timestamp() && _timestamp != st.st_mtime) ||
+             _filesize != st.st_size) {
+    ok = false;
+    if (PrintSharedArchiveAndExit) {
+      FileMapInfo::fail_continue(_timestamp != st.st_mtime ?
+                                 "Timestamp mismatch" :
+                                 "File size mismatch");
+    } else {
+      FileMapInfo::fail_continue("A jar file is not the one used while building"
+                                 " the shared archive file: %s", name);
     }
   }
   return ok;
@@ -298,11 +313,12 @@
   int i = 0;
   ClassPathEntry* cpe = jrt;
   while (cpe != NULL) {
-    const char* type = ((cpe == jrt) ? "jrt" : (cpe->is_jar_file() ? "jar" : "dir"));
+    bool is_jrt = (cpe == jrt);
+    const char* type = (is_jrt ? "jrt" : (cpe->is_jar_file() ? "jar" : "dir"));
     log_info(class, path)("add main shared path (%s) %s", type, cpe->name());
     SharedClassPathEntry* ent = shared_path(i);
-    ent->init(cpe->name(), THREAD);
-    if (cpe != jrt) { // No need to do jimage.
+    ent->init(cpe->name(), is_jrt, THREAD);
+    if (!is_jrt) {    // No need to do the modules image.
       EXCEPTION_MARK; // The following call should never throw, but would exit VM on error.
       update_shared_classpath(cpe, ent, THREAD);
     }
@@ -317,7 +333,7 @@
   while (acpe != NULL) {
     log_info(class, path)("add app shared path %s", acpe->name());
     SharedClassPathEntry* ent = shared_path(i);
-    ent->init(acpe->name(), THREAD);
+    ent->init(acpe->name(), false, THREAD);
     EXCEPTION_MARK;
     update_shared_classpath(acpe, ent, THREAD);
     acpe = acpe->next();
@@ -329,7 +345,7 @@
   while (mpe != NULL) {
     log_info(class, path)("add module path %s",mpe->name());
     SharedClassPathEntry* ent = shared_path(i);
-    ent->init(mpe->name(), THREAD);
+    ent->init(mpe->name(), false, THREAD);
     EXCEPTION_MARK;
     update_shared_classpath(mpe, ent, THREAD);
     mpe = mpe->next();
@@ -417,16 +433,15 @@
   ClassLoaderData* loader_data = ClassLoaderData::the_null_class_loader_data();
   ResourceMark rm(THREAD);
   jint manifest_size;
-  bool isSigned;
 
   if (cpe->is_jar_file()) {
+    assert(ent->is_jar(), "the shared class path entry is not a JAR file");
     char* manifest = ClassLoaderExt::read_manifest(cpe, &manifest_size, CHECK);
     if (manifest != NULL) {
       ManifestStream* stream = new ManifestStream((u1*)manifest,
                                                   manifest_size);
-      isSigned = stream->check_is_signed();
-      if (isSigned) {
-        ent->set_is_signed(true);
+      if (stream->check_is_signed()) {
+        ent->set_is_signed();
       } else {
         // Copy the manifest into the shared archive
         manifest = ClassLoaderExt::read_raw_manifest(cpe, &manifest_size, CHECK);
@@ -436,7 +451,6 @@
         char* p = (char*)(buf->data());
         memcpy(p, manifest, manifest_size);
         ent->set_manifest(buf);
-        ent->set_is_signed(false);
       }
     }
   }
--- a/src/hotspot/share/memory/filemap.hpp	Mon May 21 11:43:57 2018 -0700
+++ b/src/hotspot/share/memory/filemap.hpp	Mon May 21 15:15:58 2018 -0400
@@ -43,25 +43,36 @@
 static const int JVM_IDENT_MAX = 256;
 
 class SharedClassPathEntry {
+  enum {
+    modules_image_entry,
+    jar_entry,
+    signed_jar_entry,
+    dir_entry,
+    unknown_entry
+  };
 protected:
-  bool   _is_dir;
-  time_t _timestamp;          // jar/jimage timestamp,  0 if is directory or other
+  u1     _type;
+  time_t _timestamp;          // jar timestamp,  0 if is directory, modules image or other
   long   _filesize;           // jar/jimage file size, -1 if is directory, -2 if other
   Array<char>* _name;
   Array<u1>*   _manifest;
-  bool _is_signed;
 
 public:
-  void init(const char* name, TRAPS);
+  void init(const char* name, bool is_modules_image, TRAPS);
   void metaspace_pointers_do(MetaspaceClosure* it);
   bool validate(bool is_class_path = true);
 
-  // The _timestamp only gets set for jar files and "modules" jimage.
-  bool is_jar_or_bootimage() {
+  // The _timestamp only gets set for jar files.
+  bool has_timestamp() {
     return _timestamp != 0;
   }
-  bool is_dir() { return _is_dir; }
-  bool is_modules_image() { return ClassLoader::is_modules_image(name()); }
+  bool is_dir()            { return _type == dir_entry; }
+  bool is_modules_image()  { return _type == modules_image_entry; }
+  bool is_jar()            { return _type == jar_entry; }
+  bool is_signed()         { return _type == signed_jar_entry; }
+  void set_is_signed()     {
+    _type = signed_jar_entry;
+  }
   time_t timestamp() const { return _timestamp; }
   long   filesize()  const { return _filesize; }
   const char* name() const { return _name->data(); }
@@ -74,12 +85,6 @@
   void set_manifest(Array<u1>* manifest) {
     _manifest = manifest;
   }
-  void set_is_signed(bool s) {
-    _is_signed = s;
-  }
-  bool is_signed() {
-    return _is_signed;
-  }
 };
 
 class FileMapInfo : public CHeapObj<mtInternal> {
--- a/src/hotspot/share/runtime/os.hpp	Mon May 21 11:43:57 2018 -0700
+++ b/src/hotspot/share/runtime/os.hpp	Mon May 21 15:15:58 2018 -0400
@@ -167,7 +167,7 @@
 
   // File names are case-insensitive on windows only
   // Override me as needed
-  static int    file_name_strcmp(const char* s1, const char* s2);
+  static int    file_name_strncmp(const char* s1, const char* s2, size_t num);
 
   // unset environment variable
   static bool unsetenv(const char* name);
--- a/test/hotspot/jtreg/runtime/appcds/BootClassPathMismatch.java	Mon May 21 11:43:57 2018 -0700
+++ b/test/hotspot/jtreg/runtime/appcds/BootClassPathMismatch.java	Mon May 21 15:15:58 2018 -0400
@@ -34,6 +34,8 @@
  * @run main BootClassPathMismatch
  */
 
+import jdk.test.lib.cds.CDSOptions;
+import jdk.test.lib.cds.CDSTestUtils;
 import jdk.test.lib.process.OutputAnalyzer;
 import java.io.File;
 import java.nio.file.Files;
@@ -51,40 +53,60 @@
 
         BootClassPathMismatch test = new BootClassPathMismatch();
         test.testBootClassPathMismatch();
-        test.testBootClassPathMismatch2();
+        test.testBootClassPathMismatchWithAppClass();
+        test.testBootClassPathMismatchWithBadPath();
+        test.testBootClassPathMatchWithAppend();
         test.testBootClassPathMatch();
     }
 
-    /* Error should be detected if:
+    /* Archive contains boot classes only, with Hello class on -Xbootclasspath/a path.
+     *
+     * Error should be detected if:
      * dump time: -Xbootclasspath/a:${testdir}/hello.jar
      * run-time : -Xbootclasspath/a:${testdir}/newdir/hello.jar
+     *
+     * or
+     * dump time: -Xbootclasspath/a:${testdir}/newdir/hello.jar
+     * run-time : -Xbootclasspath/a:${testdir}/hello.jar
      */
     public void testBootClassPathMismatch() throws Exception {
         String appJar = JarBuilder.getOrCreateHelloJar();
         String appClasses[] = {"Hello"};
-        TestCommon.dump(
-            appJar, appClasses, "-Xbootclasspath/a:" + appJar);
         String testDir = TestCommon.getTestDir("newdir");
         String otherJar = testDir + File.separator + "hello.jar";
+
+        TestCommon.dump(appJar, appClasses, "-Xbootclasspath/a:" + appJar);
         TestCommon.run(
-                "-cp", appJar, "-verbose:class", "-Xbootclasspath/a:" + otherJar, "Hello")
+                "-cp", appJar, "-Xbootclasspath/a:" + otherJar, "Hello")
+            .assertAbnormalExit(mismatchMessage);
+
+        TestCommon.dump(appJar, appClasses, "-Xbootclasspath/a:" + otherJar);
+        TestCommon.run(
+                "-cp", appJar, "-Xbootclasspath/a:" + appJar, "Hello")
             .assertAbnormalExit(mismatchMessage);
     }
 
-    /* Error should be detected if:
-     * dump time: <no bootclasspath specified>
-     * run-time : -Xbootclasspath/a:${testdir}/hello.jar
+    /* Archive contains boot classes only.
+     *
+     * Error should be detected if:
+     * dump time: -Xbootclasspath/a:${testdir}/newdir/hello.jar
+     * run-time : -Xbootclasspath/a:${testdir}/newdir/hello.jar1
      */
-    public void testBootClassPathMismatch2() throws Exception {
-        String appJar = JarBuilder.getOrCreateHelloJar();
+    public void testBootClassPathMismatchWithBadPath() throws Exception {
         String appClasses[] = {"Hello"};
-        TestCommon.dump(appJar, appClasses);
+        String testDir = TestCommon.getTestDir("newdir");
+        String appJar = testDir + File.separator + "hello.jar";
+        String otherJar = testDir + File.separator + "hello.jar1";
+
+        TestCommon.dump(appJar, appClasses, "-Xbootclasspath/a:" + appJar);
         TestCommon.run(
-                "-cp", appJar, "-verbose:class", "-Xbootclasspath/a:" + appJar, "Hello")
+                "-cp", appJar, "-Xbootclasspath/a:" + otherJar, "Hello")
             .assertAbnormalExit(mismatchMessage);
     }
 
-    /* No error if:
+    /* Archive contains boot classes only, with Hello loaded from -Xbootclasspath/a at dump time.
+     *
+     * No error if:
      * dump time: -Xbootclasspath/a:${testdir}/hello.jar
      * run-time : -Xbootclasspath/a:${testdir}/hello.jar
      */
@@ -99,6 +121,37 @@
             .assertNormalExit("[class,load] Hello source: shared objects file");
     }
 
+    /* Archive contains boot classes only, runtime add -Xbootclasspath/a path.
+     *
+     * No error:
+     * dump time: No -Xbootclasspath/a
+     * run-time : -Xbootclasspath/a:${testdir}/hello.jar
+     */
+    public void testBootClassPathMatchWithAppend() throws Exception {
+      CDSOptions opts = new CDSOptions().setUseVersion(false);
+      OutputAnalyzer out = CDSTestUtils.createArchive(opts);
+      CDSTestUtils.checkDump(out);
+
+      String appJar = JarBuilder.getOrCreateHelloJar();
+      opts.addPrefix("-Xbootclasspath/a:" + appJar, "-showversion").addSuffix("Hello");
+      CDSTestUtils.runWithArchiveAndCheck(opts);
+    }
+
+    /* Archive contains app classes, with Hello on -cp path at dump time.
+     *
+     * Error should be detected if:
+     * dump time: <no bootclasspath specified>
+     * run-time : -Xbootclasspath/a:${testdir}/hello.jar
+     */
+    public void testBootClassPathMismatchWithAppClass() throws Exception {
+        String appJar = JarBuilder.getOrCreateHelloJar();
+        String appClasses[] = {"Hello"};
+        TestCommon.dump(appJar, appClasses);
+        TestCommon.run(
+                "-cp", appJar, "-Xbootclasspath/a:" + appJar, "Hello")
+            .assertAbnormalExit(mismatchMessage);
+    }
+
     private static void copyHelloToNewDir() throws Exception {
         String classDir = System.getProperty("test.classes");
         String dstDir = classDir + File.separator + "newdir";
@@ -106,8 +159,14 @@
             Files.createDirectory(Paths.get(dstDir));
         } catch (FileAlreadyExistsException e) { }
 
+        // copy as hello.jar
         Files.copy(Paths.get(classDir, "hello.jar"),
             Paths.get(dstDir, "hello.jar"),
             StandardCopyOption.REPLACE_EXISTING);
+
+        // copy as hello.jar1
+        Files.copy(Paths.get(classDir, "hello.jar"),
+            Paths.get(dstDir, "hello.jar1"),
+            StandardCopyOption.REPLACE_EXISTING);
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/hotspot/jtreg/runtime/appcds/MoveJDKTest.java	Mon May 21 15:15:58 2018 -0400
@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) 2018, 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 Test that CDS still works when the JDK is moved to a new directory
+ * @requires vm.cds
+ * @requires os.family == "linux"
+ * @library /test/lib
+ * @modules java.base/jdk.internal.misc
+ *          java.management
+ *          jdk.jartool/sun.tools.jar
+ * @compile test-classes/Hello.java
+ * @run main MoveJDKTest
+ */
+
+// This test works only on Linux because it depends on symlinks and the name of the hotspot DLL (libjvm.so).
+// It probably doesn't work on Windows.
+// TODO: change libjvm.so to libjvm.dylib on MacOS, before adding "@requires os.family == mac"
+// TODO: test on solaris, before adding "@requires os.family == solaris"
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import jdk.test.lib.process.OutputAnalyzer;
+
+public class MoveJDKTest {
+    public static void main(String[] args) throws Exception {
+        String java_home_src = System.getProperty("java.home");
+        String java_home_dst = System.getProperty("user.dir") + File.separator + "moved_jdk";
+
+        TestCommon.startNewArchiveName();
+        String jsaFile = TestCommon.getCurrentArchiveName();
+        String jsaOpt = "-XX:SharedArchiveFile=" + jsaFile;
+        {
+            ProcessBuilder pb = makeBuilder(java_home_src + "/bin/java", "-Xshare:dump", jsaOpt);
+            TestCommon.executeAndLog(pb, "dump");
+        }
+        {
+            ProcessBuilder pb = makeBuilder(java_home_src + "/bin/java",
+                                            "-Xshare:auto",
+                                            jsaOpt,
+                                            "-Xlog:class+path=info",
+                                            "-version");
+            OutputAnalyzer out = TestCommon.executeAndLog(pb, "exec-src");
+            out.shouldNotContain("shared class paths mismatch");
+            out.shouldNotContain("BOOT classpath mismatch");
+        }
+
+        clone(new File(java_home_src), new File(java_home_dst));
+        System.out.println("============== Cloned JDK at " + java_home_dst);
+
+        // Test runtime with cloned JDK
+        {
+            ProcessBuilder pb = makeBuilder(java_home_dst + "/bin/java",
+                                            "-Xshare:auto",
+                                            jsaOpt,
+                                            "-Xlog:class+path=info",
+                                            "-version");
+            OutputAnalyzer out = TestCommon.executeAndLog(pb, "exec-dst");
+            out.shouldNotContain("shared class paths mismatch");
+            out.shouldNotContain("BOOT classpath mismatch");
+        }
+
+        // Test with bad JAR file name, hello.modules
+        String helloJar = JarBuilder.getOrCreateHelloJar();
+        String fake_modules = copyFakeModulesFromHelloJar();
+        String dumptimeBootAppendOpt = "-Xbootclasspath/a:" + fake_modules;
+        {
+            ProcessBuilder pb = makeBuilder(java_home_src + "/bin/java",
+                                            "-Xshare:dump",
+                                            dumptimeBootAppendOpt,
+                                            jsaOpt);
+            TestCommon.executeAndLog(pb, "dump");
+        }
+        {
+            String runtimeBootAppendOpt = dumptimeBootAppendOpt + System.getProperty("path.separator") + helloJar;
+            ProcessBuilder pb = makeBuilder(java_home_dst + "/bin/java",
+                                            "-Xshare:auto",
+                                            runtimeBootAppendOpt,
+                                            jsaOpt,
+                                            "-Xlog:class+path=info",
+                                            "-version");
+            OutputAnalyzer out = TestCommon.executeAndLog(pb, "exec-dst");
+            out.shouldNotContain("shared class paths mismatch");
+            out.shouldNotContain("BOOT classpath mismatch");
+        }
+    }
+
+    // Do a cheap clone of the JDK. Most files can be sym-linked. However, $JAVA_HOME/bin/java and $JAVA_HOME/lib/.../libjvm.so"
+    // must be copied, because the java.home property is derived from the canonicalized paths of these 2 files.
+    static void clone(File src, File dst) throws Exception {
+        if (dst.exists()) {
+            if (!dst.isDirectory()) {
+                throw new RuntimeException("Not a directory :" + dst);
+            }
+        } else {
+            if (!dst.mkdir()) {
+                throw new RuntimeException("Cannot create directory: " + dst);
+            }
+        }
+        for (String child : src.list()) {
+            if (child.equals(".") || child.equals("..")) {
+                continue;
+            }
+
+            File child_src = new File(src, child);
+            File child_dst = new File(dst, child);
+            if (child_dst.exists()) {
+                throw new RuntimeException("Already exists: " + child_dst);
+            }
+            if (child_src.isFile()) {
+                if (child.equals("libjvm.so") || child.equals("java")) {
+                    Files.copy(child_src.toPath(), /* copy data to -> */ child_dst.toPath());
+                } else {
+                    Files.createSymbolicLink(child_dst.toPath(),  /* link to -> */ child_src.toPath());
+                }
+            } else {
+                clone(child_src, child_dst);
+            }
+        }
+    }
+
+    static ProcessBuilder makeBuilder(String... args) throws Exception {
+        System.out.print("[");
+        for (String s : args) {
+            System.out.print(" " + s);
+        }
+        System.out.println(" ]");
+        return new ProcessBuilder(args);
+    }
+
+    private static String copyFakeModulesFromHelloJar() throws Exception {
+        String classDir = System.getProperty("test.classes");
+        String newFile = "hello.modules";
+        String path = classDir + File.separator + newFile;
+
+        Files.copy(Paths.get(classDir, "hello.jar"),
+            Paths.get(classDir, newFile),
+            StandardCopyOption.REPLACE_EXISTING);
+        return path;
+    }
+}
--- a/test/hotspot/jtreg/runtime/appcds/jigsaw/modulepath/MainModuleOnly.java	Mon May 21 11:43:57 2018 -0700
+++ b/test/hotspot/jtreg/runtime/appcds/jigsaw/modulepath/MainModuleOnly.java	Mon May 21 15:15:58 2018 -0400
@@ -172,7 +172,7 @@
                        "--module-path", moduleDir.toString(),
                        "-m", TEST_MODULE1)
             .assertAbnormalExit(
-                "A jar/jimage file is not the one used while building the shared archive file:");
+                "A jar file is not the one used while building the shared archive file:");
         // create an archive with a non-empty directory in the --module-path.
         // The dumping process will exit with an error due to non-empty directory
         // in the --module-path.