diff --git a/docs/changelog/133604.yaml b/docs/changelog/133604.yaml new file mode 100644 index 0000000000000..dd7be06544cce --- /dev/null +++ b/docs/changelog/133604.yaml @@ -0,0 +1,5 @@ +pr: 133604 +summary: Update `DefBootstrap` to handle Error from `ClassValue` +area: Infra/Scripting +type: bug +issues: [] diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/DefBootstrap.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/DefBootstrap.java index 8d0b79fe268a6..990dad06ae110 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/DefBootstrap.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/DefBootstrap.java @@ -201,7 +201,10 @@ protected MethodHandle computeValue(Class receiverType) { try { return lookup(flavor, name, receiverType).asType(type); } catch (Throwable t) { - Def.rethrow(t); + // ClassValue.getFromHashMap wraps checked exceptions as Error, so we + // use a sentinel class [PainlessWrappedException] here to work around + // this issue and later unwrap the original exception + Def.rethrow(t instanceof Exception ? new PainlessWrappedException((Exception) t) : t); throw new AssertionError(); } } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScript.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScript.java index d8e6fa62567e5..4806831f2f8c5 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScript.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScript.java @@ -46,6 +46,9 @@ public interface PainlessScript { * @return The generated ScriptException. */ default ScriptException convertToScriptException(Throwable t, Map> extraMetadata) { + if (t instanceof PainlessWrappedException) { + t = t.getCause(); + } // create a script stack: this is just the script portion List scriptStack = new ArrayList<>(); ScriptException.Position pos = null; diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessWrappedException.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessWrappedException.java new file mode 100644 index 0000000000000..68d95c286812f --- /dev/null +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessWrappedException.java @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.painless; + +/** + * Checked exceptions are wrapped in {@link ClassValue}#getFromHashMap in Error + * which leads to unexpected behavior in Painless. This class is used as a + * workaround for that exception wrapping. + */ +public class PainlessWrappedException extends Error { + + /** + * Constructor. + * @param cause The {@link Exception} cause. + */ + public PainlessWrappedException(final Exception cause) { + super(cause); + } +} diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/PainlessUserTreeToIRTreePhase.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/PainlessUserTreeToIRTreePhase.java index 72ecf1fe850ec..02e7774075e55 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/PainlessUserTreeToIRTreePhase.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/PainlessUserTreeToIRTreePhase.java @@ -12,6 +12,7 @@ import org.elasticsearch.painless.Location; import org.elasticsearch.painless.PainlessError; import org.elasticsearch.painless.PainlessExplainError; +import org.elasticsearch.painless.PainlessWrappedException; import org.elasticsearch.painless.ScriptClassInfo; import org.elasticsearch.painless.ScriptClassInfo.MethodArgument; import org.elasticsearch.painless.ir.BinaryImplNode; @@ -415,6 +416,7 @@ protected static void injectSandboxExceptions(FunctionNode irFunctionNode) { for (Class throwable : List.of( PainlessError.class, + PainlessWrappedException.class, LinkageError.class, OutOfMemoryError.class, StackOverflowError.class, diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/DefBootstrapTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/DefBootstrapTests.java index b22329b8d6bfb..dca2350482616 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/DefBootstrapTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/DefBootstrapTests.java @@ -139,9 +139,11 @@ public void testMegamorphic() throws Throwable { map.put("a", "b"); assertEquals(2, (int) handle.invokeExact((Object) map)); - final IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> { + final PainlessWrappedException pwe = expectThrows(PainlessWrappedException.class, () -> { Integer.toString((int) handle.invokeExact(new Object())); }); + assertTrue(pwe.getCause() instanceof IllegalArgumentException); + IllegalArgumentException iae = (IllegalArgumentException) pwe.getCause(); assertEquals("dynamic method [java.lang.Object, size/0] not found", iae.getMessage()); assertTrue("Does not fail inside ClassValue.computeValue()", Arrays.stream(iae.getStackTrace()).anyMatch(e -> { return e.getMethodName().equals("computeValue") && e.getClassName().startsWith("org.elasticsearch.painless.DefBootstrap$PIC$");