1 /*
   2  * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  *
  23  */
  24 
  25 /*
  26  * @test
  27  * @summary SharedArchiveConsistency
  28  * @requires vm.cds
  29  * @library /test/lib
  30  * @modules java.base/jdk.internal.misc
  31  *          java.compiler
  32  *          java.management
  33  *          jdk.jartool/sun.tools.jar
  34  *          jdk.internal.jvmstat/sun.jvmstat.monitor
  35  * @build sun.hotspot.WhiteBox
  36  * @compile test-classes/Hello.java
  37  * @run driver ClassFileInstaller sun.hotspot.WhiteBox
  38  * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI SharedArchiveConsistency
  39  */
  40 import jdk.test.lib.process.OutputAnalyzer;
  41 import jdk.test.lib.Utils;
  42 import java.io.File;
  43 import java.io.FileInputStream;
  44 import java.io.FileOutputStream;
  45 import java.io.IOException;
  46 import java.nio.ByteBuffer;
  47 import java.nio.ByteOrder;
  48 import java.nio.channels.FileChannel;
  49 import java.nio.file.Files;
  50 import java.nio.file.Path;
  51 import java.nio.file.Paths;
  52 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
  53 import java.nio.file.StandardOpenOption;
  54 import static java.nio.file.StandardOpenOption.READ;
  55 import static java.nio.file.StandardOpenOption.WRITE;
  56 import java.util.ArrayList;
  57 import java.util.HashSet;
  58 import java.util.List;
  59 import java.util.Random;
  60 import sun.hotspot.WhiteBox;
  61 
  62 public class SharedArchiveConsistency {
  63     public static WhiteBox wb;
  64     public static int offset_magic;     // CDSFileMapHeaderBase::_magic
  65     public static int offset_version;   // CDSFileMapHeaderBase::_version
  66     public static int offset_jvm_ident; // CDSFileMapHeaderBase::_jvm_ident
  67     public static int offset_end_magic; // CDSFileMapHeaderBase::_end_magic
  68     public static int sp_offset_crc;    // CDSFileMapRegion::_crc
  69     public static int offset_paths_misc_info_size;
  70     public static int file_header_size = -1;// total size of header, variant, need calculation
  71     public static int CDSFileMapRegion_size; // size of CDSFileMapRegion
  72     public static int sp_offset;       // offset of CDSFileMapRegion
  73     public static int sp_used_offset;  // offset of CDSFileMapRegion::_used
  74     public static int size_t_size;     // size of size_t
  75     public static int int_size;        // size of int
  76 
  77     public static File jsa;        // will be updated during test
  78     public static File orgJsaFile; // kept the original file not touched.
  79     // The following should be consistent with the enum in the C++ MetaspaceShared class
  80     public static String[] shared_region_name = {
  81         "mc",          // MiscCode
  82         "rw",          // ReadWrite
  83         "ro",          // ReadOnly
  84         "md",          // MiscData
  85         "first_closed_archive",
  86         "last_closed_archive",
  87         "first_open_archive",
  88         "last_open_archive"
  89     };
  90 
  91     public static int num_regions = shared_region_name.length;
  92     public static String[] matchMessages = {
  93         "Unable to use shared archive",
  94         "An error has occurred while processing the shared archive file.",
  95         "Checksum verification failed.",
  96         "The shared archive file has been truncated."
  97     };
  98 
  99     public static void getFileOffsetInfo() throws Exception {
 100         wb = WhiteBox.getWhiteBox();
 101         offset_magic = wb.getOffsetForName("FileMapHeader::_magic");
 102         offset_version = wb.getOffsetForName("FileMapHeader::_version");
 103         offset_jvm_ident = wb.getOffsetForName("FileMapHeader::_jvm_ident");
 104         offset_end_magic = wb.getOffsetForName("FileMapHeader::_end_magic");
 105         sp_offset_crc = wb.getOffsetForName("CDSFileMapRegion::_crc");
 106         try {
 107             int nonExistOffset = wb.getOffsetForName("FileMapHeader::_non_exist_offset");
 108             System.exit(-1); // should fail
 109         } catch (Exception e) {
 110             // success
 111         }
 112 
 113         sp_offset = wb.getOffsetForName("FileMapHeader::_space[0]") - offset_magic;
 114         sp_used_offset = wb.getOffsetForName("CDSFileMapRegion::_used") - sp_offset_crc;
 115         size_t_size = wb.getOffsetForName("size_t_size");
 116         CDSFileMapRegion_size  = wb.getOffsetForName("CDSFileMapRegion_size");
 117     }
 118 
 119     public static int getFileHeaderSize(FileChannel fc) throws Exception {
 120         if (file_header_size != -1) {
 121             return file_header_size;
 122         }
 123         // this is not real header size, it is struct size
 124         int_size = wb.getOffsetForName("int_size");
 125         file_header_size = wb.getOffsetForName("file_header_size");
 126         offset_paths_misc_info_size = wb.getOffsetForName("FileMapHeader::_paths_misc_info_size") -
 127             offset_magic;
 128         int path_misc_info_size   = (int)readInt(fc, offset_paths_misc_info_size, int_size);
 129         file_header_size += path_misc_info_size;
 130         System.out.println("offset_paths_misc_info_size = " + offset_paths_misc_info_size);
 131         System.out.println("path_misc_info_size   = " + path_misc_info_size);
 132         System.out.println("file_header_size      = " + file_header_size);
 133         file_header_size = (int)align_up_page(file_header_size);
 134         System.out.println("file_header_size (aligned to page) = " + file_header_size);
 135         return file_header_size;
 136     }
 137 
 138     public static long align_up_page(long l) throws Exception {
 139         // wb is obtained in getFileOffsetInfo() which is called first in main() else we should call
 140         // WhiteBox.getWhiteBox() here first.
 141         int pageSize = wb.getVMPageSize();
 142         return (l + pageSize -1) & (~ (pageSize - 1));
 143     }
 144 
 145     private static long getRandomBetween(long start, long end) throws Exception {
 146         if (start > end) {
 147             throw new IllegalArgumentException("start must be less than end");
 148         }
 149         Random aRandom = Utils.getRandomInstance();
 150         int d = aRandom.nextInt((int)(end - start));
 151         if (d < 1) {
 152             d = 1;
 153         }
 154         return start + d;
 155     }
 156 
 157     public static long readInt(FileChannel fc, long offset, int nbytes) throws Exception {
 158         ByteBuffer bb = ByteBuffer.allocate(nbytes);
 159         bb.order(ByteOrder.nativeOrder());
 160         fc.position(offset);
 161         fc.read(bb);
 162         return  (nbytes > 4 ? bb.getLong(0) : bb.getInt(0));
 163     }
 164 
 165     public static void writeData(FileChannel fc, long offset, ByteBuffer bb) throws Exception {
 166         fc.position(offset);
 167         fc.write(bb);
 168         fc.force(true);
 169     }
 170 
 171     public static FileChannel getFileChannel(File jsaFile) throws Exception {
 172         List<StandardOpenOption> arry = new ArrayList<StandardOpenOption>();
 173         arry.add(READ);
 174         arry.add(WRITE);
 175         return FileChannel.open(jsaFile.toPath(), new HashSet<StandardOpenOption>(arry));
 176     }
 177 
 178     public static void modifyJsaContentRandomly(File jsaFile) throws Exception {
 179         FileChannel fc = getFileChannel(jsaFile);
 180         // corrupt random area in the data areas
 181         long[] used    = new long[num_regions];       // record used bytes
 182         long start0, start, end, off;
 183         int used_offset, path_info_size;
 184 
 185         int bufSize;
 186         System.out.printf("%-24s%12s%12s%16s\n", "Space Name", "Used bytes", "Reg Start", "Random Offset");
 187         start0 = getFileHeaderSize(fc);
 188         for (int i = 0; i < num_regions; i++) {
 189             used[i] = get_region_used_size_aligned(fc, i);
 190             start = start0;
 191             for (int j = 0; j < i; j++) {
 192                 start += align_up_page(used[j]);
 193             }
 194             end = start + used[i];
 195             if (start == end) {
 196                 continue; // Ignore empty regions
 197             }
 198             off = getRandomBetween(start, end);
 199             System.out.printf("%-24s%12d%12d%16d\n", shared_region_name[i], used[i], start, off);
 200             if (end - off < 1024) {
 201                 bufSize = (int)(end - off + 1);
 202             } else {
 203                 bufSize = 1024;
 204             }
 205             ByteBuffer bbuf = ByteBuffer.wrap(new byte[bufSize]);
 206             writeData(fc, off, bbuf);
 207         }
 208         if (fc.isOpen()) {
 209             fc.close();
 210         }
 211     }
 212 
 213     static long get_region_used_size_aligned(FileChannel fc, int region) throws Exception {
 214         long n = sp_offset + CDSFileMapRegion_size * region + sp_used_offset;
 215         long alignment = WhiteBox.getWhiteBox().metaspaceReserveAlignment();
 216         long used = readInt(fc, n, size_t_size);
 217         used = (used + alignment - 1) & ~(alignment - 1);
 218         return used;
 219     }
 220 
 221     public static boolean modifyJsaContent(int region, File jsaFile) throws Exception {
 222         FileChannel fc = getFileChannel(jsaFile);
 223         byte[] buf = new byte[4096];
 224         ByteBuffer bbuf = ByteBuffer.wrap(buf);
 225 
 226         long total = 0L;
 227         long[] used = new long[num_regions];
 228         System.out.printf("%-24s%12s\n", "Space name", "Used bytes");
 229         for (int i = 0; i < num_regions; i++) {
 230             used[i] = get_region_used_size_aligned(fc, i);
 231             System.out.printf("%-24s%12d\n", shared_region_name[i], used[i]);
 232             total += used[i];
 233         }
 234         System.out.printf("%-24s%12d\n", "Total: ", total);
 235         long header_size = getFileHeaderSize(fc);
 236         long region_start_offset = header_size;
 237         for (int i=0; i<region; i++) {
 238             region_start_offset += used[i];
 239         }
 240         if (used[region] == 0) {
 241             System.out.println("Region " + shared_region_name[region] + " is empty. Nothing to corrupt.");
 242             return false;
 243         }
 244         System.out.println("Corrupt " + shared_region_name[region] + " section, start = " + region_start_offset
 245                            + " (header_size + 0x" + Long.toHexString(region_start_offset-header_size) + ")");
 246         long bytes_written = 0L;
 247         while (bytes_written < used[region]) {
 248             writeData(fc, region_start_offset + bytes_written, bbuf);
 249             bbuf.clear();
 250             bytes_written += 4096;
 251         }
 252         fc.force(true);
 253         if (fc.isOpen()) {
 254             fc.close();
 255         }
 256         return true;
 257     }
 258 
 259     public static void modifyJsaHeader(File jsaFile) throws Exception {
 260         FileChannel fc = getFileChannel(jsaFile);
 261         // screw up header info
 262         byte[] buf = new byte[getFileHeaderSize(fc)];
 263         ByteBuffer bbuf = ByteBuffer.wrap(buf);
 264         writeData(fc, 0L, bbuf);
 265         if (fc.isOpen()) {
 266             fc.close();
 267         }
 268     }
 269 
 270     public static void modifyJvmIdent() throws Exception {
 271         FileChannel fc = getFileChannel(jsa);
 272         int headerSize = getFileHeaderSize(fc);
 273         System.out.println("    offset_jvm_ident " + offset_jvm_ident);
 274         byte[] buf = new byte[256];
 275         ByteBuffer bbuf = ByteBuffer.wrap(buf);
 276         writeData(fc, (long)offset_jvm_ident, bbuf);
 277         if (fc.isOpen()) {
 278             fc.close();
 279         }
 280     }
 281 
 282     public static void modifyHeaderIntField(long offset, int value) throws Exception {
 283         FileChannel fc = getFileChannel(jsa);
 284         int headerSize = getFileHeaderSize(fc);
 285         System.out.println("    offset " + offset);
 286         byte[] buf = ByteBuffer.allocate(4).putInt(value).array();
 287         ByteBuffer bbuf = ByteBuffer.wrap(buf);
 288         writeData(fc, offset, bbuf);
 289         if (fc.isOpen()) {
 290             fc.close();
 291         }
 292     }
 293 
 294     public static void copyFile(File from, File to) throws Exception {
 295         if (to.exists()) {
 296             if(!to.delete()) {
 297                 throw new IOException("Could not delete file " + to);
 298             }
 299         }
 300         to.createNewFile();
 301         setReadWritePermission(to);
 302         Files.copy(from.toPath(), to.toPath(), REPLACE_EXISTING);
 303     }
 304 
 305     // Copy file with bytes deleted or inserted
 306     // del -- true, deleted, false, inserted
 307     public static void copyFile(File from, File to, boolean del) throws Exception {
 308         try (
 309             FileChannel inputChannel = new FileInputStream(from).getChannel();
 310             FileChannel outputChannel = new FileOutputStream(to).getChannel()
 311         ) {
 312             long size = inputChannel.size();
 313             int init_size = getFileHeaderSize(inputChannel);
 314             outputChannel.transferFrom(inputChannel, 0, init_size);
 315             int n = (int)getRandomBetween(0, 1024);
 316             if (del) {
 317                 System.out.println("Delete " + n + " bytes at data start section");
 318                 inputChannel.position(init_size + n);
 319                 outputChannel.transferFrom(inputChannel, init_size, size - init_size - n);
 320             } else {
 321                 System.out.println("Insert " + n + " bytes at data start section");
 322                 outputChannel.position(init_size);
 323                 outputChannel.write(ByteBuffer.wrap(new byte[n]));
 324                 outputChannel.transferFrom(inputChannel, init_size + n , size - init_size);
 325             }
 326         }
 327     }
 328 
 329     public static void restoreJsaFile() throws Exception {
 330         Files.copy(orgJsaFile.toPath(), jsa.toPath(), REPLACE_EXISTING);
 331     }
 332 
 333     public static void setReadWritePermission(File file) throws Exception {
 334         if (!file.canRead()) {
 335             if (!file.setReadable(true)) {
 336                 throw new IOException("Cannot modify file " + file + " as readable");
 337             }
 338         }
 339         if (!file.canWrite()) {
 340             if (!file.setWritable(true)) {
 341                 throw new IOException("Cannot modify file " + file + " as writable");
 342             }
 343         }
 344     }
 345 
 346     public static void testAndCheck(String[] execArgs) throws Exception {
 347         OutputAnalyzer output = TestCommon.execCommon(execArgs);
 348         String stdtxt = output.getOutput();
 349         System.out.println("Note: this test may fail in very rare occasions due to CRC32 checksum collision");
 350         for (String message : matchMessages) {
 351             if (stdtxt.contains(message)) {
 352                 // match any to return
 353                 return;
 354             }
 355         }
 356         TestCommon.checkExec(output);
 357     }
 358 
 359     // dump with hello.jsa, then
 360     // read the jsa file
 361     //   1) run normal
 362     //   2) modify header
 363     //   3) keep header correct but modify content in each region specified by shared_region_name[]
 364     //   4) update both header and content, test
 365     //   5) delete bytes in data begining
 366     //   6) insert bytes in data begining
 367     //   7) randomly corrupt data in each region specified by shared_region_name[]
 368     public static void main(String... args) throws Exception {
 369         // must call to get offset info first!!!
 370         getFileOffsetInfo();
 371         Path currentRelativePath = Paths.get("");
 372         String currentDir = currentRelativePath.toAbsolutePath().toString();
 373         System.out.println("Current relative path is: " + currentDir);
 374         // get jar file
 375         String jarFile = JarBuilder.getOrCreateHelloJar();
 376 
 377         // dump (appcds.jsa created)
 378         TestCommon.testDump(jarFile, null);
 379 
 380         // test, should pass
 381         System.out.println("1. Normal, should pass but may fail\n");
 382 
 383         String[] execArgs = {"-Xlog:cds", "-cp", jarFile, "Hello"};
 384         // tests that corrupt contents of the archive need to run with
 385         // VerifySharedSpaces enabled to detect inconsistencies
 386         String[] verifyExecArgs = {"-Xlog:cds", "-XX:+VerifySharedSpaces", "-cp", jarFile, "Hello"};
 387 
 388         OutputAnalyzer output = TestCommon.execCommon(execArgs);
 389 
 390         try {
 391             TestCommon.checkExecReturn(output, 0, true, "Hello World");
 392         } catch (Exception e) {
 393             TestCommon.checkExecReturn(output, 1, true, matchMessages[0]);
 394         }
 395 
 396         // get current archive name
 397         jsa = new File(TestCommon.getCurrentArchiveName());
 398         if (!jsa.exists()) {
 399             throw new IOException(jsa + " does not exist!");
 400         }
 401 
 402         setReadWritePermission(jsa);
 403 
 404         // save as original untouched
 405         orgJsaFile = new File(new File(currentDir), "appcds.jsa.bak");
 406         copyFile(jsa, orgJsaFile);
 407 
 408         // modify jsa header, test should fail
 409         System.out.println("\n2. Corrupt header, should fail\n");
 410         modifyJsaHeader(jsa);
 411         output = TestCommon.execCommon(execArgs);
 412         output.shouldContain("The shared archive file has a bad magic number");
 413         output.shouldNotContain("Checksum verification failed");
 414 
 415         copyFile(orgJsaFile, jsa);
 416         // modify _jvm_ident and _paths_misc_info_size, test should fail
 417         System.out.println("\n2a. Corrupt _jvm_ident and _paths_misc_info_size, should fail\n");
 418         modifyJvmIdent();
 419         modifyHeaderIntField(offset_paths_misc_info_size, Integer.MAX_VALUE);
 420         output = TestCommon.execCommon(execArgs);
 421         output.shouldContain("The shared archive file was created by a different version or build of HotSpot");
 422         output.shouldNotContain("Checksum verification failed");
 423 
 424         copyFile(orgJsaFile, jsa);
 425         // modify _magic and _paths_misc_info_size, test should fail
 426         System.out.println("\n2b. Corrupt _magic and _paths_misc_info_size, should fail\n");
 427         modifyHeaderIntField(offset_magic, 0x00000000);
 428         modifyHeaderIntField(offset_paths_misc_info_size, Integer.MAX_VALUE);
 429         output = TestCommon.execCommon(execArgs);
 430         output.shouldContain("The shared archive file has a bad magic number");
 431         output.shouldNotContain("Checksum verification failed");
 432 
 433         copyFile(orgJsaFile, jsa);
 434         // modify _version and _paths_misc_info_size, test should fail
 435         System.out.println("\n2c. Corrupt _version and _paths_misc_info_size, should fail\n");
 436         modifyHeaderIntField(offset_version, 0x00000000);
 437         modifyHeaderIntField(offset_paths_misc_info_size, Integer.MAX_VALUE);
 438         output = TestCommon.execCommon(execArgs);
 439         output.shouldContain("The shared archive file has the wrong version");
 440         output.shouldNotContain("Checksum verification failed");
 441 
 442         copyFile(orgJsaFile, jsa);
 443         // modify _end_magic and _paths_misc_info_size, test should fail
 444         System.out.println("\n2d. Corrupt _end_magic and _paths_misc_info_size, should fail\n");
 445         modifyHeaderIntField(offset_end_magic, 0x00000000);
 446         modifyHeaderIntField(offset_paths_misc_info_size, Integer.MAX_VALUE);
 447         output = TestCommon.execCommon(execArgs);
 448         output.shouldContain("The shared archive file has a bad end magic number");
 449         output.shouldNotContain("Checksum verification failed");
 450 
 451         File newJsaFile = null;
 452         // modify content
 453         System.out.println("\n3. Corrupt Content, should fail\n");
 454         for (int i=0; i<num_regions; i++) {
 455             newJsaFile = new File(TestCommon.getNewArchiveName(shared_region_name[i]));
 456             copyFile(orgJsaFile, newJsaFile);
 457             TestCommon.setCurrentArchiveName(newJsaFile.toString());
 458             if (modifyJsaContent(i, newJsaFile)) {
 459                 testAndCheck(verifyExecArgs);
 460             }
 461         }
 462 
 463         // modify both header and content, test should fail
 464         System.out.println("\n4. Corrupt Header and Content, should fail\n");
 465         newJsaFile = new File(TestCommon.getNewArchiveName("header-and-content"));
 466         copyFile(orgJsaFile, newJsaFile);
 467         TestCommon.setCurrentArchiveName(newJsaFile.toString());
 468         modifyJsaHeader(newJsaFile);
 469         modifyJsaContent(0, newJsaFile);  // this will not be reached since failed on header change first
 470         output = TestCommon.execCommon(execArgs);
 471         output.shouldContain("The shared archive file has a bad magic number");
 472         output.shouldNotContain("Checksum verification failed");
 473 
 474         // delete bytes in data section
 475         System.out.println("\n5. Delete bytes at beginning of data section, should fail\n");
 476         copyFile(orgJsaFile, jsa, true);
 477         TestCommon.setCurrentArchiveName(jsa.toString());
 478         testAndCheck(verifyExecArgs);
 479 
 480         // insert bytes in data section forward
 481         System.out.println("\n6. Insert bytes at beginning of data section, should fail\n");
 482         copyFile(orgJsaFile, jsa, false);
 483         testAndCheck(verifyExecArgs);
 484 
 485         System.out.println("\n7. modify Content in random areas, should fail\n");
 486         newJsaFile = new File(TestCommon.getNewArchiveName("random-areas"));
 487         copyFile(orgJsaFile, newJsaFile);
 488         TestCommon.setCurrentArchiveName(newJsaFile.toString());
 489         modifyJsaContentRandomly(newJsaFile);
 490         testAndCheck(verifyExecArgs);
 491     }
 492 }