Skip to content

Commit 9f83bfb

Browse files
committed
HHH-18901 The entity enhancement process made defensive against concurrent enhancement
In particular, made it defensive against concurrent enhancement of the same resource, or of resources that might trigger loading symbols which another thread is re-processing.
1 parent 2a6d810 commit 9f83bfb

File tree

10 files changed

+273
-47
lines changed

10 files changed

+273
-47
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* SPDX-License-Identifier: LGPL-2.1-or-later
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.bytecode.enhance.internal.bytebuddy;
6+
7+
import java.util.Objects;
8+
import java.util.concurrent.ConcurrentHashMap;
9+
import java.util.concurrent.ConcurrentMap;
10+
11+
import net.bytebuddy.description.type.TypeDescription;
12+
import net.bytebuddy.pool.TypePool;
13+
14+
/**
15+
* A CacheProvider for ByteBuddy which is specifically designed for
16+
* our CoreTypePool: it ensures the cache content doesn't get tainted
17+
* by model types or anything else which isn't responsibility of this
18+
* particular type pool.
19+
* @implNote This cache instance shares the same @{@link CorePrefixFilter}
20+
* instance of the @{@link CoreTypePool} which is using it, and uses it
21+
* to guard writes into its internal caches.
22+
*/
23+
class CoreCacheProvider implements TypePool.CacheProvider {
24+
25+
private final ConcurrentMap<String, TypePool.Resolution> storage = new ConcurrentHashMap<>();
26+
private final CorePrefixFilter acceptedPrefixes;
27+
28+
CoreCacheProvider(final CorePrefixFilter acceptedPrefixes) {
29+
this.acceptedPrefixes = Objects.requireNonNull( acceptedPrefixes );
30+
register(
31+
Object.class.getName(),
32+
new TypePool.Resolution.Simple( TypeDescription.ForLoadedType.of( Object.class ) )
33+
);
34+
}
35+
36+
/**
37+
* {@inheritDoc}
38+
*/
39+
@Override
40+
public TypePool.Resolution find(final String name) {
41+
return storage.get( name );
42+
}
43+
44+
/**
45+
* {@inheritDoc}
46+
*/
47+
@Override
48+
public TypePool.Resolution register(String name, TypePool.Resolution resolution) {
49+
//Ensure we DO NOT cache anything from a non-core namespace, to not leak application specific code:
50+
if ( acceptedPrefixes.isCoreClassName( name ) ) {
51+
TypePool.Resolution cached = storage.putIfAbsent( name, resolution );
52+
return cached == null
53+
? resolution
54+
: cached;
55+
}
56+
else {
57+
return resolution;
58+
}
59+
}
60+
61+
/**
62+
* {@inheritDoc}
63+
*/
64+
@Override
65+
public void clear() {
66+
storage.clear();
67+
}
68+
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* SPDX-License-Identifier: LGPL-2.1-or-later
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.bytecode.enhance.internal.bytebuddy;
6+
7+
import java.util.Objects;
8+
9+
/**
10+
* We differentiate between core classes and application classes during symbol
11+
* resolution for the purposes of entity enhancement.
12+
* The discriminator is the prefix of the fully qualified classname, for
13+
* example it could be package names.
14+
* The "core classes" don't have to be comprehensively defined: we want a small
15+
* set of prefixes for which we know with certainty that a)They won't be used
16+
* in application code (assuming people honour reasonable package name rules)
17+
* or any code that needs being enhanced. and b) frequently need to be looked up
18+
* during the enhancement process.
19+
* A great example is the "jakarta.persistence.Entity" annotation: we'll most likely
20+
* need to load it when doing any form of introspection on user's code, but we expect
21+
* the bytecode which represents the annotation to not be enhanced.
22+
* We then benefit from caching such representations of object types which are frequently
23+
* loaded; since caching end user code would lead to enhancement problems, it's best
24+
* to keep the list conservative when in doubt.
25+
* For example, don't consider all of {@code "org.hibernate."} prefixes as safe, as
26+
* that would include entities used during our own testsuite and entities defined by Envers.
27+
*
28+
*/
29+
public final class CorePrefixFilter {
30+
31+
private final String[] acceptedPrefixes;
32+
public static final CorePrefixFilter DEFAULT_INSTANCE = new CorePrefixFilter();
33+
34+
/**
35+
* Do not invoke: use DEFAULT_INSTANCE
36+
*/
37+
CorePrefixFilter() {
38+
//By default optimise for jakarta annotations, java util collections, and Hibernate marker interfaces
39+
this("jakarta.", "java.", "org.hibernate.annotations.", "org.hibernate.bytecode.enhance.spi.", "org.hibernate.engine.spi.");
40+
}
41+
42+
public CorePrefixFilter(final String... acceptedPrefixes) {
43+
this.acceptedPrefixes = Objects.requireNonNull( acceptedPrefixes );
44+
}
45+
46+
public boolean isCoreClassName(final String name) {
47+
for ( String acceptedPrefix : this.acceptedPrefixes ) {
48+
if ( name.startsWith( acceptedPrefix ) ) {
49+
return true;
50+
}
51+
}
52+
return false;
53+
}
54+
55+
}

hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CoreTypePool.java

+16-18
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55
package org.hibernate.bytecode.enhance.internal.bytebuddy;
66

7+
import java.util.Objects;
78
import java.util.concurrent.ConcurrentHashMap;
89

910
import net.bytebuddy.description.type.TypeDescription;
@@ -25,42 +26,39 @@ public class CoreTypePool extends TypePool.AbstractBase implements TypePool {
2526

2627
private final ClassLoader hibernateClassLoader = CoreTypePool.class.getClassLoader();
2728
private final ConcurrentHashMap<String, Resolution> resolutions = new ConcurrentHashMap<>();
28-
private final String[] acceptedPrefixes;
29+
private final CorePrefixFilter acceptedPrefixes;
2930

3031
/**
31-
* Construct a new {@link CoreTypePool} with its default configuration:
32-
* to only load classes whose package names start with either "jakarta."
33-
* or "java."
32+
* Construct a new {@link CoreTypePool} with its default configuration.
33+
*
34+
* @see CorePrefixFilter
3435
*/
3536
public CoreTypePool() {
36-
//By default optimise for jakarta annotations, and java util collections
37-
this("jakarta.", "java.", "org.hibernate.annotations.");
37+
this( CorePrefixFilter.DEFAULT_INSTANCE );
3838
}
3939

4040
/**
4141
* Construct a new {@link CoreTypePool} with a choice of which prefixes
4242
* for fully qualified classnames will be loaded by this {@link TypePool}.
43+
*
44+
* @deprecated used by Quarkus
4345
*/
46+
@Deprecated
4447
public CoreTypePool(final String... acceptedPrefixes) {
48+
this( new CorePrefixFilter( acceptedPrefixes ) );
49+
}
50+
51+
public CoreTypePool(CorePrefixFilter acceptedPrefixes) {
4552
//While we implement a cache in this class we also want to enable
4653
//ByteBuddy's default caching mechanism as it will cache the more
4754
//useful output of the parsing and introspection of such types.
48-
super( new TypePool.CacheProvider.Simple() );
49-
this.acceptedPrefixes = acceptedPrefixes;
50-
}
51-
52-
private boolean isCoreClassName(final String name) {
53-
for ( String acceptedPrefix : this.acceptedPrefixes ) {
54-
if ( name.startsWith( acceptedPrefix ) ) {
55-
return true;
56-
}
57-
}
58-
return false;
55+
super( new CoreCacheProvider( acceptedPrefixes ) );
56+
this.acceptedPrefixes = Objects.requireNonNull( acceptedPrefixes );
5957
}
6058

6159
@Override
6260
protected Resolution doDescribe(final String name) {
63-
if ( isCoreClassName( name ) ) {
61+
if ( acceptedPrefixes.isCoreClassName( name ) ) {
6462
final Resolution resolution = resolutions.get( name );
6563
if ( resolution != null ) {
6664
return resolution;

hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java

+13-1
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,18 @@ public void discoverTypes(String className, byte[] originalBytes) {
163163
}
164164

165165
private DynamicType.Builder<?> doEnhance(Supplier<DynamicType.Builder<?>> builderSupplier, TypeDescription managedCtClass) {
166+
// skip if the class was already enhanced. This is very common in WildFly as classloading is highly concurrent.
167+
// We need to ensure that no class is instrumented multiple times as that might result in incorrect bytecode.
168+
// N.B. there is a second check below using a different approach: checking for the marker interfaces,
169+
// which does not address the case of extended bytecode enhancement
170+
// (because it enhances classes that do not end up with these marker interfaces).
171+
// I'm currently inclined to keep both checks, as one is safer and the other has better backwards compatibility.
172+
if ( managedCtClass.getDeclaredAnnotations().isAnnotationPresent( EnhancementInfo.class ) ) {
173+
verifyVersions( managedCtClass, enhancementContext );
174+
log.debugf( "Skipping enhancement of [%s]: it's already annotated with @EnhancementInfo", managedCtClass.getName() );
175+
return null;
176+
}
177+
166178
// can't effectively enhance interfaces
167179
if ( managedCtClass.isInterface() ) {
168180
log.debugf( "Skipping enhancement of [%s]: it's an interface", managedCtClass.getName() );
@@ -179,7 +191,7 @@ private DynamicType.Builder<?> doEnhance(Supplier<DynamicType.Builder<?>> builde
179191
if ( alreadyEnhanced( managedCtClass ) ) {
180192
verifyVersions( managedCtClass, enhancementContext );
181193

182-
log.debugf( "Skipping enhancement of [%s]: already enhanced", managedCtClass.getName() );
194+
log.debugf( "Skipping enhancement of [%s]: it's already implementing 'Managed'", managedCtClass.getName() );
183195
return null;
184196
}
185197

hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ModelTypePool.java

+12-3
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ public class ModelTypePool extends TypePool.Default implements EnhancerClassLoca
1818

1919
private final ConcurrentHashMap<String, Resolution> resolutions = new ConcurrentHashMap<>();
2020
private final OverridingClassFileLocator locator;
21+
private final SafeCacheProvider poolCache;
2122

22-
private ModelTypePool(CacheProvider cacheProvider, OverridingClassFileLocator classFileLocator, CoreTypePool parent) {
23+
private ModelTypePool(SafeCacheProvider cacheProvider, OverridingClassFileLocator classFileLocator, CoreTypePool parent) {
2324
super( cacheProvider, classFileLocator, ReaderMode.FAST, parent );
25+
this.poolCache = cacheProvider;
2426
this.locator = classFileLocator;
2527
}
2628

@@ -60,7 +62,7 @@ public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFile
6062
* @return
6163
*/
6264
public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFileLocator, CoreTypePool coreTypePool) {
63-
return buildModelTypePool( classFileLocator, coreTypePool, new TypePool.CacheProvider.Simple() );
65+
return buildModelTypePool( classFileLocator, coreTypePool, new SafeCacheProvider() );
6466
}
6567

6668
/**
@@ -70,7 +72,7 @@ public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFile
7072
* @param cacheProvider
7173
* @return
7274
*/
73-
public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFileLocator, CoreTypePool coreTypePool, CacheProvider cacheProvider) {
75+
public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFileLocator, CoreTypePool coreTypePool, SafeCacheProvider cacheProvider) {
7476
Objects.requireNonNull( classFileLocator );
7577
Objects.requireNonNull( coreTypePool );
7678
Objects.requireNonNull( cacheProvider );
@@ -90,6 +92,13 @@ protected Resolution doDescribe(final String name) {
9092

9193
@Override
9294
public void registerClassNameAndBytes(final String className, final byte[] bytes) {
95+
//Very important: ensure the registered override is actually effective in case this class
96+
//was already resolved in the recent past; this could have happened for example as a side effect
97+
//of symbol resolution during enhancement of a different class, or very simply when attempting
98+
//to re-enhanced the same class - which happens frequently in WildFly because of the class transformers
99+
//being triggered concurrently by multiple parallel deployments.
100+
resolutions.remove( className );
101+
poolCache.remove( className );
93102
locator.put( className, new ClassFileLocator.Resolution.Explicit( Objects.requireNonNull( bytes ) ) );
94103
}
95104

hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/OverridingClassFileLocator.java

+39-9
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,42 @@
55
package org.hibernate.bytecode.enhance.internal.bytebuddy;
66

77
import java.io.IOException;
8+
import java.util.HashMap;
89
import java.util.Objects;
9-
import java.util.concurrent.ConcurrentHashMap;
1010

1111
import net.bytebuddy.dynamic.ClassFileLocator;
1212

1313
/**
1414
* Allows wrapping another ClassFileLocator to add the ability to define
1515
* resolution overrides for specific resources.
16+
* This is useful when enhancing entities and we need to process an
17+
* input byte array representing the bytecode of the entity, and some
18+
* external party is providing the byte array explicitly, avoiding for
19+
* us having to load it from the classloader.
20+
* We'll still need to load several other symbols from a parent @{@link ClassFileLocator}
21+
* (typically based on the classloader), but need to return the provided
22+
* byte array for some selected resources.
23+
* Any override is scoped to the current thread; this is to avoid
24+
* interference among threads in systems which perform parallel
25+
* class enhancement, for example containers requiring "on the fly"
26+
* transformation of classes as they are being loaded will often
27+
* perform such operations concurrently.
1628
*/
17-
public final class OverridingClassFileLocator implements ClassFileLocator {
29+
final class OverridingClassFileLocator implements ClassFileLocator {
1830

19-
private final ConcurrentHashMap<String, Resolution> registeredResolutions = new ConcurrentHashMap<>();
31+
private final ThreadLocal<HashMap<String, Resolution>> registeredResolutions = ThreadLocal.withInitial( () -> new HashMap<>() );
2032
private final ClassFileLocator parent;
2133

22-
public OverridingClassFileLocator(final ClassFileLocator parent) {
34+
/**
35+
* @param parent the @{@link ClassFileLocator} which will be used to load any resource which wasn't explicitly registered as an override.
36+
*/
37+
OverridingClassFileLocator(final ClassFileLocator parent) {
2338
this.parent = Objects.requireNonNull( parent );
2439
}
2540

2641
@Override
2742
public Resolution locate(final String name) throws IOException {
28-
final Resolution resolution = registeredResolutions.get( name );
43+
final Resolution resolution = getLocalMap().get( name );
2944
if ( resolution != null ) {
3045
return resolution;
3146
}
@@ -34,17 +49,32 @@ public Resolution locate(final String name) throws IOException {
3449
}
3550
}
3651

52+
private HashMap<String, Resolution> getLocalMap() {
53+
return registeredResolutions.get();
54+
}
55+
3756
@Override
38-
public void close() throws IOException {
39-
//Nothing to do: we're not responsible for parent
57+
public void close() {
58+
registeredResolutions.remove();
4059
}
4160

61+
/**
62+
* Registers an explicit resolution override
63+
*
64+
* @param className
65+
* @param explicit
66+
*/
4267
void put(String className, Resolution.Explicit explicit) {
43-
registeredResolutions.put( className, explicit );
68+
getLocalMap().put( className, explicit );
4469
}
4570

71+
/**
72+
* Removes an explicit resolution override
73+
*
74+
* @param className
75+
*/
4676
void remove(String className) {
47-
registeredResolutions.remove( className );
77+
getLocalMap().remove( className );
4878
}
4979

5080
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* SPDX-License-Identifier: LGPL-2.1-or-later
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.bytecode.enhance.internal.bytebuddy;
6+
7+
import java.util.HashMap;
8+
import java.util.Map;
9+
10+
import net.bytebuddy.pool.TypePool;
11+
12+
/**
13+
* An implementation of @{@link net.bytebuddy.pool.TypePool.CacheProvider} which scopes
14+
* all state to the current thread, and allows to remove specific registrations.
15+
* The threadscoping is necessary to resolve a race condition happening during concurrent entity enhancement:
16+
* while one thread is resolving metadata about the entity which needs to be enhanced, other threads
17+
* might be working on the same operation (or a different entity which needs this one to be resolved)
18+
* and the resolution output - potentially shared across them - could be tainted as they do need
19+
* to occasionally work on different input because of the specific overrides managed via @{@link OverridingClassFileLocator}.
20+
*/
21+
final class SafeCacheProvider implements TypePool.CacheProvider {
22+
23+
private final ThreadLocal<Map<String, TypePool.Resolution>> delegate = ThreadLocal.withInitial( () -> new HashMap<>() );
24+
25+
@Override
26+
public TypePool.Resolution find(final String name) {
27+
return delegate.get().get( name );
28+
}
29+
30+
@Override
31+
public TypePool.Resolution register(final String name, final TypePool.Resolution resolution) {
32+
final TypePool.Resolution cached = delegate.get().putIfAbsent( name, resolution );
33+
return cached == null
34+
? resolution
35+
: cached;
36+
}
37+
38+
@Override
39+
public void clear() {
40+
delegate.get().clear();
41+
delegate.remove();
42+
}
43+
44+
public TypePool.Resolution remove(final String name) {
45+
return delegate.get().remove( name );
46+
}
47+
}

0 commit comments

Comments
 (0)