wendigo commented on issue #3460:
URL:
https://github.com/apache/maven-surefire/issues/3460#issuecomment-5570352137
Root cause, confirmed via bytecode analysis + full stack trace (`Caused by:`
chain):
`StackWalkerStrategy.<clinit>` calls
`Thread.currentThread().getContextClassLoader()` and passes it to
`ReflectionUtils.tryLoadClass(ClassLoader, String)`:
```java
public static Class<?> tryLoadClass(ClassLoader loader, String name) {
try {
return loader.loadClass(name); // NPE if loader == null
} catch (NoClassDefFoundError | ClassNotFoundException e) {
return null;
}
}
```
This only catches `NoClassDefFoundError`/`ClassNotFoundException` — **not
`NullPointerException`**. When the current thread's context classloader happens
to be `null` at the moment `StackWalkerStrategy` is first touched,
`tryLoadClass` throws an uncaught NPE, which propagates out of `<clinit>` as
`ExceptionInInitializerError`, permanently poisoning the class for the rest of
the JVM fork's lifetime — every subsequent reference (from any thread, any
test) then fails with `NoClassDefFoundError: Could not initialize class
...StackWalkerStrategy`.
Full first-occurrence trace we captured:
```
Caused by: java.lang.ExceptionInInitializerError: Exception
java.lang.NullPointerException [in thread "main"]
at
org.apache.maven.surefire.api.util.ReflectionUtils.tryLoadClass(ReflectionUtils.java:152)
at
org.apache.maven.surefire.api.report.StackWalkerStrategy.<clinit>(StackWalkerStrategy.java:60)
at
org.apache.maven.surefire.api.report.StackTraceProvider.getStack(StackTraceProvider.java:107)
at
org.apache.maven.surefire.api.report.TestOutputReportEntry.<init>(TestOutputReportEntry.java:52)
...
at
jakarta.validation.Validation$ProviderSpecificBootstrapImpl.configure(Validation.java:214)
```
In our case the trigger is Jakarta Bean Validation's provider bootstrap
(`Validation$ProviderSpecificBootstrapImpl.configure()`), which is known to
temporarily swap/clear the thread's context classloader while doing its
`ServiceLoader`-based provider discovery — a fairly common pattern in JVM
libraries (also seen with `ForkJoinPool.commonPool()` worker threads, which
have a `null` TCCL by design since JDK 9). Any first captured console write
racing that window crashes the whole fork.
Confirmed this is a 3.6.0-only regression: the exact same reproduction
passes cleanly on 3.5.6 (`-Ddep.plugin.surefire.version=3.5.6`), with no code
changes — same log line fires first, no crash.
Minimal fix: catch `RuntimeException` (or at least `NullPointerException`)
alongside `NoClassDefFoundError`/`ClassNotFoundException` in
`ReflectionUtils.tryLoadClass`, so a null/broken classloader just disables the
fast `StackWalker`-based path (`AVAILABLE = false`) instead of poisoning the
class for the whole fork.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]