test/hotspot/jtreg/runtime/ReservedStack/ReservedStackTest.java
author iignatyev
Fri, 27 Jul 2018 00:00:28 -0700
changeset 51299 24517a097dc1
parent 50709 483806baeba1
child 51283 ac4f5ef0edd9
permissions -rw-r--r--
8208370: fix typo in ReservedStack tests' @requires Reviewed-by: kvn

/*
 * Copyright (c) 2015, 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 ReservedStackTest
 *
 * @requires vm.opt.DeoptimizeALot == null | vm.opt.DeoptimizeALot == false
 * @library /test/lib
 * @modules java.base/jdk.internal.misc
 * @modules java.base/jdk.internal.vm.annotation
 *
 * @run main/othervm -XX:MaxInlineLevel=2 -XX:CompileCommand=exclude,java/util/concurrent/locks/AbstractOwnableSynchronizer.setExclusiveOwnerThread ReservedStackTest
 */

/* The exclusion of java.util.concurrent.locks.AbstractOwnableSynchronizer.setExclusiveOwnerThread()
 * from the compilable methods is required to ensure that the test will be able
 * to trigger a StackOverflowError on the right method.
 */


/*
 * Notes about this test:
 * This test tries to reproduce a rare but nasty corruption bug that
 * occurs when a StackOverflowError is thrown in some critical sections
 * of the ReentrantLock implementation.
 *
 * Here's the critical section where a corruption could occur
 * (from java.util.concurrent.ReentrantLock.java)
 *
 * final void lock() {
 *     if (compareAndSetState(0, 1))
 *         setExclusiveOwnerThread(Thread.currentThread());
 *     else
 *         acquire(1);
 * }
 *
 * The corruption occurs when the compareAndSetState(0, 1)
 * successfully updates the status of the lock but the method
 * fails to set the owner because of a stack overflow.
 * HotSpot checks for stack overflow on method invocations.
 * The test must trigger a stack overflow either when
 * Thread.currentThread() or setExclusiveOwnerThread() is
 * invoked.
 *
 * The test starts with a recursive invocation loop until a
 * first StackOverflowError is thrown, the Error is caught
 * and a few dozen frames are exited. Now the thread has
 * little free space on its execution stack and will try
 * to trigger a stack overflow in the critical section.
 * The test has a huge array of ReentrantLocks instances.
 * The thread invokes a recursive method which, at each
 * of its invocations, tries to acquire the next lock
 * in the array. The execution continues until a
 * StackOverflowError is thrown or the end of the array
 * is reached.
 * If no StackOverflowError has been thrown, the test
 * is non conclusive (recommendation: increase the size
 * of the ReentrantLock array).
 * The status of all Reentrant locks in the array is checked,
 * if a corruption is detected, the test failed, otherwise
 * the test passed.
 *
 * To have a chance that the stack overflow occurs on one
 * of the two targeted method invocations, the test is
 * repeated in different threads. Each Java thread has a
 * random size area allocated at the beginning of its
 * stack to prevent false sharing. The test relies on this
 * to have different stack alignments when it hits the targeted
 * methods (the test could have been written with a native
 * method with alloca, but using different Java threads makes
 * the test 100% Java).
 *
 * One additional trick is required to ensure that the stack
 * overflow will occur on the Thread.currentThread() getter
 * or the setExclusiveOwnerThread() setter.
 *
 * Potential stack overflows are detected by stack banging,
 * at method invocation time.
 * In interpreted code, the stack banging performed for the
 * lock() method goes further than the stack banging performed
 * for the getter or the setter method, so the potential stack
 * overflow is detected before entering the critical section.
 * In compiled code, the getter and the setter are in-lined,
 * so the stack banging is only performed before entering the
 * critical section.
 * In order to have a stack banging that goes further for the
 * getter/setter methods than for the lock() method, the test
 * exploits the property that interpreter frames are (much)
 * bigger than compiled code frames. When the test is run,
 * a compiler option disables the compilation of the
 * setExclusiveOwnerThread() method.
 *
 */

import java.util.concurrent.locks.ReentrantLock;
import jdk.test.lib.Platform;
import jdk.test.lib.process.ProcessTools;
import jdk.test.lib.process.OutputAnalyzer;

public class ReservedStackTest {

    static class ReentrantLockTest {

        private ReentrantLock lockArray[];
        // Frame sizes vary a lot between interpreted code and compiled code
        // so the lock array has to be big enough to cover all cases.
        // If test fails with message "Not conclusive test", try to increase
        // LOCK_ARRAY_SIZE value
        private static final int LOCK_ARRAY_SIZE = 8192;
        private boolean stackOverflowErrorReceived;
        StackOverflowError soe = null;
        private int index = -1;

        public void initialize() {
            lockArray = new ReentrantLock[LOCK_ARRAY_SIZE];
            for (int i = 0; i < LOCK_ARRAY_SIZE; i++) {
                lockArray[i] = new ReentrantLock();
            }
            stackOverflowErrorReceived = false;
        }

