[
https://issues.apache.org/jira/browse/GROOVY-12364?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Paul King updated GROOVY-12364:
-------------------------------
Description:
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
was:
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.
> 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
>
> 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)