blob: fa9fe51485a16e69945d04f02ee0aded1772b7d2 [file] [log] [blame]
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicReference;
import sun.misc.Unsafe;
class Main {
public static void main(String args[]) throws Exception {
// Stress-test read-modify-write operations in adjacent memory locations.
// This is intended to uncover bugs triggered by spurious CAS failures on
// architectures where such spurious failures can happen. Bug: 218453177
$noinline$testVarHandleBytes();
$noinline$testVarHandleInts();
$noinline$testVarHandleLongs();
$noinline$testVarHandleReferences();
$noinline$testUnsafeInts();
$noinline$testUnsafeLongs();
$noinline$testUnsafeReferences();
// Stress-test read-modify-write operations on the same memory locations.
// This is intended to uncover bugs with false-positive comparison in CAS.
$noinline$testAtomicReference();
}
public static void $noinline$testVarHandleBytes() throws Exception {
// Prepare `VarHandle` objects.
VarHandle[] vhs = new VarHandle[] {
MethodHandles.lookup().findVarHandle(FourBytes.class, "b1", byte.class),
MethodHandles.lookup().findVarHandle(FourBytes.class, "b2", byte.class),
MethodHandles.lookup().findVarHandle(FourBytes.class, "b3", byte.class),
MethodHandles.lookup().findVarHandle(FourBytes.class, "b4", byte.class)
};
// Prepare threads.
final FourBytes fourBytes = new FourBytes();
final StopFlag stopFlag = new StopFlag();
Thread[] threads = new Thread[4];
for (int i = 0; i != 4; ++i) {
final VarHandle vh = vhs[i];
threads[i] = new Thread() {
public void run() {
byte value = 0;
while (!stopFlag.stop) {
byte nextValue = (byte) (value + 1);
boolean success = vh.compareAndSet(fourBytes, value, nextValue);
assertTrue(success);
value = nextValue;
}
}
};
}
// Start threads.
for (int i = 0; i != 4; ++i) {
threads[i].start();
}
// Let the threads run for 5s.
Thread.sleep(5000);
// Stop threads.
stopFlag.stop = true;
for (int i = 0; i != 4; ++i) {
threads[i].join();
}
}
public static void $noinline$testVarHandleInts() throws Exception {
// Prepare `VarHandle` objects.
VarHandle[] vhs = new VarHandle[] {
MethodHandles.lookup().findVarHandle(FourInts.class, "i1", int.class),
MethodHandles.lookup().findVarHandle(FourInts.class, "i2", int.class),
MethodHandles.lookup().findVarHandle(FourInts.class, "i3", int.class),
MethodHandles.lookup().findVarHandle(FourInts.class, "i4", int.class)
};
// Prepare threads.
final FourInts fourInts = new FourInts();
final StopFlag stopFlag = new StopFlag();
Thread[] threads = new Thread[4];
for (int i = 0; i != 4; ++i) {
final VarHandle vh = vhs[i];
threads[i] = new Thread() {
public void run() {
int value = 0;
while (!stopFlag.stop) {
int nextValue = value + 1;
boolean success = vh.compareAndSet(fourInts, value, nextValue);
assertTrue(success);
value = nextValue;
}
}
};
}
// Start threads.
for (int i = 0; i != 4; ++i) {
threads[i].start();
}
// Let the threads run for 5s.
Thread.sleep(5000);
// Stop threads.
stopFlag.stop = true;
for (int i = 0; i != 4; ++i) {
threads[i].join();
}
}
public static void $noinline$testVarHandleLongs() throws Exception {
// Prepare `VarHandle` objects.
VarHandle[] vhs = new VarHandle[] {
MethodHandles.lookup().findVarHandle(FourLongs.class, "l1", long.class),
MethodHandles.lookup().findVarHandle(FourLongs.class, "l2", long.class),
MethodHandles.lookup().findVarHandle(FourLongs.class, "l3", long.class),
MethodHandles.lookup().findVarHandle(FourLongs.class, "l4", long.class)
};
// Prepare threads.
final FourLongs fourLongs = new FourLongs();
final StopFlag stopFlag = new StopFlag();
Thread[] threads = new Thread[4];
for (int i = 0; i != 4; ++i) {
final VarHandle vh = vhs[i];
threads[i] = new Thread() {
public void run() {
long value = 0;
while (!stopFlag.stop) {
long nextValue = value + 1L;
boolean success = vh.compareAndSet(fourLongs, value, nextValue);
assertTrue(success);
value = nextValue;
}
}
};
}
// Start threads.
for (int i = 0; i != 4; ++i) {
threads[i].start();
}
// Let the threads run for 5s.
Thread.sleep(5000);
// Stop threads.
stopFlag.stop = true;
for (int i = 0; i != 4; ++i) {
threads[i].join();
}
}
public static void $noinline$testVarHandleReferences() throws Exception {
// Prepare `VarHandle` objects.
VarHandle[] vhs = new VarHandle[] {
MethodHandles.lookup().findVarHandle(FourReferences.class, "r1", Object.class),
MethodHandles.lookup().findVarHandle(FourReferences.class, "r2", Object.class),
MethodHandles.lookup().findVarHandle(FourReferences.class, "r3", Object.class),
MethodHandles.lookup().findVarHandle(FourReferences.class, "r4", Object.class)
};
// Prepare threads.
final FourReferences fourReferences = new FourReferences();
Object[] values = new Object[] {
null,
new Object(),
new Object(),
new Object()
};
final StopFlag stopFlag = new StopFlag();
Thread[] threads = new Thread[4];
for (int i = 0; i != 4; ++i) {
final VarHandle vh = vhs[i];
threads[i] = new Thread() {
public void run() {
int index = 0;
while (!stopFlag.stop) {
Object value = values[index];
index = (index + 1) & 3;
Object nextValue = values[index];
boolean success = vh.compareAndSet(fourReferences, value, nextValue);
assertTrue(success);
}
}
};
}
// Start threads.
for (int i = 0; i != 4; ++i) {
threads[i].start();
}
// Allocate memory to trigger some GCs
for (int i = 0; i != 640 * 1024; ++i) {
$noinline$allocateAtLeast1KiB();
}
// Stop threads.
stopFlag.stop = true;
for (int i = 0; i != 4; ++i) {
threads[i].join();
}
}
public static void $noinline$testUnsafeInts() throws Exception {
// Prepare Unsafe offsets.
final Unsafe unsafe = getUnsafe();
long[] offsets = new long[] {
unsafe.objectFieldOffset(FourInts.class.getField("i1")),
unsafe.objectFieldOffset(FourInts.class.getField("i2")),
unsafe.objectFieldOffset(FourInts.class.getField("i3")),
unsafe.objectFieldOffset(FourInts.class.getField("i4"))
};
// Prepare threads.
final FourInts fourInts = new FourInts();
final StopFlag stopFlag = new StopFlag();
Thread[] threads = new Thread[4];
for (int i = 0; i != 4; ++i) {
final long offset = offsets[i];
threads[i] = new Thread() {
public void run() {
int value = 0;
while (!stopFlag.stop) {
int nextValue = value + 1;
boolean success = unsafe.compareAndSwapInt(
fourInts, offset, value, nextValue);
assertTrue(success);
value = nextValue;
}
}
};
}
// Start threads.
for (int i = 0; i != 4; ++i) {
threads[i].start();
}
// Let the threads run for 5s.
Thread.sleep(5000);
// Stop threads.
stopFlag.stop = true;
for (int i = 0; i != 4; ++i) {
threads[i].join();
}
}
public static void $noinline$testUnsafeLongs() throws Exception {
// Prepare Unsafe offsets.
final Unsafe unsafe = getUnsafe();
long[] offsets = new long[] {
unsafe.objectFieldOffset(FourLongs.class.getField("l1")),
unsafe.objectFieldOffset(FourLongs.class.getField("l2")),
unsafe.objectFieldOffset(FourLongs.class.getField("l3")),
unsafe.objectFieldOffset(FourLongs.class.getField("l4"))
};
// Prepare threads.
final FourLongs fourLongs = new FourLongs();
final StopFlag stopFlag = new StopFlag();
Thread[] threads = new Thread[4];
for (int i = 0; i != 4; ++i) {
final long offset = offsets[i];
threads[i] = new Thread() {
public void run() {
long value = 0;
while (!stopFlag.stop) {
long nextValue = value + 1L;
boolean success = unsafe.compareAndSwapLong(
fourLongs, offset, value, nextValue);
assertTrue(success);
value = nextValue;
}
}
};
}
// Start threads.
for (int i = 0; i != 4; ++i) {
threads[i].start();
}
// Let the threads run for 5s.
Thread.sleep(5000);
// Stop threads.
stopFlag.stop = true;
for (int i = 0; i != 4; ++i) {
threads[i].join();
}
}
public static void $noinline$testUnsafeReferences() throws Exception {
// Prepare Unsafe offsets.
// D8 rewrites the bytecode with a workaround for CAS bug. To test the raw
// `Unsafe.compareAndSwapObject()` call, we implement the call in smali
// and wrap it in an indirect call.
final UnsafeDispatch unsafeDispatch =
(UnsafeDispatch) Class.forName("UnsafeWrapper").newInstance();
final Unsafe unsafe = getUnsafe();
long[] offsets = new long[] {
unsafe.objectFieldOffset(FourReferences.class.getField("r1")),
unsafe.objectFieldOffset(FourReferences.class.getField("r2")),
unsafe.objectFieldOffset(FourReferences.class.getField("r3")),
unsafe.objectFieldOffset(FourReferences.class.getField("r4"))
};
// Prepare threads.
final FourReferences fourReferences = new FourReferences();
Object[] values = new Object[] {
null,
new Object(),
new Object(),
new Object()
};
final StopFlag stopFlag = new StopFlag();
Thread[] threads = new Thread[4];
for (int i = 0; i != 4; ++i) {
final long offset = offsets[i];
threads[i] = new Thread() {
public void run() {
int index = 0;
while (!stopFlag.stop) {
Object value = values[index];
index = (index + 1) & 3;
Object nextValue = values[index];
boolean success = unsafeDispatch.compareAndSwapObject(
unsafe, fourReferences, offset, value, nextValue);
assertTrue(success);
}
}
};
}
// Start threads.
for (int i = 0; i != 4; ++i) {
threads[i].start();
}
// Allocate memory to trigger some GCs
for (int i = 0; i != 640 * 1024; ++i) {
$noinline$allocateAtLeast1KiB();
}
// Stop threads.
stopFlag.stop = true;
for (int i = 0; i != 4; ++i) {
threads[i].join();
}
}
// Instead of using a `VarHandle` directly, this test uses `AtomicReference` which is
// implemented using a `VarHandle`. This is because the normal `VarHandle` checks are
// done without read barrier which makes them likely to fail and take the slow-path to
// the runtime while the GC is marking (which is the case we're most interested in).
// The `AtomicReference` uses a boot-image `VarHandle` which is optimized to avoid
// those checks, making it more likely to hit bugs in the raw RMW operation.
public static void $noinline$testAtomicReference() throws Exception {
// Prepare `AtomicReference` object.
// D8 rewrites the bytecode with a workaround for CAS bug. To test the raw
// `AtomicReference.compareAndSet()` call, we implement the call in smali
// and wrap it in an indirect call.
final AtomicReferenceDispatch atomicReferenceDispatch =
(AtomicReferenceDispatch) Class.forName("AtomicReferenceWrapper").newInstance();
final AtomicReference aref = new AtomicReference(null);
// Prepare threads.
final Object[] objects = new Object[] {
null,
new Object(),
new Object(),
new Object()
};
final StopFlag stopFlag = new StopFlag();
Thread[] threads = new Thread[4];
for (int i = 0; i != 4; ++i) {
if (i == 0) {
threads[i] = new Thread() {
public void run() {
int index = 0;
Object value = objects[index];
while (!stopFlag.stop) {
index = (index + 1) & 3;
Object nextValue = objects[index];
boolean success = atomicReferenceDispatch.compareAndSet(
aref, value, nextValue);
assertTrue(success);
value = nextValue;
}
}
};
} else {
final Object value = objects[i];
assertTrue(value != null);
threads[i] = new Thread() {
public void run() {
// This thread is trying to overwrite a value with the same value.
// For a false-positive in CAS compare, it would actually change
// the value and cause the thread `threads[0]` to fail.
assertTrue(value != null);
while (!stopFlag.stop) {
// Do not check the return value.
atomicReferenceDispatch.compareAndSet(aref, value, value);
}
}
};
}
};
// Start threads.
for (int i = 0; i != 4; ++i) {
threads[i].start();
}
// Allocate memory to trigger some GCs
for (int i = 0; i != 640 * 1024; ++i) {
$noinline$allocateAtLeast1KiB();
}
// Stop threads.
stopFlag.stop = true;
for (int i = 0; i != 4; ++i) {
threads[i].join();
}
}
public static void assertTrue(boolean value) {
if (!value) {
throw new Error("Assertion failed!");
}
}
public static Unsafe getUnsafe() throws Exception {
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
Field f = unsafeClass.getDeclaredField("theUnsafe");
f.setAccessible(true);
return (Unsafe) f.get(null);
}
public static void $noinline$allocateAtLeast1KiB() {
// Give GC more work by allocating Object arrays.
memory[allocationIndex] = new Object[1024 / 4];
++allocationIndex;
if (allocationIndex == memory.length) {
allocationIndex = 0;
}
}
// We shall retain some allocated memory and release old allocations
// so that the GC has something to do.
public static Object[] memory = new Object[1024];
public static int allocationIndex = 0;
}
class StopFlag {
public volatile boolean stop = false;
}
class FourBytes {
public byte b1 = (byte) 0;
public byte b2 = (byte) 0;
public byte b3 = (byte) 0;
public byte b4 = (byte) 0;
}
class FourInts {
public int i1 = 0;
public int i2 = 0;
public int i3 = 0;
public int i4 = 0;
}
class FourLongs {
public long l1 = 0L;
public long l2 = 0L;
public long l3 = 0L;
public long l4 = 0L;
}
class FourReferences {
public Object r1 = null;
public Object r2 = null;
public Object r3 = null;
public Object r4 = null;
}
abstract class UnsafeDispatch {
public abstract boolean compareAndSwapObject(
Unsafe unsafe, Object obj, long offset, Object expected, Object new_value);
}
abstract class AtomicReferenceDispatch {
public abstract boolean compareAndSet(AtomicReference aref, Object expected, Object new_value);
}