You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@groovy.apache.org by "Paul King (Jira)" <ji...@apache.org> on 2026/09/10 02:18:00 UTC
[jira] [Resolved] (GROOVY-12364) Native image: dynamic call to a @CallerSensitive method (e.g. Logger.getLogger) is fatal under AOT link mode
[ https://issues.apache.org/jira/browse/GROOVY-12364?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel ]
Paul King resolved GROOVY-12364.
--------------------------------
Fix Version/s: 6.0.0-RC-2
Resolution: Fixed
> Native image: dynamic call to a @CallerSensitive method (e.g. Logger.getLogger) is fatal under AOT link mode
> ------------------------------------------------------------------------------------------------------------
>
> Key: GROOVY-12364
> URL: https://issues.apache.org/jira/browse/GROOVY-12364
> Project: Groovy
> Issue Type: Bug
> Reporter: Paul King
> Assignee: Paul King
> Priority: Major
> Fix For: 6.0.0-RC-2
>
>
> In a GraalVM native image, a *dynamic* Groovy call to a JDK method annotated {{@CallerSensitive}} whose JDK implementation has a {{@CallerSensitiveAdapter}} variant (JDK 18+) kills the process with an uncatchable VM error. {{java.util.logging.Logger.getLogger(String)}} is the everyday case, so any dynamic Groovy class with a {{static final Logger LOG = Logger.getLogger(...)}} field cannot even initialise in a native image:
> {noformat}
> Fatal error: Cannot invoke method that has a @CallerSensitiveAdapter without an explicit caller
> {noformat}
> followed by a native crash dump. The Java frames in the dump are (innermost first):
> {noformat}
> com.oracle.svm.shared.util.VMError.shouldNotReachHere(VMError.java:91)
> com.oracle.svm.core.reflect.SubstrateMethodAccessor.methodHandleInvoke(SubstrateMethodAccessor.java:130)
> com.oracle.svm.core.methodhandles.Util_java_lang_invoke_MethodHandle.invokeInternal(Target_java_lang_invoke_MethodHandle.java:263)
> java.lang.invoke.MethodHandle.invokeBasic(MethodHandle.java:119)
> com.oracle.svm.core.methodhandles.MethodHandleIntrinsicImpl.execute(MethodHandleIntrinsicImpl.java:178)
> ... (LambdaForm.interpretWithArguments / invokeBasic repeated for each link of Groovy's guarded chain) ...
> java.lang.invoke.MethodHandleImpl.guardWithCatch(MethodHandleImpl.java:957)
> org.codehaus.groovy.vmplugin.v8.IndyInterface.aotDispatch(IndyInterface.java:604)
> Svc.<clinit>(NativeLogProbe.groovy:6) // static final Logger JUL = Logger.getLogger('svc')
> {noformat}
> h3. Reproducer
> Precompile and build with the agent-recorded metadata as in the Groovy 6 native-image instructions (GraalVM CE 25.2.4, Groovy 6.0.0-SNAPSHOT master):
> {code:groovy}
> class CsNative {
> static void main(String[] args) {
> switch (args[0]) {
> case 'logger': println java.util.logging.Logger.getLogger('x').name; break // fatal
> case 'lookup': println java.lang.invoke.MethodHandles.lookup().lookupClass(); break // fatal
> case 'forname': println Class.forName('java.lang.String'); break // ok
> case 'syslog': println System.getLogger('y').name; break // ok
> case 'ok': println Integer.parseInt('42'); break // ok
> }
> }
> }
> {code}
> Results, same binary:
> ||call||native image||JVM with {{-Dgroovy.indy.aot.link=true}}||
> |{{Logger.getLogger('x')}}|Fatal error|ok|
> |{{MethodHandles.lookup()}}|Fatal error|ok|
> |{{Class.forName(...)}}, {{System.getLogger(...)}}|ok|ok|
> |{{Logger.getMethod('getLogger', String).invoke(null, 'r')}} from {{@CompileStatic}} code|ok|ok|
> |{{MethodHandles.lookup().unreflect(getLogger).invokeWithArguments('m')}} from {{@CompileStatic}} code|ok|ok|
> So neither the method nor reflection nor method handles as such are the problem; only Groovy's *runtime-built* handle for the target is.
> h3. Cause
> {{Selector.MethodSelector.unreflect}} obtains the target handle with {{callSite.getLookup().unreflect(method)}}. For a caller-sensitive method the JDK returns a handle bound to the lookup class (the Groovy caller; {{MethodHandles.Lookup#unreflect}} / {{MethodHandleImpl.bindCaller}}), which on HotSpot gives exactly the right caller semantics. The AOT link mode (GROOVY-12234) then invokes the guarded chain from {{aotDispatch}}. GraalVM cannot intrinsify a handle that is only constructed at run time, so it executes the chain in its MethodHandle interpreter, and when the interpreter reaches the caller-sensitive leaf, {{SubstrateMethodAccessor.methodHandleInvoke}} requires the adapter form with an explicit caller argument, which the bound handle does not supply, and it aborts with {{VMError.shouldNotReachHere}} rather than throwing. The last two rows of the table show the same handle works when GraalVM can see its construction at build time, and that GraalVM's reflective path handles caller-sensitive methods fine.
> {{Class.forName}} and {{System.getLogger}} survive because GraalVM substitutes them directly.
> h3. Proposed fix
> Groovy already knows which targets are caller-sensitive: {{CachedMethod.isCallerSensitive()}} (annotation probe plus the serialization classes, conservative when undecidable), used today to keep such methods off the reflective cold tier on HotSpot, where {{Method.invoke}} would misreport the caller. Under AOT link mode the trade-off is the opposite: the handle path is fatal and the reflective path works. So for an AOT-linked site whose selected method is caller-sensitive, dispatch through {{Method.invoke}} (the reflective wrapper already used as the AOT steady state) instead of building the handle chain; static targets such as {{Logger.getLogger}} need to be admitted to that path, which currently accepts instance methods only. Interface selections should probe the receiver's implementation as the cold tier does.
> Consequence to document: in a native image the observed caller of such a method becomes Groovy's runtime rather than the calling class. For {{Logger.getLogger}}, {{System.getLogger}} and resource bundles that only affects module/loader resolution, which is a single application module in an image anyway; for {{MethodHandles.lookup()}} it means the returned lookup is not the caller's, so that call should be static-compiled or avoided in dynamic native code. Not crashing is still strictly better than the current fatal error.
> Longer term this is worth reporting to GraalVM: a caller-bound handle produced by {{Lookup.unreflect}} should be invocable from the MethodHandle interpreter (the caller is known at bind time), and a VM-fatal error for a user-triggerable condition is disproportionate; an {{UnsupportedOperationException}} would at least let Groovy fall back at run time.
> h3. Other links
> https://github.com/oracle/graal/issues/7486
--
This message was sent by Atlassian Jira
(v8.20.10#820010)