Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scripting: enable regular expressions by default #63029

Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.elasticsearch.painless.spi.annotation;

import java.util.Collections;
import java.util.List;

public class InjectConstantAnnotation {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have some basic java docs on this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

public static final String NAME = "inject_constant";
public final List<String> injects;
public InjectConstantAnnotation(List<String> injects) {
this.injects = Collections.unmodifiableList(injects);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.elasticsearch.painless.spi.annotation;

import java.util.ArrayList;
import java.util.Map;

public class InjectConstantAnnotationParser implements WhitelistAnnotationParser {

public static final InjectConstantAnnotationParser INSTANCE = new InjectConstantAnnotationParser();

private InjectConstantAnnotationParser() {}

@Override
public Object parse(Map<String, String> arguments) {
if (arguments.isEmpty()) {
throw new IllegalArgumentException("[@inject_constant] requires at least one name to inject");
}
ArrayList<String> argList = new ArrayList<>(arguments.size());
for (int i = 1; i <= arguments.size(); i++) {
String argNum = Integer.toString(i);
if (arguments.containsKey(argNum) == false) {
throw new IllegalArgumentException("[@inject_constant] missing argument number [" + argNum + "]");
}
// TODO(stu): Jack, how do I verify against CompilerSettings.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Todos like this are very cryptic. Can we just have a normal comment if some explanation is needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed when draft.

// answer: do validation in PainlessLookupBuilder
argList.add(arguments.get(argNum));
}

return new InjectConstantAnnotation(argList);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ public interface WhitelistAnnotationParser {
Stream.of(
new AbstractMap.SimpleEntry<>(NoImportAnnotation.NAME, NoImportAnnotationParser.INSTANCE),
new AbstractMap.SimpleEntry<>(DeprecatedAnnotation.NAME, DeprecatedAnnotationParser.INSTANCE),
new AbstractMap.SimpleEntry<>(NonDeterministicAnnotation.NAME, NonDeterministicAnnotationParser.INSTANCE)
new AbstractMap.SimpleEntry<>(NonDeterministicAnnotation.NAME, NonDeterministicAnnotationParser.INSTANCE),
new AbstractMap.SimpleEntry<>(InjectConstantAnnotation.NAME, InjectConstantAnnotationParser.INSTANCE)
).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ ScriptScope compile(Loader loader, String name, String source, CompilerSettings
ScriptScope scriptScope = new ScriptScope(painlessLookup, settings, scriptClassInfo, scriptName, source, root.getIdentifier() + 1);
new PainlessSemanticHeaderPhase().visitClass(root, scriptScope);
new PainlessSemanticAnalysisPhase().visitClass(root, scriptScope);
// TODO(stu): Make this phase optional #60156
// TODO: Make this phase optional #60156
new DocFieldsPhase().visitClass(root, scriptScope);
new PainlessUserTreeToIRTreePhase().visitClass(root, scriptScope);
ClassNode classNode = (ClassNode)scriptScope.getDecoration(root, IRNodeDecoration.class).getIRNode();
Expand Down Expand Up @@ -255,7 +255,7 @@ byte[] compile(String name, String source, CompilerSettings settings, Printer de
ScriptScope scriptScope = new ScriptScope(painlessLookup, settings, scriptClassInfo, scriptName, source, root.getIdentifier() + 1);
new PainlessSemanticHeaderPhase().visitClass(root, scriptScope);
new PainlessSemanticAnalysisPhase().visitClass(root, scriptScope);
// TODO(stu): Make this phase optional #60156
// TODO: Make this phase optional #60156
new DocFieldsPhase().visitClass(root, scriptScope);
new PainlessUserTreeToIRTreePhase().visitClass(root, scriptScope);
ClassNode classNode = (ClassNode)scriptScope.getDecoration(root, IRNodeDecoration.class).getIRNode();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,26 @@

import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Setting.Property;
import org.elasticsearch.painless.api.Augmentation;

import java.util.HashMap;
import java.util.Map;

/**
* Settings to use when compiling a script.
*/
public final class CompilerSettings {
/**
* Are regexes enabled? This is a node level setting because regexes break out of painless's lovely sandbox and can cause stack
* overflows and we can't analyze the regex to be sure it won't.
* Are regexes enabled? If
*/
public static final Setting<Boolean> REGEX_ENABLED = Setting.boolSetting("script.painless.regex.enabled", false, Property.NodeScope);
public static final Setting<RegexEnabled> REGEX_ENABLED =
new Setting<>("script.painless.regex.enabled", RegexEnabled.USE_FACTOR.value, RegexEnabled::parse, Property.NodeScope);

/**
* How complex can a regex be? This is the number of characters that can be considered expressed as a multiple of string length.
*/
public static final Setting<Integer> REGEX_LIMIT_FACTOR =
Setting.intSetting("script.painless.regex.limit-factor", 6, 1, Property.NodeScope);

/**
* Constant to be used when specifying the maximum loop counter when compiling a script.
Expand Down Expand Up @@ -65,12 +75,20 @@ public final class CompilerSettings {
* For testing. Do not use.
*/
private int initialCallSiteDepth = 0;
private int testInject0 = 2;
private int testInject1 = 4;
private int testInject2 = 6;

/**
* Are regexes enabled? They are currently disabled by default because they break out of the loop counter and even fairly simple
* <strong>looking</strong> regexes can cause stack overflows.
* Are regexes enabled? Defaults to using the factor setting.
*/
private RegexEnabled regexesEnabled = RegexEnabled.USE_FACTOR;


/**
* How complex can regexes be? Expressed as a multiple of the input string.
*/
private boolean regexesEnabled = false;
private int regexLimitFactor = 0;

/**
* Returns the value for the cumulative total number of statements that can be made in all loops
Expand Down Expand Up @@ -123,18 +141,67 @@ public void setInitialCallSiteDepth(int depth) {
}

/**
* Are regexes enabled? They are currently disabled by default because they break out of the loop counter and even fairly simple
* <strong>looking</strong> regexes can cause stack overflows.
* Are regexes enabled?
*/
public boolean areRegexesEnabled() {
public RegexEnabled areRegexesEnabled() {
return regexesEnabled;
}

/**
* Are regexes enabled? They are currently disabled by default because they break out of the loop counter and even fairly simple
* <strong>looking</strong> regexes can cause stack overflows.
*/
public void setRegexesEnabled(boolean regexesEnabled) {
public void setRegexesEnabled(RegexEnabled regexesEnabled) {
this.regexesEnabled = regexesEnabled;
}

public void setRegexLimitFactor(int regexLimitFactor) {
this.regexLimitFactor = regexLimitFactor;
}

public int getRegexLimitFactor() {
return regexLimitFactor;
}

public Map<String, Object> asMap() {
int regexLimitFactor = this.regexLimitFactor;
if (regexesEnabled == RegexEnabled.TRUE) {
regexLimitFactor = Augmentation.UNLIMITED_PATTERN_FACTOR;
} else if (regexesEnabled == RegexEnabled.FALSE) {
regexLimitFactor = Augmentation.DISABLED_PATTERN_FACTOR;
}
Map<String, Object> map = new HashMap<>();
map.put("regex_limit_factor", regexLimitFactor);

// for testing only
map.put("testInject0", testInject0);
map.put("testInject1", testInject1);
map.put("testInject2", testInject2);

return map;
}

public enum RegexEnabled {
TRUE("true"),
FALSE("false"),
USE_FACTOR("use-factor");
final String value;

RegexEnabled(String value) {
this.value = value;
}

public static RegexEnabled parse(String value) {
if (TRUE.value.equals(value)) {
return TRUE;
} else if (FALSE.value.equals(value)) {
return FALSE;
} else if (USE_FACTOR.value.equals(value)) {
return USE_FACTOR;
}
throw new IllegalArgumentException(
"invalid value [" + value + "] must be one of [" + TRUE.value + "," + FALSE.value + "," + USE_FACTOR.value + "]"
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,8 @@ static MethodHandle arrayLengthGetter(Class<?> arrayType) {
* @throws IllegalArgumentException if no matching whitelisted method was found.
* @throws Throwable if a method reference cannot be converted to an functional interface
*/
static MethodHandle lookupMethod(PainlessLookup painlessLookup, FunctionTable functions,
MethodHandles.Lookup methodHandlesLookup, MethodType callSiteType, Class<?> receiverClass, String name, Object args[])
static MethodHandle lookupMethod(PainlessLookup painlessLookup, FunctionTable functions, Map<String, Object> constants,
MethodHandles.Lookup methodHandlesLookup, MethodType callSiteType, Class<?> receiverClass, String name, Object[] args)
throws Throwable {

String recipeString = (String) args[0];
Expand All @@ -206,7 +206,15 @@ static MethodHandle lookupMethod(PainlessLookup painlessLookup, FunctionTable fu
"[" + typeToCanonicalTypeName(receiverClass) + ", " + name + "/" + (numArguments - 1) + "] not found");
}

return painlessMethod.methodHandle;
MethodHandle handle = painlessMethod.methodHandle;
Object[] injections = PainlessLookupUtility.buildInjections(painlessMethod, constants);

if (injections.length > 0) {
// method handle contains the "this" pointer so start injections at 1
handle = MethodHandles.insertArguments(handle, 1, injections);
}

return handle;
}

// convert recipe string to a bitset for convenience (the code below should be refactored...)
Expand Down Expand Up @@ -236,7 +244,13 @@ static MethodHandle lookupMethod(PainlessLookup painlessLookup, FunctionTable fu
"dynamic method [" + typeToCanonicalTypeName(receiverClass) + ", " + name + "/" + arity + "] not found");
}

MethodHandle handle = method.methodHandle;
MethodHandle handle = method.methodHandle;
Object[] injections = PainlessLookupUtility.buildInjections(method, constants);

if (injections.length > 0) {
// method handle contains the "this" pointer so start injections at 1
handle = MethodHandles.insertArguments(handle, 1, injections);
}

int replaced = 0;
upTo = 1;
Expand All @@ -257,22 +271,25 @@ static MethodHandle lookupMethod(PainlessLookup painlessLookup, FunctionTable fu
// we have everything.
filter = lookupReferenceInternal(painlessLookup,
functions,
constants,
methodHandlesLookup,
interfaceType,
type,
call,
numCaptures);
numCaptures
);
} else if (signature.charAt(0) == 'D') {
// the interface type is now known, but we need to get the implementation.
// this is dynamically based on the receiver type (and cached separately, underneath
// this cache). It won't blow up since we never nest here (just references)
Class<?> captures[] = new Class<?>[numCaptures];
Class<?>[] captures = new Class<?>[numCaptures];
for (int capture = 0; capture < captures.length; capture++) {
captures[capture] = callSiteType.parameterType(i + 1 + capture);
}
MethodType nestedType = MethodType.methodType(interfaceType, captures);
CallSite nested = DefBootstrap.bootstrap(painlessLookup,
functions,
constants,
methodHandlesLookup,
call,
nestedType,
Expand Down Expand Up @@ -300,8 +317,10 @@ static MethodHandle lookupMethod(PainlessLookup painlessLookup, FunctionTable fu
* This is just like LambdaMetaFactory, only with a dynamic type. The interface type is known,
* so we simply need to lookup the matching implementation method based on receiver type.
*/
static MethodHandle lookupReference(PainlessLookup painlessLookup, FunctionTable functions,
MethodHandles.Lookup methodHandlesLookup, String interfaceClass, Class<?> receiverClass, String name) throws Throwable {
static MethodHandle lookupReference(PainlessLookup painlessLookup, FunctionTable functions, Map<String, Object> constants,
MethodHandles.Lookup methodHandlesLookup, String interfaceClass, Class<?> receiverClass, String name)
throws Throwable {

Class<?> interfaceType = painlessLookup.canonicalTypeNameToType(interfaceClass);
if (interfaceType == null) {
throw new IllegalArgumentException("type [" + interfaceClass + "] not found");
Expand All @@ -317,25 +336,30 @@ static MethodHandle lookupReference(PainlessLookup painlessLookup, FunctionTable
"dynamic method [" + typeToCanonicalTypeName(receiverClass) + ", " + name + "/" + arity + "] not found");
}

return lookupReferenceInternal(painlessLookup, functions, methodHandlesLookup,
interfaceType, PainlessLookupUtility.typeToCanonicalTypeName(implMethod.targetClass),
implMethod.javaMethod.getName(), 1);
return lookupReferenceInternal(painlessLookup, functions, constants,
methodHandlesLookup, interfaceType, PainlessLookupUtility.typeToCanonicalTypeName(implMethod.targetClass),
implMethod.javaMethod.getName(), 1);
}

/** Returns a method handle to an implementation of clazz, given method reference signature. */
private static MethodHandle lookupReferenceInternal(PainlessLookup painlessLookup, FunctionTable functions,
MethodHandles.Lookup methodHandlesLookup, Class<?> clazz, String type, String call, int captures) throws Throwable {
final FunctionRef ref = FunctionRef.create(painlessLookup, functions, null, clazz, type, call, captures);
private static MethodHandle lookupReferenceInternal(
PainlessLookup painlessLookup, FunctionTable functions, Map<String, Object> constants,
MethodHandles.Lookup methodHandlesLookup, Class<?> clazz, String type, String call, int captures
) throws Throwable {

final FunctionRef ref = FunctionRef.create(painlessLookup, functions, null, clazz, type, call, captures, constants);
final CallSite callSite = LambdaBootstrap.lambdaBootstrap(
methodHandlesLookup,
ref.interfaceMethodName,
ref.factoryMethodType,
ref.interfaceMethodType,
ref.delegateClassName,
ref.delegateInvokeType,
ref.delegateMethodName,
ref.delegateMethodType,
ref.isDelegateInterface ? 1 : 0
methodHandlesLookup,
ref.interfaceMethodName,
ref.factoryMethodType,
ref.interfaceMethodType,
ref.delegateClassName,
ref.delegateInvokeType,
ref.delegateMethodName,
ref.delegateMethodType,
ref.isDelegateInterface ? 1 : 0,
ref.isDelegateAugmented ? 1 : 0,
ref.delegateInjections
);
return callSite.dynamicInvoker().asType(MethodType.methodType(clazz, ref.factoryMethodType.parameterArray()));
}
Expand Down
Loading