undefect. CWE-407 — 63 sites patched across 27 ecosystems
Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
This commit is contained in:
commit
0a580b313d
70422 changed files with 17213626 additions and 0 deletions
543
test/jdk/java/lang/invoke/ClassValueTest.java
Normal file
543
test/jdk/java/lang/invoke/ClassValueTest.java
Normal file
|
|
@ -0,0 +1,543 @@
|
|||
/*
|
||||
* Copyright (c) 2011, 2025, 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
|
||||
* @bug 8351045 8351996 8358535
|
||||
* @summary tests for class-specific values
|
||||
* @modules java.base/java.lang:+open
|
||||
* @library /test/lib
|
||||
* @run junit ClassValueTest
|
||||
*/
|
||||
|
||||
import java.lang.classfile.ClassFile;
|
||||
import java.lang.constant.ClassDesc;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import jdk.test.lib.util.ForceGC;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* @author jrose
|
||||
*/
|
||||
final class ClassValueTest {
|
||||
static String nameForCV1(Class<?> type) {
|
||||
return "CV1:" + type.getName();
|
||||
}
|
||||
int countForCV1;
|
||||
final ClassValue<String> CV1 = new CV1();
|
||||
private class CV1 extends ClassValue<String> {
|
||||
protected String computeValue(Class<?> type) {
|
||||
countForCV1++;
|
||||
return nameForCV1(type);
|
||||
}
|
||||
}
|
||||
|
||||
static final Class<?>[] CLASSES = {
|
||||
String.class,
|
||||
Integer.class,
|
||||
int.class,
|
||||
boolean[].class,
|
||||
char[][].class,
|
||||
ClassValueTest.class
|
||||
};
|
||||
|
||||
@Test
|
||||
public void testGet() {
|
||||
countForCV1 = 0;
|
||||
for (Class<?> c : CLASSES) {
|
||||
assertEquals(nameForCV1(c), CV1.get(c));
|
||||
}
|
||||
assertEquals(CLASSES.length, countForCV1);
|
||||
for (Class<?> c : CLASSES) {
|
||||
assertEquals(nameForCV1(c), CV1.get(c));
|
||||
}
|
||||
assertEquals(CLASSES.length, countForCV1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemove() {
|
||||
for (Class<?> c : CLASSES) {
|
||||
CV1.get(c);
|
||||
}
|
||||
countForCV1 = 0;
|
||||
int REMCOUNT = 3;
|
||||
for (int i = 0; i < REMCOUNT; i++) {
|
||||
CV1.remove(CLASSES[i]);
|
||||
}
|
||||
assertEquals(0, countForCV1); // no change
|
||||
for (Class<?> c : CLASSES) {
|
||||
assertEquals(nameForCV1(c), CV1.get(c));
|
||||
}
|
||||
assertEquals(REMCOUNT, countForCV1);
|
||||
}
|
||||
|
||||
static String nameForCVN(Class<?> type, int n) {
|
||||
return "CV[" + n + "]" + type.getName();
|
||||
}
|
||||
int countForCVN;
|
||||
class CVN extends ClassValue<String> {
|
||||
final int n;
|
||||
CVN(int n) { this.n = n; }
|
||||
protected String computeValue(Class<?> type) {
|
||||
countForCVN++;
|
||||
return nameForCVN(type, n);
|
||||
}
|
||||
};
|
||||
|
||||
@Test
|
||||
public void testGetMany() {
|
||||
int CVN_COUNT1 = 100, CVN_COUNT2 = 100;
|
||||
CVN cvns[] = new CVN[CVN_COUNT1 * CVN_COUNT2];
|
||||
for (int n = 0; n < cvns.length; n++) {
|
||||
cvns[n] = new CVN(n);
|
||||
}
|
||||
countForCVN = 0;
|
||||
for (int pass = 0; pass <= 2; pass++) {
|
||||
for (int i1 = 0; i1 < CVN_COUNT1; i1++) {
|
||||
eachClass:
|
||||
for (Class<?> c : CLASSES) {
|
||||
for (int i2 = 0; i2 < CVN_COUNT2; i2++) {
|
||||
int n = i1*CVN_COUNT2 + i2;
|
||||
assertEquals(0, countForCVN);
|
||||
assertEquals(nameForCVN(c, n), cvns[n].get(c));
|
||||
cvns[n].get(c); //get it again
|
||||
//System.out.println("getting "+n+":"+cvns[n].get(c));
|
||||
boolean doremove = (((i1 + i2) & 3) == 0);
|
||||
switch (pass) {
|
||||
case 0:
|
||||
assertEquals(1, countForCVN);
|
||||
break;
|
||||
case 1:
|
||||
// remove on middle pass
|
||||
assertEquals(0, countForCVN);
|
||||
if (doremove) {
|
||||
//System.out.println("removing "+n+":"+cvns[n].get(c));
|
||||
cvns[n].remove(c);
|
||||
assertEquals(0, countForCVN);
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
assertEquals(doremove ? 1 : 0, countForCVN);
|
||||
break;
|
||||
}
|
||||
countForCVN = 0;
|
||||
if (i1 > i2 && i1 < i2+5) continue eachClass; // leave diagonal gap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assertEquals(0, countForCVN);
|
||||
System.out.println("[rechecking values]");
|
||||
for (int i = 0; i < cvns.length * 10; i++) {
|
||||
int n = i % cvns.length;
|
||||
for (Class<?> c : CLASSES) {
|
||||
assertEquals(nameForCVN(c, n), cvns[n].get(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final long COMPUTE_TIME_MILLIS = 100;
|
||||
// Adjust this timeout to fail faster for test stalls
|
||||
private static final Duration TIMEOUT = Duration.ofNanos((long) (
|
||||
Duration.of(1, ChronoUnit.MINUTES).toNanos()
|
||||
* Double.parseDouble(System.getProperty("test.timeout.factor", "1.0"))));
|
||||
|
||||
private static void await(CountDownLatch latch) {
|
||||
try {
|
||||
if (!latch.await(TIMEOUT.toNanos(), TimeUnit.NANOSECONDS)) {
|
||||
fail("No signal received");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
fail(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void awaitThreads(Thread... threads) {
|
||||
awaitThreads(Arrays.asList(threads));
|
||||
}
|
||||
|
||||
private static void awaitThreads(Iterable<Thread> threads) {
|
||||
for (var t : threads) {
|
||||
try {
|
||||
if (!t.join(TIMEOUT)) {
|
||||
fail("Thread not stopping " + t);
|
||||
}
|
||||
} catch (InterruptedException ex) {
|
||||
fail(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that get() + remove() can prevent stale value from being installed.
|
||||
* Uses junit to do basic stress.
|
||||
*/
|
||||
@Test
|
||||
void testRemoveStale() throws InterruptedException {
|
||||
CountDownLatch oldInputUsed = new CountDownLatch(1);
|
||||
CountDownLatch inputUpdated = new CountDownLatch(1);
|
||||
AtomicInteger input = new AtomicInteger(0);
|
||||
ClassValue<Integer> cv = new ClassValue<>() {
|
||||
@Override
|
||||
protected Integer computeValue(Class<?> type) {
|
||||
// must get early to represent using outdated input
|
||||
int v = input.get();
|
||||
oldInputUsed.countDown();
|
||||
// ensure input is updated when we return
|
||||
await(inputUpdated);
|
||||
return v;
|
||||
}
|
||||
};
|
||||
var innocuous = Thread.startVirtualThread(() -> cv.get(int.class));
|
||||
var refreshInput = Thread.startVirtualThread(() -> {
|
||||
await(oldInputUsed);
|
||||
input.incrementAndGet();
|
||||
cv.remove(int.class); // Let's recompute with updated inputs!
|
||||
inputUpdated.countDown();
|
||||
});
|
||||
awaitThreads(innocuous, refreshInput);
|
||||
assertEquals(1, input.get(), "input not updated");
|
||||
assertEquals(1, cv.get(int.class), "CV not using up-to-date input");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that calling get() from computeValue() terminates.
|
||||
*/
|
||||
@Test
|
||||
void testGetInCompute() {
|
||||
ClassValue<Object> cv = new ClassValue<>() {
|
||||
@Override
|
||||
protected Object computeValue(Class<?> type) {
|
||||
get(type);
|
||||
get(type);
|
||||
get(type);
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
};
|
||||
try {
|
||||
cv.get(int.class);
|
||||
} catch (Throwable ex) {
|
||||
// swallow if any
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that calling remove() from computeValue() terminates.
|
||||
*/
|
||||
@Test
|
||||
void testRemoveInCompute() {
|
||||
ClassValue<Boolean> cv = new ClassValue<>() {
|
||||
@Override
|
||||
protected Boolean computeValue(Class<?> type) {
|
||||
remove(type);
|
||||
remove(type);
|
||||
remove(type);
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
};
|
||||
assertTrue(cv.get(int.class));
|
||||
}
|
||||
|
||||
private static Class<?> createWeakClass() {
|
||||
var bytes = ClassFile.of().build(ClassDesc.of("WeakTest"), _ -> {});
|
||||
try {
|
||||
return MethodHandles.lookup().defineHiddenClass(bytes, true).lookupClass();
|
||||
} catch (IllegalAccessException ex) {
|
||||
return fail(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWeakAgainstClass() {
|
||||
Class<?> hidden = createWeakClass();
|
||||
ClassValue<int[]> cv = new ClassValue<>() {
|
||||
@Override
|
||||
protected int[] computeValue(Class<?> type) {
|
||||
return new int[23];
|
||||
}
|
||||
};
|
||||
|
||||
WeakReference<?> ref = new WeakReference<>(cv.get(hidden));
|
||||
hidden = null; // Remove reference for interpreter
|
||||
if (!ForceGC.wait(() -> ref.refersTo(null))) {
|
||||
fail("Timeout");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // JDK-8352622
|
||||
void testWeakAgainstClassValue() {
|
||||
ClassValue<int[]> cv = new ClassValue<>() {
|
||||
@Override
|
||||
protected int[] computeValue(Class<?> type) {
|
||||
return new int[23];
|
||||
}
|
||||
};
|
||||
|
||||
WeakReference<?> ref = new WeakReference<>(cv.get(int.class));
|
||||
cv = null; // Remove reference for interpreter
|
||||
if (!ForceGC.wait(() -> {
|
||||
CV1.get(int.class); // flush the weak maps
|
||||
return ref.refersTo(null);
|
||||
})) {
|
||||
fail("Timeout");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSingletonWinner() {
|
||||
CountDownLatch raceStart = new CountDownLatch(1);
|
||||
ClassValue<int[]> cv = new ClassValue<>() {
|
||||
@Override
|
||||
protected int[] computeValue(Class<?> type) {
|
||||
await(raceStart);
|
||||
return new int[] {ThreadLocalRandom.current().nextInt()};
|
||||
}
|
||||
};
|
||||
AtomicReference<int[]> truth = new AtomicReference<>(null);
|
||||
AtomicInteger truthSwapCount = new AtomicInteger(0);
|
||||
|
||||
List<Thread> threads = new ArrayList<>(100);
|
||||
Runnable job = () -> {
|
||||
var res = cv.get(ClassValueTest.class);
|
||||
var item = truth.compareAndExchange(null, res);
|
||||
if (item != null) {
|
||||
assertSame(item, res);
|
||||
} else {
|
||||
truthSwapCount.incrementAndGet();
|
||||
}
|
||||
};
|
||||
for (int i = 0; i < 100; i++) {
|
||||
threads.add(Thread.startVirtualThread(job));
|
||||
}
|
||||
raceStart.countDown();
|
||||
awaitThreads(threads);
|
||||
assertEquals(1, truthSwapCount.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRacyRemoveInCompute() {
|
||||
ClassValue<Object> cv = new ClassValue<>() {
|
||||
@Override
|
||||
protected Object computeValue(Class<?> type) {
|
||||
remove(type);
|
||||
try {
|
||||
Thread.sleep(COMPUTE_TIME_MILLIS);
|
||||
} catch (InterruptedException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
remove(type);
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
};
|
||||
|
||||
var threads = Arrays.stream(CLASSES)
|
||||
.map(clz -> Thread.startVirtualThread(() -> cv.get(clz)))
|
||||
.toList();
|
||||
awaitThreads(threads);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecursiveInitialization() {
|
||||
record Holder() {
|
||||
static final ClassValue<Object> clv = new ClassValue<>() {
|
||||
@Override
|
||||
protected Object computeValue(Class<?> type) {
|
||||
return new One();
|
||||
}
|
||||
};
|
||||
|
||||
record One() {
|
||||
static {
|
||||
Holder.clv.get(One.class);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Holder.clv.get(Holder.One.class);
|
||||
}
|
||||
|
||||
private static final ScopedValue<Integer> THREAD_ID = ScopedValue.newInstance();
|
||||
|
||||
@Test
|
||||
void testNoRecomputeOnUnrelatedRemoval() throws InterruptedException {
|
||||
CountDownLatch t1Started = new CountDownLatch(1);
|
||||
CountDownLatch removeTamper = new CountDownLatch(1);
|
||||
CountDownLatch t2Started = new CountDownLatch(1);
|
||||
CountDownLatch t1Returned = new CountDownLatch(1);
|
||||
AtomicInteger t1Tries = new AtomicInteger();
|
||||
ClassValue<Object> clv = new ClassValue<>() {
|
||||
@Override
|
||||
protected Object computeValue(Class<?> type) {
|
||||
int id = THREAD_ID.get();
|
||||
if (id == 1) {
|
||||
t1Tries.incrementAndGet();
|
||||
t1Started.countDown();
|
||||
await(t2Started); // implies unrelated changes
|
||||
} else if (id == 2) {
|
||||
t2Started.countDown();
|
||||
// Don't race to install - we want to check computeValue tampers
|
||||
await(t1Returned);
|
||||
} else if (id != -1) {
|
||||
fail("Id: " + id);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
ScopedValue.where(THREAD_ID, -1).run(() -> clv.get(long.class)); // set up unrelated class
|
||||
var t1 = Thread.startVirtualThread(() ->
|
||||
ScopedValue.where(THREAD_ID, 1).run(() -> {
|
||||
clv.get(int.class);
|
||||
t1Returned.countDown(); // returned after x calls to computeValue
|
||||
}));
|
||||
var t2 = Thread.startVirtualThread(() ->
|
||||
ScopedValue.where(THREAD_ID, 2).run(() -> {
|
||||
await(removeTamper);
|
||||
clv.get(int.class); // clv version diff from that of promise
|
||||
}));
|
||||
await(t1Started);
|
||||
clv.remove(long.class);
|
||||
removeTamper.countDown(); // removed unrelated class
|
||||
awaitThreads(t1, t2);
|
||||
assertEquals(1, t1Tries.get(), "Redundant computeValue retries");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNoObsoleteInstallation() throws InterruptedException {
|
||||
CountDownLatch slowComputationStart = new CountDownLatch(1);
|
||||
CountDownLatch slowComputationContinue = new CountDownLatch(1);
|
||||
ClassValue<Integer> clv = new ClassValue<>() {
|
||||
@Override
|
||||
protected Integer computeValue(Class<?> type) {
|
||||
if (!THREAD_ID.isBound()) {
|
||||
return 5;
|
||||
}
|
||||
int threadId = THREAD_ID.get();
|
||||
if (threadId == 1) {
|
||||
if (slowComputationContinue.getCount() == 0) {
|
||||
return 42;
|
||||
} else {
|
||||
// First invocation
|
||||
slowComputationStart.countDown();
|
||||
await(slowComputationContinue);
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
return fail("Unknown thread " + threadId);
|
||||
}
|
||||
}
|
||||
};
|
||||
var t = Thread.startVirtualThread(() -> ScopedValue.where(THREAD_ID, 1).run(() -> {
|
||||
int v = clv.get(int.class);
|
||||
assertEquals(42, v, "recomputed value");
|
||||
}));
|
||||
await(slowComputationStart);
|
||||
assertEquals(5, clv.get(int.class), "fast computation installed value");
|
||||
clv.remove(int.class);
|
||||
slowComputationContinue.countDown();
|
||||
awaitThreads(t);
|
||||
assertEquals(42, clv.get(int.class), "slow computation reinstalled value");
|
||||
}
|
||||
|
||||
// ClassValue cache invalidated and not reinstated when another
|
||||
// unrelated entry is removed
|
||||
@Test
|
||||
public void testCacheRefresh() throws Throwable {
|
||||
// Setup
|
||||
var lookup = MethodHandles.privateLookupIn(ClassValue.class, MethodHandles.lookup());
|
||||
var classValueEntryClass = Class.forName("java.lang.ClassValue$Entry");
|
||||
MethodHandle getCacheCarefully = lookup.findStatic(ClassValue.class, "getCacheCarefully",
|
||||
MethodType.methodType(classValueEntryClass.arrayType(), Class.class));
|
||||
var classValueMapClass = Class.forName("java.lang.ClassValue$ClassValueMap");
|
||||
MethodHandle probeHomeLocation = lookup.findStatic(classValueMapClass, "probeHomeLocation",
|
||||
MethodType.methodType(classValueEntryClass, classValueEntryClass.arrayType(), ClassValue.class));
|
||||
MethodHandle match = lookup.findVirtual(ClassValue.class, "match",
|
||||
MethodType.methodType(boolean.class, classValueEntryClass));
|
||||
|
||||
// Work
|
||||
ClassValue<?> clv = new ClassValue<>() {
|
||||
@Override
|
||||
protected String computeValue(Class<?> type) {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
// A class that shouldn't have arbitrary values stuffing the cache
|
||||
var cleanClass = clv.getClass();
|
||||
clv.get(cleanClass); // create cache on clean class
|
||||
assertTrue(checkDirectCacheMatch(
|
||||
getCacheCarefully,
|
||||
probeHomeLocation,
|
||||
match,
|
||||
clv,
|
||||
cleanClass
|
||||
));
|
||||
clv.get(int.class);
|
||||
clv.remove(int.class); // invalidate cache on clean class
|
||||
assertFalse(checkDirectCacheMatch(
|
||||
getCacheCarefully,
|
||||
probeHomeLocation,
|
||||
match,
|
||||
clv,
|
||||
cleanClass
|
||||
));
|
||||
clv.get(cleanClass);
|
||||
assertTrue(checkDirectCacheMatch(
|
||||
getCacheCarefully,
|
||||
probeHomeLocation,
|
||||
match,
|
||||
clv,
|
||||
cleanClass
|
||||
));
|
||||
}
|
||||
|
||||
private static boolean checkDirectCacheMatch(
|
||||
MethodHandle getCacheCarefully,
|
||||
MethodHandle probeHomeLocation,
|
||||
MethodHandle match,
|
||||
ClassValue<?> clv,
|
||||
Class<?> cl
|
||||
) throws Throwable {
|
||||
Object cache = getCacheCarefully.invoke(cl);
|
||||
Object entry = probeHomeLocation.invoke(cache, clv);
|
||||
return (boolean) match.invoke(clv, entry);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue