Skip to content

Commit 4a0672f

Browse files
Claude4.6oclaude
andcommitted
Prepare for Java 25+: prefer ReflectionFactory over sun.misc.Unsafe
- Unsafe.java now tries ReflectionFactory.newConstructorForSerialization() first (same mechanism as ObjectInputStream), falling back to sun.misc.Unsafe - ReflectionFactory produces safer objects (runs Object init, fields get defaults) - sun.misc.Unsafe kept as fallback since ReflectionFactory package (jdk.internal.reflect) may not be accessible without --add-opens - Serialization constructors cached per class in ConcurrentHashMap - ClassUtilities.setUseUnsafe() API preserved for backward compatibility Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e62f524 commit 4a0672f

2 files changed

Lines changed: 125 additions & 52 deletions

File tree

src/main/java/com/cedarsoftware/util/ClassUtilities.java

Lines changed: 14 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2664,14 +2664,13 @@ static void trySetAccessible(AccessibleObject object) {
26642664
}
26652665
}
26662666

2667-
// Try instantiation via unsafe (if turned on). It is off by default. Use
2668-
// ClassUtilities.setUseUnsafe(true) to enable it. This may result in heap-dumps
2669-
// for e.g. ConcurrentHashMap or can cause problems when the class is not initialized,
2670-
// that's why we try ordinary constructors first.
2667+
// Try instantiation via ReflectionFactory serialization constructor (if turned on).
2668+
// It is off by default. Use ClassUtilities.setUseUnsafe(true) to enable it.
2669+
// This uses the same mechanism as ObjectInputStream — creates a synthetic constructor
2670+
// that runs Object.<init>() instead of the class's own constructors.
26712671
private static Object tryUnsafeInstantiation(Class<?> c) {
26722672
if (unsafeDepth.get() > 0) {
26732673
try {
2674-
// Security: Apply security checks even in unsafe mode to prevent bypassing security controls
26752674
SecurityChecker.verifyClass(c);
26762675
return unsafe.allocateInstance(c);
26772676
} catch (Exception ignored) {
@@ -2682,44 +2681,21 @@ private static Object tryUnsafeInstantiation(Class<?> c) {
26822681

26832682
/**
26842683
* Turn on (or off) the 'unsafe' option of Class construction for the current thread only.
2685-
* This setting is thread-local and does not affect other threads. The unsafe option uses
2686-
* internal JVM mechanisms to bypass constructors and should be used with extreme caution
2687-
* as it may break on future JDKs or under strict security managers.
2688-
*
2689-
* <p><strong>THREAD SAFETY:</strong> This setting uses ThreadLocal storage, so each thread
2690-
* maintains its own independent unsafe mode state. Enabling unsafe mode in one thread does
2691-
* not affect other threads. This is critical for multi-threaded environments like web servers
2692-
* where concurrent requests must not interfere with each other.</p>
2693-
*
2694-
* <p><strong>SECURITY WARNING:</strong> Enabling unsafe instantiation bypasses normal Java
2695-
* security mechanisms, constructor validations, and initialization logic. This can lead to
2696-
* security vulnerabilities and unstable object states. Only enable in trusted environments
2697-
* where you have full control over the codebase and understand the security implications.</p>
2698-
*
2699-
* <p>It is used when all constructors have been tried and the Java class could
2700-
* not be instantiated. Remember to disable unsafe mode when done (typically in a finally block)
2701-
* to avoid leaving the thread in an altered state.</p>
2684+
* When enabled, allows constructor-bypassing instantiation as a last resort when no
2685+
* suitable constructor can be found.
2686+
* <p>
2687+
* This uses {@code ReflectionFactory.newConstructorForSerialization()} — the same mechanism
2688+
* used by {@code ObjectInputStream} for deserialization. The synthetic constructor runs
2689+
* {@code Object.<init>()} and fields get Java default values (null/0/false).
2690+
* <p>
2691+
* This setting is thread-local and does not affect other threads.
27022692
*
27032693
* @param state boolean true = on, false = off (for the current thread only)
2704-
* @throws SecurityException if a security manager exists and denies the required permissions
27052694
*/
2706-
@SuppressWarnings("removal")
27072695
public static void setUseUnsafe(boolean state) {
2708-
// Add security check for unsafe instantiation access
2709-
SecurityManager sm = System.getSecurityManager();
2710-
if (sm != null && state) {
2711-
// Use a custom permission for enabling unsafe operations in java-util
2712-
// The old "accessClassInPackage.sun.misc" check is outdated for modern JDKs
2713-
sm.checkPermission(new RuntimePermission("com.cedarsoftware.util.enableUnsafe"));
2714-
}
2715-
2716-
// Counter-based approach for reentrant support:
2717-
// - setUseUnsafe(true) increments the counter
2718-
// - setUseUnsafe(false) decrements the counter (but not below 0)
2719-
// - Unsafe mode is active when counter > 0
27202696
if (state) {
27212697
unsafeDepth.set(unsafeDepth.get() + 1);
2722-
// Initialize unsafe singleton on first enable
2698+
// Initialize singleton on first enable
27232699
if (unsafe == null) {
27242700
synchronized (ClassUtilities.class) {
27252701
if (unsafe == null) {
@@ -2729,7 +2705,7 @@ public static void setUseUnsafe(boolean state) {
27292705
// Failed to initialize - revert the increment
27302706
unsafeDepth.set(unsafeDepth.get() - 1);
27312707
if (LOG.isLoggable(Level.FINE)) {
2732-
LOG.log(Level.FINE, "Failed to initialize unsafe instantiation: " + e.getMessage());
2708+
LOG.log(Level.FINE, "Failed to initialize ReflectionFactory instantiation: " + e.getMessage());
27332709
}
27342710
}
27352711
}
Lines changed: 111 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,149 @@
11
package com.cedarsoftware.util;
22

3+
import java.lang.reflect.Constructor;
34
import java.lang.reflect.Field;
45
import java.lang.reflect.Method;
6+
import java.util.concurrent.ConcurrentHashMap;
7+
import java.util.concurrent.ConcurrentMap;
58

69
import static com.cedarsoftware.util.ClassUtilities.trySetAccessible;
710

811
/**
9-
* Wrapper for unsafe, decouples direct usage of sun.misc.* package.
12+
* Provides constructor-bypassing object instantiation using two strategies:
13+
* <ol>
14+
* <li><b>ReflectionFactory</b> (preferred) — creates a synthetic constructor that runs
15+
* {@code Object.<init>()} instead of the class's own constructors. This is the same
16+
* mechanism used by {@code ObjectInputStream}. Fields get Java default values and the
17+
* object header is properly initialized. May not be accessible on JDK 17+ without
18+
* {@code --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED}.</li>
19+
* <li><b>sun.misc.Unsafe</b> (fallback) — allocates raw memory without any constructor call.
20+
* Still accessible on current JDKs but deprecated for removal starting in JDK 26.</li>
21+
* </ol>
1022
*
1123
* @author Kai Hufenback
24+
* John DeRegnaucourt (jdereg@cedarsoft.com)
1225
*/
1326
final class Unsafe {
27+
private final Object reflectionFactory;
28+
private final Method newConstructorForSerialization;
29+
private final Constructor<?> objectConstructor;
1430
private final Object sunUnsafe;
15-
private final Method allocateInstance;
31+
private final Method unsafeAllocateInstance;
32+
private final ConcurrentMap<Class<?>, Constructor<?>> serializationConstructorCache = new ConcurrentHashMap<>();
1633

1734
/**
18-
* Constructs unsafe object, acting as a wrapper.
35+
* Constructs the wrapper, reflectively loading ReflectionFactory and sun.misc.Unsafe.
36+
* At least one must be available, otherwise throws IllegalStateException.
1937
*/
2038
public Unsafe() {
39+
// ── Strategy 1: ReflectionFactory (preferred, same as ObjectInputStream) ──
40+
Object rfInstance = null;
41+
Method ncsMethod = null;
42+
Constructor<?> objCtor = null;
43+
try {
44+
// JDK 9+: jdk.internal.reflect.ReflectionFactory
45+
// JDK 8: sun.reflect.ReflectionFactory
46+
Class<?> rfClass;
47+
try {
48+
rfClass = Class.forName("sun.reflect.ReflectionFactory");
49+
} catch (ClassNotFoundException e) {
50+
rfClass = Class.forName("jdk.internal.reflect.ReflectionFactory");
51+
}
52+
53+
Method getFactory = rfClass.getDeclaredMethod("getReflectionFactory");
54+
rfInstance = getFactory.invoke(null);
55+
ncsMethod = rfClass.getDeclaredMethod("newConstructorForSerialization", Class.class, Constructor.class);
56+
objCtor = Object.class.getDeclaredConstructor();
57+
58+
// Verify it works
59+
Constructor<?> test = (Constructor<?>) ncsMethod.invoke(rfInstance, Object.class, objCtor);
60+
if (test == null) {
61+
rfInstance = null;
62+
ncsMethod = null;
63+
objCtor = null;
64+
}
65+
} catch (Exception ignored) {
66+
rfInstance = null;
67+
ncsMethod = null;
68+
objCtor = null;
69+
}
70+
this.reflectionFactory = rfInstance;
71+
this.newConstructorForSerialization = ncsMethod;
72+
this.objectConstructor = objCtor;
73+
74+
// ── Strategy 2: sun.misc.Unsafe (fallback, deprecated in JDK 23+) ──
75+
Object unsafeObj = null;
76+
Method allocMethod = null;
2177
try {
2278
Class<?> unsafeClass = ClassUtilities.forName("sun.misc.Unsafe", ClassUtilities.getClassLoader(Unsafe.class));
2379
Field f = unsafeClass.getDeclaredField("theUnsafe");
2480
trySetAccessible(f);
25-
sunUnsafe = f.get(null);
26-
allocateInstance = ReflectionUtils.getMethod(unsafeClass, "allocateInstance", Class.class);
27-
} catch (Exception e) {
28-
throw new IllegalStateException("Unable to use sun.misc.Unsafe to construct objects.", e);
81+
unsafeObj = f.get(null);
82+
allocMethod = ReflectionUtils.getMethod(unsafeClass, "allocateInstance", Class.class);
83+
} catch (Exception ignored) {
84+
// sun.misc.Unsafe not available (JDK 26+ or security restriction)
85+
}
86+
this.sunUnsafe = unsafeObj;
87+
this.unsafeAllocateInstance = allocMethod;
88+
89+
if (reflectionFactory == null && sunUnsafe == null) {
90+
throw new IllegalStateException("Neither ReflectionFactory nor sun.misc.Unsafe is available for constructor-bypassing instantiation.");
2991
}
3092
}
3193

3294
/**
33-
* Creates an object without invoking constructor or initializing variables.
34-
* <b>Be careful using this with JDK objects, like URL or ConcurrentHashMap this may bring your VM into troubles.</b>
95+
* Creates an object without invoking the class's own constructors.
96+
* Tries ReflectionFactory first (safer), then falls back to sun.misc.Unsafe.
3597
*
36-
* @param clazz to instantiate
98+
* @param clazz the class to instantiate
3799
* @return allocated Object
100+
* @throws IllegalArgumentException if the class cannot be instantiated
38101
*/
39102
public Object allocateInstance(Class<?> clazz) {
40103
if (clazz == null || clazz.isInterface()) {
41104
String name = clazz == null ? "null" : clazz.getName();
42105
throw new IllegalArgumentException("Unable to create instance of class: " + name);
43106
}
44107

108+
// Strategy 1: ReflectionFactory — serialization constructor cached per class
109+
if (reflectionFactory != null) {
110+
try {
111+
Constructor<?> ctor = serializationConstructorCache.get(clazz);
112+
if (ctor == null) {
113+
ctor = createSerializationConstructor(clazz);
114+
if (ctor != null) {
115+
serializationConstructorCache.put(clazz, ctor);
116+
return ctor.newInstance();
117+
}
118+
} else {
119+
return ctor.newInstance();
120+
}
121+
} catch (Exception ignored) {
122+
// ReflectionFactory failed for this class — fall through to Unsafe
123+
}
124+
}
125+
126+
// Strategy 2: sun.misc.Unsafe — raw allocation fallback
127+
if (sunUnsafe != null && unsafeAllocateInstance != null) {
128+
try {
129+
return ReflectionUtils.call(sunUnsafe, unsafeAllocateInstance, clazz);
130+
} catch (IllegalArgumentException e) {
131+
throw new IllegalArgumentException("Unable to create instance of class: " + clazz.getName(), e);
132+
}
133+
}
134+
135+
throw new IllegalArgumentException("Unable to create instance of class: " + clazz.getName());
136+
}
137+
138+
private Constructor<?> createSerializationConstructor(Class<?> clazz) {
45139
try {
46-
return ReflectionUtils.call(sunUnsafe, allocateInstance, clazz);
47-
} catch (IllegalArgumentException e) {
48-
String name = clazz.getName();
49-
throw new IllegalArgumentException("Unable to create instance of class: " + name, e);
140+
Constructor<?> ctor = (Constructor<?>) newConstructorForSerialization.invoke(reflectionFactory, clazz, objectConstructor);
141+
if (ctor != null) {
142+
trySetAccessible(ctor);
143+
}
144+
return ctor;
145+
} catch (Exception e) {
146+
return null;
50147
}
51148
}
52149
}

0 commit comments

Comments
 (0)