Skip to content

Commit fc2c3da

Browse files
committed
[Scripting] Make Max Script Length Setting Dynamic (#35184)
This changes the current script.max_size_in_bytes to be dynamic so it can be set through the cluster settings API. This setting is also applied to inline scripts in the compile method of ScriptService to prevent excessively long inline scripts from being compiled. The script length limit is removed from Painless as this is no longer necessary with the protection in compile.
1 parent d722535 commit fc2c3da

File tree

6 files changed

+84
-41
lines changed

6 files changed

+84
-41
lines changed

docs/reference/modules/scripting/using.asciidoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ you can change this behavior by using the `script.cache.expire` setting.
204204
You can configure the size of this cache by using the `script.cache.max_size` setting.
205205
By default, the cache size is `100`.
206206

207-
NOTE: The size of stored scripts is limited to 65,535 bytes. This can be
207+
NOTE: The size of scripts is limited to 65,535 bytes. This can be
208208
changed by setting `script.max_size_in_bytes` setting to increase that soft
209209
limit, but if scripts are really large then a
210210
<<modules-scripting-engine,native script engine>> should be considered.

modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,6 @@
4848
*/
4949
final class Compiler {
5050

51-
/**
52-
* The maximum number of characters allowed in the script source.
53-
*/
54-
static final int MAXIMUM_SOURCE_LENGTH = 16384;
55-
5651
/**
5752
* Define the class with lowest privileges.
5853
*/
@@ -212,12 +207,6 @@ private static void addFactoryMethod(Map<String, Class<?>> additionalClasses, Cl
212207
* @return An executable script that implements both a specified interface and is a subclass of {@link PainlessScript}
213208
*/
214209
Constructor<?> compile(Loader loader, MainMethodReserved reserved, String name, String source, CompilerSettings settings) {
215-
if (source.length() > MAXIMUM_SOURCE_LENGTH) {
216-
throw new IllegalArgumentException("Scripts may be no longer than " + MAXIMUM_SOURCE_LENGTH +
217-
" characters. The passed in script is " + source.length() + " characters. Consider using a" +
218-
" plugin if a script longer than this length is a requirement.");
219-
}
220-
221210
ScriptClassInfo scriptClassInfo = new ScriptClassInfo(painlessLookup, scriptClass);
222211
SSource root = Walker.buildPainlessTree(scriptClassInfo, reserved, name, source, settings, painlessLookup,
223212
null);
@@ -248,12 +237,6 @@ Constructor<?> compile(Loader loader, MainMethodReserved reserved, String name,
248237
* @return The bytes for compilation.
249238
*/
250239
byte[] compile(String name, String source, CompilerSettings settings, Printer debugStream) {
251-
if (source.length() > MAXIMUM_SOURCE_LENGTH) {
252-
throw new IllegalArgumentException("Scripts may be no longer than " + MAXIMUM_SOURCE_LENGTH +
253-
" characters. The passed in script is " + source.length() + " characters. Consider using a" +
254-
" plugin if a script longer than this length is a requirement.");
255-
}
256-
257240
ScriptClassInfo scriptClassInfo = new ScriptClassInfo(painlessLookup, scriptClass);
258241
SSource root = Walker.buildPainlessTree(scriptClassInfo, new MainMethodReserved(), name, source, settings, painlessLookup,
259242
debugStream);

modules/lang-painless/src/test/java/org/elasticsearch/painless/WhenThingsGoWrongTests.java

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import org.elasticsearch.script.ScriptException;
2525

2626
import java.lang.invoke.WrongMethodTypeException;
27-
import java.util.Arrays;
2827
import java.util.Collections;
2928

3029
import static java.util.Collections.emptyMap;
@@ -200,21 +199,6 @@ public void testLoopLimits() {
200199
"The maximum number of statements that can be executed in a loop has been reached."));
201200
}
202201

203-
public void testSourceLimits() {
204-
final char[] tooManyChars = new char[Compiler.MAXIMUM_SOURCE_LENGTH + 1];
205-
Arrays.fill(tooManyChars, '0');
206-
207-
IllegalArgumentException expected = expectScriptThrows(IllegalArgumentException.class, false, () -> {
208-
exec(new String(tooManyChars));
209-
});
210-
assertTrue(expected.getMessage().contains("Scripts may be no longer than"));
211-
212-
final char[] exactlyAtLimit = new char[Compiler.MAXIMUM_SOURCE_LENGTH];
213-
Arrays.fill(exactlyAtLimit, '0');
214-
// ok
215-
assertEquals(0, exec(new String(exactlyAtLimit)));
216-
}
217-
218202
public void testIllegalDynamicMethod() {
219203
IllegalArgumentException expected = expectScriptThrows(IllegalArgumentException.class, () -> {
220204
exec("def x = 'test'; return x.getClass().toString()");

server/src/main/java/org/elasticsearch/script/ScriptMetaData.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public static final class Builder {
6868
* is no existing {@link ScriptMetaData}.
6969
*/
7070
public Builder(ScriptMetaData previous) {
71-
this.scripts = previous == null ? new HashMap<>() :new HashMap<>(previous.scripts);
71+
this.scripts = previous == null ? new HashMap<>() : new HashMap<>(previous.scripts);
7272
}
7373

7474
/**
@@ -393,6 +393,13 @@ public EnumSet<MetaData.XContentContext> context() {
393393
return MetaData.ALL_CONTEXTS;
394394
}
395395

396+
/**
397+
* Returns the map of stored scripts.
398+
*/
399+
Map<String, StoredScriptSource> getStoredScripts() {
400+
return scripts;
401+
}
402+
396403
/**
397404
* Retrieves a stored script based on a user-specified id.
398405
*/

server/src/main/java/org/elasticsearch/script/ScriptService.java

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848

4949
import java.io.Closeable;
5050
import java.io.IOException;
51+
import java.nio.charset.StandardCharsets;
5152
import java.util.Collections;
5253
import java.util.HashSet;
5354
import java.util.List;
@@ -97,8 +98,7 @@ public class ScriptService extends AbstractComponent implements Closeable, Clust
9798
public static final Setting<TimeValue> SCRIPT_CACHE_EXPIRE_SETTING =
9899
Setting.positiveTimeSetting("script.cache.expire", TimeValue.timeValueMillis(0), Property.NodeScope);
99100
public static final Setting<Integer> SCRIPT_MAX_SIZE_IN_BYTES =
100-
Setting.intSetting("script.max_size_in_bytes", 65535, Property.NodeScope);
101-
// public Setting(String key, Function<Settings, String> defaultValue, Function<String, T> parser, Property... properties) {
101+
Setting.intSetting("script.max_size_in_bytes", 65535, 0, Property.Dynamic, Property.NodeScope);
102102
public static final Setting<Tuple<Integer, TimeValue>> SCRIPT_MAX_COMPILATIONS_RATE =
103103
new Setting<>("script.max_compilations_rate", "75/5m", MAX_COMPILATION_RATE_FUNCTION, Property.Dynamic, Property.NodeScope);
104104

@@ -122,6 +122,8 @@ public class ScriptService extends AbstractComponent implements Closeable, Clust
122122

123123
private ClusterState clusterState;
124124

125+
private int maxSizeInBytes;
126+
125127
private Tuple<Integer, TimeValue> rate;
126128
private long lastInlineCompileTime;
127129
private double scriptsPerTimeWindow;
@@ -220,10 +222,12 @@ public ScriptService(Settings settings, Map<String, ScriptEngine> engines, Map<S
220222
this.cache = cacheBuilder.removalListener(new ScriptCacheRemovalListener()).build();
221223

222224
this.lastInlineCompileTime = System.nanoTime();
225+
this.setMaxSizeInBytes(SCRIPT_MAX_SIZE_IN_BYTES.get(settings));
223226
this.setMaxCompilationRate(SCRIPT_MAX_COMPILATIONS_RATE.get(settings));
224227
}
225228

226229
void registerClusterSettingsListeners(ClusterSettings clusterSettings) {
230+
clusterSettings.addSettingsUpdateConsumer(SCRIPT_MAX_SIZE_IN_BYTES, this::setMaxSizeInBytes);
227231
clusterSettings.addSettingsUpdateConsumer(SCRIPT_MAX_COMPILATIONS_RATE, this::setMaxCompilationRate);
228232
}
229233

@@ -240,6 +244,22 @@ private ScriptEngine getEngine(String lang) {
240244
return scriptEngine;
241245
}
242246

247+
/**
248+
* Changes the maximum number of bytes a script's source is allowed to have.
249+
* @param newMaxSizeInBytes The new maximum number of bytes.
250+
*/
251+
void setMaxSizeInBytes(int newMaxSizeInBytes) {
252+
for (Map.Entry<String, StoredScriptSource> source : getScriptsFromClusterState().entrySet()) {
253+
if (source.getValue().getSource().getBytes(StandardCharsets.UTF_8).length > newMaxSizeInBytes) {
254+
throw new IllegalArgumentException("script.max_size_in_bytes cannot be set to [" + newMaxSizeInBytes + "], " +
255+
"stored script [" + source.getKey() + "] exceeds the new value with a size of " +
256+
"[" + source.getValue().getSource().getBytes(StandardCharsets.UTF_8).length + "]");
257+
}
258+
}
259+
260+
maxSizeInBytes = newMaxSizeInBytes;
261+
}
262+
243263
/**
244264
* This configures the maximum script compilations per five minute window.
245265
*
@@ -303,6 +323,13 @@ public <FactoryType> FactoryType compile(Script script, ScriptContext<FactoryTyp
303323
throw new IllegalArgumentException("cannot execute scripts using [" + context.name + "] context");
304324
}
305325

326+
if (type == ScriptType.INLINE) {
327+
if (idOrCode.getBytes(StandardCharsets.UTF_8).length > maxSizeInBytes) {
328+
throw new IllegalArgumentException("exceeded max allowed inline script size in bytes [" + maxSizeInBytes + "] " +
329+
"with size [" + idOrCode.getBytes(StandardCharsets.UTF_8).length + "] for script [" + idOrCode + "]");
330+
}
331+
}
332+
306333
if (logger.isTraceEnabled()) {
307334
logger.trace("compiling lang: [{}] type: [{}] script: {}", lang, type, idOrCode);
308335
}
@@ -397,6 +424,20 @@ public boolean isAnyContextEnabled() {
397424
return contextsAllowed == null || contextsAllowed.isEmpty() == false;
398425
}
399426

427+
Map<String, StoredScriptSource> getScriptsFromClusterState() {
428+
if (clusterState == null) {
429+
return Collections.emptyMap();
430+
}
431+
432+
ScriptMetaData scriptMetadata = clusterState.metaData().custom(ScriptMetaData.TYPE);
433+
434+
if (scriptMetadata == null) {
435+
return Collections.emptyMap();
436+
}
437+
438+
return scriptMetadata.getStoredScripts();
439+
}
440+
400441
StoredScriptSource getScriptFromClusterState(String id) {
401442
ScriptMetaData scriptMetadata = clusterState.metaData().custom(ScriptMetaData.TYPE);
402443

@@ -415,10 +456,8 @@ StoredScriptSource getScriptFromClusterState(String id) {
415456

416457
public void putStoredScript(ClusterService clusterService, PutStoredScriptRequest request,
417458
ActionListener<AcknowledgedResponse> listener) {
418-
int max = SCRIPT_MAX_SIZE_IN_BYTES.get(settings);
419-
420-
if (request.content().length() > max) {
421-
throw new IllegalArgumentException("exceeded max allowed stored script size in bytes [" + max + "] with size [" +
459+
if (request.content().length() > maxSizeInBytes) {
460+
throw new IllegalArgumentException("exceeded max allowed stored script size in bytes [" + maxSizeInBytes + "] with size [" +
422461
request.content().length() + "] for script [" + request.id() + "]");
423462
}
424463

server/src/test/java/org/elasticsearch/script/ScriptServiceTests.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.elasticsearch.common.bytes.BytesArray;
2828
import org.elasticsearch.common.bytes.BytesReference;
2929
import org.elasticsearch.common.collect.Tuple;
30+
import org.elasticsearch.common.settings.ClusterSettings;
3031
import org.elasticsearch.common.settings.Settings;
3132
import org.elasticsearch.common.unit.TimeValue;
3233
import org.elasticsearch.common.xcontent.XContentFactory;
@@ -54,6 +55,7 @@ public class ScriptServiceTests extends ESTestCase {
5455
private Map<String, ScriptContext<?>> contexts;
5556
private ScriptService scriptService;
5657
private Settings baseSettings;
58+
private ClusterSettings clusterSettings;
5759

5860
@Before
5961
public void setup() throws IOException {
@@ -78,12 +80,22 @@ public void setup() throws IOException {
7880
private void buildScriptService(Settings additionalSettings) throws IOException {
7981
Settings finalSettings = Settings.builder().put(baseSettings).put(additionalSettings).build();
8082
scriptService = new ScriptService(finalSettings, engines, contexts) {
83+
@Override
84+
Map<String, StoredScriptSource> getScriptsFromClusterState() {
85+
Map<String, StoredScriptSource> scripts = new HashMap<>();
86+
scripts.put("test1", new StoredScriptSource("test", "1+1", Collections.emptyMap()));
87+
scripts.put("test2", new StoredScriptSource("test", "1", Collections.emptyMap()));
88+
return scripts;
89+
}
90+
8191
@Override
8292
StoredScriptSource getScriptFromClusterState(String id) {
8393
//mock the script that gets retrieved from an index
8494
return new StoredScriptSource("test", "1+1", Collections.emptyMap());
8595
}
8696
};
97+
clusterSettings = new ClusterSettings(finalSettings, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS);
98+
scriptService.registerClusterSettingsListeners(clusterSettings);
8799
}
88100

89101
// even though circuit breaking is allowed to be configured per minute, we actually weigh this over five minutes
@@ -305,6 +317,24 @@ public void testGetStoredScript() throws Exception {
305317
assertNull(scriptService.getStoredScript(cs, new GetStoredScriptRequest("_id")));
306318
}
307319

320+
public void testMaxSizeLimit() throws Exception {
321+
buildScriptService(Settings.builder().put(ScriptService.SCRIPT_MAX_SIZE_IN_BYTES.getKey(), 4).build());
322+
scriptService.compile(new Script(ScriptType.INLINE, "test", "1+1", Collections.emptyMap()), randomFrom(contexts.values()));
323+
IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> {
324+
scriptService.compile(new Script(ScriptType.INLINE, "test", "10+10", Collections.emptyMap()), randomFrom(contexts.values()));
325+
});
326+
assertEquals("exceeded max allowed inline script size in bytes [4] with size [5] for script [10+10]", iae.getMessage());
327+
clusterSettings.applySettings(Settings.builder().put(ScriptService.SCRIPT_MAX_SIZE_IN_BYTES.getKey(), 6).build());
328+
scriptService.compile(new Script(ScriptType.INLINE, "test", "10+10", Collections.emptyMap()), randomFrom(contexts.values()));
329+
clusterSettings.applySettings(Settings.builder().put(ScriptService.SCRIPT_MAX_SIZE_IN_BYTES.getKey(), 5).build());
330+
scriptService.compile(new Script(ScriptType.INLINE, "test", "10+10", Collections.emptyMap()), randomFrom(contexts.values()));
331+
iae = expectThrows(IllegalArgumentException.class, () -> {
332+
clusterSettings.applySettings(Settings.builder().put(ScriptService.SCRIPT_MAX_SIZE_IN_BYTES.getKey(), 2).build());
333+
});
334+
assertEquals("script.max_size_in_bytes cannot be set to [2], stored script [test1] exceeds the new value with a size of [3]",
335+
iae.getMessage());
336+
}
337+
308338
private void assertCompileRejected(String lang, String script, ScriptType scriptType, ScriptContext scriptContext) {
309339
try {
310340
scriptService.compile(new Script(scriptType, lang, script, Collections.emptyMap()), scriptContext);

0 commit comments

Comments
 (0)