        public String getResult() {
            if (!stackOverflowErrorReceived) {
                return "ERROR: Not conclusive test: no StackOverflowError received";
            }
            for (int i = 0; i < LOCK_ARRAY_SIZE; i++) {
                if (lockArray[i].isLocked()) {
                    if (!lockArray[i].isHeldByCurrentThread()) {
                        StringBuilder s = new StringBuilder();
                        s.append("FAILED: ReentrantLock ");
                        s.append(i);
                        s.append(" looks corrupted");
                        return s.toString();
                    }
                }
            }
            return "PASSED";
        }

        public void run() {
            try {
                lockAndCall(0);
            } catch (StackOverflowError e) {
                soe = e;
                stackOverflowErrorReceived = true;
                throw e;
            }
        }

        private void lockAndCall(int i) {
            index = i;
            if (i < LOCK_ARRAY_SIZE) {
                lockArray[i].lock();
                lockAndCall(i + 1);
            }
        }
    }

    static class RunWithSOEContext implements Runnable {

        int counter;
        int deframe;
        int decounter;
        int setupSOEFrame;
        int testStartFrame;
        ReentrantLockTest test;

        public RunWithSOEContext(ReentrantLockTest test, int deframe) {
            this.test = test;
            this.deframe = deframe;
        }

        @Override
        @jdk.internal.vm.annotation.ReservedStackAccess
        public void run() {
            counter = 0;
            decounter = deframe;
            test.initialize();
            recursiveCall();
            String result = test.getResult();
            // The feature is not fully implemented on all platforms,
            // corruptions are still possible.
            if (isSupportedPlatform && !result.contains("PASSED")) {
                throw new Error(result);
            } else {
                // Either the test passed or this platform is not supported.
                // On not supported platforms, we only expect the VM to
                // not crash during the test. This is especially important
                // on Windows where the detection of SOE in annotated
                // sections is implemented but the reserved zone mechanism
                // to avoid the corruption cannot be implemented yet
                // because of JDK-8067946
                System.out.println("PASSED");
            }
        }

        void recursiveCall() {
            // Unused local variables to increase the frame size
            long l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15, l16, l17, l18, l19;
            long l20, l21, l22, l23, l24, l25, l26, l27, l28, l30, l31, l32, l33, l34, l35, l36, l37;
            counter++;
            try {
                recursiveCall();
            } catch (StackOverflowError e) {
            }
            decounter--;
            if (decounter == 0) {
                setupSOEFrame = counter;
                testStartFrame = counter - deframe;
                test.run();
            }
        }
    }

    private static boolean isAlwaysSupportedPlatform() {
        // Note: To date Aarch64 is the only platform that we don't statically
        // know if it supports the reserved stack area. This is because the
        // open Aarch64 port supports it and the Oracle arm64 port does not.
        return Platform.isAix() ||
            (Platform.isLinux() &&
             (Platform.isPPC() || Platform.isS390x() || Platform.isX64() ||
              Platform.isX86())) ||
            Platform.isOSX() ||
            Platform.isSolaris();
    }

    private static boolean isNeverSupportedPlatform() {
        return !isAlwaysSupportedPlatform() && !Platform.isAArch64();
    }

    private static boolean isSupportedPlatform;

    private static void initIsSupportedPlatform() throws Exception {
        // In order to dynamicaly determine if the platform supports the reserved
        // stack area, run with -XX:StackReservedPages=1 and see if we get the
        // expected warning message for platforms that don't support it.
        ProcessBuilder pb = ProcessTools.createJavaProcessBuilder("-XX:StackReservedPages=1", "-version");
        OutputAnalyzer output = new OutputAnalyzer(pb.start());
        System.out.println("StackReservedPages=1 log: [" + output.getOutput() + "]");
        if (output.getExitValue() != 0) {
            String msg = "Could not launch with -XX:StackReservedPages=1: exit " + output.getExitValue();
            System.err.println("FAILED: " + msg);
            throw new RuntimeException(msg);
        }

        isSupportedPlatform = true;
        String matchStr = "Reserved Stack Area not supported on this platform";
        int match_idx = output.getOutput().indexOf(matchStr);
        if (match_idx >= 0) {
            isSupportedPlatform = false;
        }

        // Do a sanity check. Some platforms we know are always supported. Make sure
        // we didn't determine that one of those platforms is not supported.
        if (!isSupportedPlatform && isAlwaysSupportedPlatform()) {
            String msg  = "This platform should be supported: " + Platform.getOsArch();
            System.err.println("FAILED: " +  msg);
            throw new RuntimeException(msg);
        }

        // And some platforms we know are never supported. Make sure
        // we didn't determine that one of those platforms is supported.
        if (isSupportedPlatform && isNeverSupportedPlatform()) {
            String msg  = "This platform should not be supported: " + Platform.getOsArch();
            System.err.println("FAILED: " +  msg);
            throw new RuntimeException(msg);
        }
    }

    public static void main(String[] args) throws Exception {
        initIsSupportedPlatform();
        for (int i = 0; i < 100; i++) {
            // Each iteration has to be executed by a new thread. The test
            // relies on the random size area pushed by the VM at the beginning
            // of the stack of each Java thread it creates.
            Thread thread = new Thread(new RunWithSOEContext(new ReentrantLockTest(), 256));
            thread.start();
            try {
                thread.join();
            } catch (InterruptedException ex) { }
        }
    }
}