Add limits to parser recursion (#1918)
This commit is contained in:
1 parent
3e703995ca
commit
434f699887
12 files changed
+435
-99
No files matched your search
@@ -299,6 +299,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
|
||||
private final ExternalMemberRegistry externalMemberRegistry;
|
||||
private final SymbolTable symbolTable;
|
||||
private final boolean isMethodReturnTypeChecked;
|
||||
private @Nullable Node lastVisited;
|
||||
|
||||
public AstBuilder(
|
||||
Source source, VmLanguage language, ModuleInfo moduleInfo, ModuleResolver moduleResolver) {
|
||||
@@ -2327,13 +2328,20 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
|
||||
}
|
||||
|
||||
public UnresolvedTypeNode visitType(Type type) {
|
||||
lastVisited = type;
|
||||
return (UnresolvedTypeNode) type.accept(this);
|
||||
}
|
||||
|
||||
public ExpressionNode visitExpr(Expr expr) {
|
||||
lastVisited = expr;
|
||||
return (ExpressionNode) expr.accept(this);
|
||||
}
|
||||
|
||||
/** Returns the source section of the last type or expression that was entered. */
|
||||
public @Nullable SourceSection getLastVisitedSection() {
|
||||
return lastVisited == null ? null : createSourceSection(lastVisited);
|
||||
}
|
||||
|
||||
public TypeParameter[] visitTypeParameterList(
|
||||
@Nullable TypeParameterList ctx, FrameSlotVariable @Nullable [] typeBindings) {
|
||||
if (ctx == null) return new TypeParameter[0];
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.oracle.truffle.api.instrumentation.ProvidedTags;
|
||||
import com.oracle.truffle.api.nodes.Node;
|
||||
import com.oracle.truffle.api.source.Source;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.pkl.core.ast.PklRootNode;
|
||||
import org.pkl.core.ast.builder.AstBuilder;
|
||||
import org.pkl.core.module.ModuleKey;
|
||||
import org.pkl.core.module.ResolvedModuleKey;
|
||||
@@ -107,10 +108,21 @@ public final class VmLanguage extends TruffleLanguage<VmContext> {
|
||||
throw VmUtils.toVmException(e, source, moduleName);
|
||||
}
|
||||
|
||||
PklRootNode moduleNode;
|
||||
var builder =
|
||||
AstBuilder.create(
|
||||
source, this, moduleContext, moduleKey, resolvedModuleKey, moduleResolver);
|
||||
var moduleNode = builder.visitModule(moduleContext);
|
||||
try {
|
||||
moduleNode = builder.visitModule(moduleContext);
|
||||
} catch (StackOverflowError e) {
|
||||
// the syntax tree is too deep for the AST builder
|
||||
var section = builder.getLastVisitedSection();
|
||||
throw new VmExceptionBuilder()
|
||||
.evalError("moduleTooDeeplyNested")
|
||||
.withSourceSection(section != null ? section : source.createSection(1))
|
||||
.withMemberName(IoUtils.inferModuleName(moduleKey))
|
||||
.build();
|
||||
}
|
||||
moduleNode.getCallTarget().call(emptyModule, emptyModule);
|
||||
MinPklVersionChecker.check(emptyModule, importNode);
|
||||
}
|
||||
|
||||
@@ -846,6 +846,9 @@ Microbenchmark is a constant expression.
|
||||
stackOverflow=\
|
||||
A stack overflow occurred.
|
||||
|
||||
moduleTooDeeplyNested=\
|
||||
Code is nested too deeply, or contains too many chained operations.
|
||||
|
||||
cannotHaveCircularProjectDependenciesSingle=\
|
||||
Local project dependencies cannot be circular.\n\
|
||||
\n\
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed 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
|
||||
*
|
||||
* https://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.pkl.core.parser
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.DynamicTest
|
||||
import org.junit.jupiter.api.DynamicTest.dynamicTest
|
||||
import org.junit.jupiter.api.TestFactory
|
||||
import org.pkl.core.Evaluator
|
||||
import org.pkl.core.ModuleSource
|
||||
import org.pkl.core.PklException
|
||||
|
||||
/** Maliciously deep modules must fail with a [PklException] rather than a [StackOverflowError]. */
|
||||
class EvaluatorDepthTest {
|
||||
companion object {
|
||||
private const val STACK_SIZE = 512L * 1024
|
||||
private const val HUGE = 100_000
|
||||
|
||||
private val nesting: Map<String, (Int) -> String> =
|
||||
mapOf(
|
||||
"parens" to { n -> "x = " + "(".repeat(n) + "1" + ")".repeat(n) },
|
||||
"if" to { n -> "x = " + "if (true) 1 else ".repeat(n) + "2" },
|
||||
"objects" to { n -> "x " + "{ y ".repeat(n) + "{}" + " }".repeat(n) },
|
||||
)
|
||||
|
||||
private val chains: Map<String, (Int) -> String> =
|
||||
mapOf(
|
||||
"binary operator" to { n -> "x = " + "1 + ".repeat(n) + "1" },
|
||||
"qualified access" to { n -> "x = new Listing {}" + ".toList()".repeat(n) },
|
||||
"subscript" to { n -> "x = a" + "[1]".repeat(n) },
|
||||
"non-null" to { n -> "x = a" + "!!".repeat(n) },
|
||||
"constrained type" to { n -> "x: Int" + "(isPositive)".repeat(n) + " = 1" },
|
||||
)
|
||||
|
||||
private fun runOnSmallStack(action: () -> Unit): Throwable? {
|
||||
var thrown: Throwable? = null
|
||||
val thread =
|
||||
Thread(null, { thrown = runCatching(action).exceptionOrNull() }, "test", STACK_SIZE)
|
||||
thread.start()
|
||||
thread.join()
|
||||
return thrown
|
||||
}
|
||||
|
||||
private fun evaluate(source: String) {
|
||||
Evaluator.preconfigured().use { it.evaluate(ModuleSource.text(source)) }
|
||||
}
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
fun `deep nesting fails with a PklException`(): List<DynamicTest> = nesting.map { (shape, gen) ->
|
||||
dynamicTest(shape) {
|
||||
val thrown = runOnSmallStack { evaluate(gen(HUGE)) }
|
||||
assertThat(thrown).isInstanceOf(PklException::class.java)
|
||||
assertThat(thrown).hasMessageContaining("nested too deeply")
|
||||
}
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
fun `long chains do not overflow the stack`(): List<DynamicTest> = chains.map { (shape, gen) ->
|
||||
dynamicTest(shape) {
|
||||
val thrown = runOnSmallStack { evaluate(gen(HUGE)) }
|
||||
assertThat(thrown).isInstanceOf(PklException::class.java)
|
||||
assertThat(thrown).hasMessageContaining("nested too deeply")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,9 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import org.pkl.parser.GenericParser;
|
||||
import org.pkl.parser.GenericParserError;
|
||||
import org.pkl.parser.syntax.generic.Node;
|
||||
import org.pkl.parser.util.ErrorMessages;
|
||||
|
||||
/**
|
||||
* A formatter for Pkl files that applies canonical formatting rules.
|
||||
@@ -144,8 +146,15 @@ public class Formatter {
|
||||
}
|
||||
|
||||
private void format(String input, Appendable output) {
|
||||
var ast = new GenericParser().parseModule(input);
|
||||
generate(new Builder(input, grammarVersion).format(ast), output);
|
||||
format(input, new GenericParser().parseModule(input), output);
|
||||
}
|
||||
|
||||
void format(String input, Node ast, Appendable output) {
|
||||
try {
|
||||
generate(new Builder(input, grammarVersion).format(ast), output);
|
||||
} catch (StackOverflowError e) {
|
||||
throw new GenericParserError(ErrorMessages.create("tooDeeplyNested"), ast.span);
|
||||
}
|
||||
}
|
||||
|
||||
private static void generate(FormatNode formatAst, Appendable output) {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed 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
|
||||
*
|
||||
* https://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.pkl.formatter
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.DynamicTest
|
||||
import org.junit.jupiter.api.DynamicTest.dynamicTest
|
||||
import org.junit.jupiter.api.TestFactory
|
||||
import org.pkl.parser.GenericParser
|
||||
import org.pkl.parser.GenericParserError
|
||||
|
||||
/** Maliciously deep input must not make the formatter throw a [StackOverflowError]. */
|
||||
class FormatterDepthTest {
|
||||
companion object {
|
||||
private const val STACK_SIZE = 512L * 1024
|
||||
private const val LARGE_STACK_SIZE = 512L * 1024 * 1024
|
||||
private const val NESTING_DEPTH = 10_000
|
||||
private const val CHAIN_LENGTH = 100_000
|
||||
|
||||
private val nesting: Map<String, (Int) -> String> =
|
||||
mapOf(
|
||||
"parens" to { n -> "x = " + "(".repeat(n) + "1" + ")".repeat(n) },
|
||||
"types" to { n -> "x: " + "List<".repeat(n) + "Int" + ">".repeat(n) },
|
||||
"objects" to { n -> "x " + "{ y ".repeat(n) + "{}" + " }".repeat(n) },
|
||||
)
|
||||
|
||||
private val chains: Map<String, (Int) -> String> =
|
||||
mapOf(
|
||||
"binary operator" to { n -> "x = " + "1 + ".repeat(n) + "1" },
|
||||
"qualified access" to { n -> "x = a" + ".b".repeat(n) },
|
||||
"subscript" to { n -> "x = a" + "[1]".repeat(n) },
|
||||
"constrained type" to { n -> "x: Int" + "(isPositive)".repeat(n) },
|
||||
)
|
||||
|
||||
private fun <T> runOn(stackSize: Long, action: () -> T): Result<T> {
|
||||
var result: Result<T>? = null
|
||||
val thread = Thread(null, { result = runCatching(action) }, "test", stackSize)
|
||||
thread.start()
|
||||
thread.join()
|
||||
return result!!
|
||||
}
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
fun `deep nesting fails with a formatter error`(): List<DynamicTest> =
|
||||
nesting.map { (shape, gen) ->
|
||||
dynamicTest(shape) { assertFormatFails(gen(NESTING_DEPTH)) }
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
fun `long chains fail with a formatter error`(): List<DynamicTest> = chains.map { (shape, gen) ->
|
||||
dynamicTest(shape) { assertFormatFails(gen(CHAIN_LENGTH)) }
|
||||
}
|
||||
|
||||
private fun assertFormatFails(source: String) {
|
||||
val ast = runOn(LARGE_STACK_SIZE) { GenericParser().parseModule(source) }.getOrThrow()
|
||||
val thrown =
|
||||
runOn(STACK_SIZE) { Formatter().format(source, ast, StringBuilder()) }.exceptionOrNull()
|
||||
assertThat(thrown).isInstanceOf(GenericParserError::class.java)
|
||||
assertThat(thrown).hasMessageContaining("nested too deeply")
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,12 @@ import org.pkl.parser.syntax.generic.Node;
|
||||
|
||||
public final class GenericParser {
|
||||
public Node parseModule(String source) {
|
||||
return new GenericParserImpl(source).parseModule();
|
||||
var parser = new GenericParserImpl(source);
|
||||
try {
|
||||
return parser.parseModule();
|
||||
} catch (StackOverflowError e) {
|
||||
throw parser.tooDeeplyNested();
|
||||
}
|
||||
}
|
||||
|
||||
public Node parseExpressionInput(String source) {
|
||||
|
||||
@@ -944,7 +944,7 @@ class GenericParserImpl {
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
private Node parseExprRest(Node expr) {
|
||||
// amends
|
||||
if (lookahead() == Token.LBRACE) {
|
||||
while (lookahead() == Token.LBRACE) {
|
||||
var children = new ArrayList<Node>();
|
||||
children.add(expr);
|
||||
ff(children);
|
||||
@@ -952,7 +952,8 @@ class GenericParserImpl {
|
||||
|| expr.type == NodeType.AMENDS_EXPR
|
||||
|| expr.type == NodeType.NEW_EXPR) {
|
||||
children.add(parseObjectBody());
|
||||
return parseExprRest(new Node(NodeType.AMENDS_EXPR, children));
|
||||
expr = new Node(NodeType.AMENDS_EXPR, children);
|
||||
continue;
|
||||
}
|
||||
throw parserError("unexpectedCurlyProbablyAmendsExpression", expr.text(lexer.getSource()));
|
||||
}
|
||||
@@ -1251,31 +1252,33 @@ class GenericParserImpl {
|
||||
}
|
||||
|
||||
private Node parseTypeEnd(Node type) {
|
||||
var children = new ArrayList<Node>();
|
||||
children.add(type);
|
||||
// nullable types
|
||||
if (lookahead() == Token.QUESTION) {
|
||||
ff(children);
|
||||
children.add(makeTerminal(next()));
|
||||
var res = new Node(NodeType.NULLABLE_TYPE, children);
|
||||
return parseTypeEnd(res);
|
||||
while (true) {
|
||||
var children = new ArrayList<Node>();
|
||||
children.add(type);
|
||||
// nullable types
|
||||
if (lookahead() == Token.QUESTION) {
|
||||
ff(children);
|
||||
children.add(makeTerminal(next()));
|
||||
type = new Node(NodeType.NULLABLE_TYPE, children);
|
||||
continue;
|
||||
}
|
||||
// constrained types: have to start in the same line as the type
|
||||
var fla = fullLookahead();
|
||||
if (fla.tk.token == Token.LPAREN && noSemicolonInbetween() && fla.tk.newLinesBetween == 0) {
|
||||
ff(children);
|
||||
var constraint = new ArrayList<Node>();
|
||||
constraint.add(makeTerminal(next()));
|
||||
var elements = new ArrayList<Node>();
|
||||
ff(elements);
|
||||
parseListOf(Token.RPAREN, elements, () -> parseExpr(")"));
|
||||
constraint.add(new Node(NodeType.CONSTRAINED_TYPE_ELEMENTS, elements));
|
||||
expect(Token.RPAREN, constraint, "unexpectedToken2", ",", ")");
|
||||
children.add(new Node(NodeType.CONSTRAINED_TYPE_CONSTRAINT, constraint));
|
||||
type = new Node(NodeType.CONSTRAINED_TYPE, children);
|
||||
continue;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
// constrained types: have to start in the same line as the type
|
||||
var fla = fullLookahead();
|
||||
if (fla.tk.token == Token.LPAREN && noSemicolonInbetween() && fla.tk.newLinesBetween == 0) {
|
||||
ff(children);
|
||||
var constraint = new ArrayList<Node>();
|
||||
constraint.add(makeTerminal(next()));
|
||||
var elements = new ArrayList<Node>();
|
||||
ff(elements);
|
||||
parseListOf(Token.RPAREN, elements, () -> parseExpr(")"));
|
||||
constraint.add(new Node(NodeType.CONSTRAINED_TYPE_ELEMENTS, elements));
|
||||
expect(Token.RPAREN, constraint, "unexpectedToken2", ",", ")");
|
||||
children.add(new Node(NodeType.CONSTRAINED_TYPE_CONSTRAINT, constraint));
|
||||
var res = new Node(NodeType.CONSTRAINED_TYPE, children);
|
||||
return parseTypeEnd(res);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
private Node parseAnnotation() {
|
||||
@@ -1470,6 +1473,10 @@ class GenericParserImpl {
|
||||
}
|
||||
}
|
||||
|
||||
GenericParserError tooDeeplyNested() {
|
||||
return parserError("tooDeeplyNested");
|
||||
}
|
||||
|
||||
private GenericParserError parserError(String messageKey, Object... args) {
|
||||
return new GenericParserError(ErrorMessages.create(messageKey, args), spanLookahead);
|
||||
}
|
||||
|
||||
@@ -23,14 +23,29 @@ public final class Parser {
|
||||
public Parser() {}
|
||||
|
||||
public Module parseModule(String source) {
|
||||
return new ParserImpl(source).parseModule();
|
||||
var parser = new ParserImpl(source);
|
||||
try {
|
||||
return parser.parseModule();
|
||||
} catch (StackOverflowError e) {
|
||||
throw parser.tooDeeplyNested();
|
||||
}
|
||||
}
|
||||
|
||||
public Expr parseExpressionInput(String source) {
|
||||
return new ParserImpl(source).parseExpressionInput();
|
||||
var parser = new ParserImpl(source);
|
||||
try {
|
||||
return parser.parseExpressionInput();
|
||||
} catch (StackOverflowError e) {
|
||||
throw parser.tooDeeplyNested();
|
||||
}
|
||||
}
|
||||
|
||||
public ReplInput parseReplInput(String source) {
|
||||
return new ParserImpl(source).parseReplInput();
|
||||
var parser = new ParserImpl(source);
|
||||
try {
|
||||
return parser.parseReplInput();
|
||||
} catch (StackOverflowError e) {
|
||||
throw parser.tooDeeplyNested();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1089,58 +1089,62 @@ final class ParserImpl {
|
||||
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
private Expr parseExprRest(Expr expr) {
|
||||
// non-null
|
||||
if (lookahead == Token.NON_NULL) {
|
||||
var end = next().span;
|
||||
var res = new NonNullExpr(expr, expr.span().endWith(end));
|
||||
return parseExprRest(res);
|
||||
}
|
||||
// amends
|
||||
if (lookahead == Token.LBRACE) {
|
||||
if (expr instanceof ParenthesizedExpr
|
||||
|| expr instanceof AmendsExpr
|
||||
|| expr instanceof NewExpr) {
|
||||
var body = parseObjectBody();
|
||||
return parseExprRest(new AmendsExpr(expr, body, expr.span().endWith(body.span())));
|
||||
while (true) {
|
||||
// non-null
|
||||
if (lookahead == Token.NON_NULL) {
|
||||
var end = next().span;
|
||||
expr = new NonNullExpr(expr, expr.span().endWith(end));
|
||||
}
|
||||
throw parserError("unexpectedCurlyProbablyAmendsExpression", expr.text(lexer.getSource()));
|
||||
}
|
||||
// qualified access
|
||||
if (lookahead == Token.DOT || lookahead == Token.QDOT) {
|
||||
var isNullable = next().token == Token.QDOT;
|
||||
var identifier = parseIdentifier();
|
||||
TypeArgumentList typeArgumentList = null;
|
||||
if (lookahead == Token.DCOLON && !precededBySemicolon && _lookahead.newLinesBetween == 0) {
|
||||
// amends
|
||||
else if (lookahead == Token.LBRACE) {
|
||||
if (expr instanceof ParenthesizedExpr
|
||||
|| expr instanceof AmendsExpr
|
||||
|| expr instanceof NewExpr) {
|
||||
var body = parseObjectBody();
|
||||
expr = new AmendsExpr(expr, body, expr.span().endWith(body.span()));
|
||||
} else {
|
||||
throw parserError(
|
||||
"unexpectedCurlyProbablyAmendsExpression", expr.text(lexer.getSource()));
|
||||
}
|
||||
}
|
||||
// qualified access
|
||||
else if (lookahead == Token.DOT || lookahead == Token.QDOT) {
|
||||
var isNullable = next().token == Token.QDOT;
|
||||
var identifier = parseIdentifier();
|
||||
TypeArgumentList typeArgumentList = null;
|
||||
if (lookahead == Token.DCOLON && !precededBySemicolon && _lookahead.newLinesBetween == 0) {
|
||||
next();
|
||||
typeArgumentList = parseTypeArgumentList();
|
||||
}
|
||||
ArgumentList argumentList = null;
|
||||
if (lookahead == Token.LPAREN && !precededBySemicolon && _lookahead.newLinesBetween == 0) {
|
||||
argumentList = parseArgumentList();
|
||||
} else if (typeArgumentList != null) {
|
||||
// type args but no args
|
||||
throw new ParserError(ErrorMessages.create("unexpectedToken", "("), spanLookahead);
|
||||
}
|
||||
var lastSpan = argumentList != null ? argumentList.span() : identifier.span();
|
||||
expr =
|
||||
new QualifiedAccessExpr(
|
||||
expr,
|
||||
identifier,
|
||||
isNullable,
|
||||
typeArgumentList,
|
||||
argumentList,
|
||||
expr.span().endWith(lastSpan));
|
||||
}
|
||||
// subscript (needs to be in the same line as the expression)
|
||||
else if (lookahead == Token.LBRACK
|
||||
&& !precededBySemicolon
|
||||
&& _lookahead.newLinesBetween == 0) {
|
||||
next();
|
||||
typeArgumentList = parseTypeArgumentList();
|
||||
var exp = parseExpr("]");
|
||||
var end = expect(Token.RBRACK, "unexpectedToken", "]").span;
|
||||
expr = new SubscriptExpr(expr, exp, expr.span().endWith(end));
|
||||
} else {
|
||||
return expr;
|
||||
}
|
||||
ArgumentList argumentList = null;
|
||||
if (lookahead == Token.LPAREN && !precededBySemicolon && _lookahead.newLinesBetween == 0) {
|
||||
argumentList = parseArgumentList();
|
||||
} else if (typeArgumentList != null) {
|
||||
// type args but no args
|
||||
throw new ParserError(ErrorMessages.create("unexpectedToken", "("), spanLookahead);
|
||||
}
|
||||
var lastSpan = argumentList != null ? argumentList.span() : identifier.span();
|
||||
var res =
|
||||
new QualifiedAccessExpr(
|
||||
expr,
|
||||
identifier,
|
||||
isNullable,
|
||||
typeArgumentList,
|
||||
argumentList,
|
||||
expr.span().endWith(lastSpan));
|
||||
return parseExprRest(res);
|
||||
}
|
||||
// subscript (needs to be in the same line as the expression)
|
||||
if (lookahead == Token.LBRACK && !precededBySemicolon && _lookahead.newLinesBetween == 0) {
|
||||
next();
|
||||
var exp = parseExpr("]");
|
||||
var end = expect(Token.RBRACK, "unexpectedToken", "]").span;
|
||||
var res = new SubscriptExpr(expr, exp, expr.span().endWith(end));
|
||||
return parseExprRest(res);
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
|
||||
private Expr parseSingleLineStringLiteralExpr() {
|
||||
@@ -1493,25 +1497,28 @@ final class ParserImpl {
|
||||
}
|
||||
|
||||
private Type parseTypeEnd(Type type) {
|
||||
// nullable types
|
||||
if (lookahead == Token.QUESTION) {
|
||||
var end = spanLookahead;
|
||||
next();
|
||||
var res = new Type.NullableType(type, type.span().endWith(end));
|
||||
return parseTypeEnd(res);
|
||||
while (true) {
|
||||
// nullable types
|
||||
if (lookahead == Token.QUESTION) {
|
||||
var end = spanLookahead;
|
||||
next();
|
||||
type = new Type.NullableType(type, type.span().endWith(end));
|
||||
}
|
||||
// constrained types: have to start in the same line as the type
|
||||
else if (lookahead == Token.LPAREN
|
||||
&& !precededBySemicolon
|
||||
&& _lookahead.newLinesBetween == 0) {
|
||||
next();
|
||||
var constraints = parseListOf(Token.COMMA, Token.RPAREN, () -> parseExpr(")"));
|
||||
var end = expect(Token.RPAREN, "unexpectedToken2", ",", ")").span;
|
||||
var children = new ArrayList<Node>(constraints.size() + 1);
|
||||
children.add(type);
|
||||
children.addAll(constraints);
|
||||
type = new Type.ConstrainedType(children, type.span().endWith(end));
|
||||
} else {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
// constrained types: have to start in the same line as the type
|
||||
if (lookahead == Token.LPAREN && !precededBySemicolon && _lookahead.newLinesBetween == 0) {
|
||||
next();
|
||||
var constraints = parseListOf(Token.COMMA, Token.RPAREN, () -> parseExpr(")"));
|
||||
var end = expect(Token.RPAREN, "unexpectedToken2", ",", ")").span;
|
||||
var children = new ArrayList<Node>(constraints.size() + 1);
|
||||
children.add(type);
|
||||
children.addAll(constraints);
|
||||
var res = new Type.ConstrainedType(children, type.span().endWith(end));
|
||||
return parseTypeEnd(res);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
private Annotation parseAnnotation() {
|
||||
@@ -1829,6 +1836,10 @@ final class ParserImpl {
|
||||
return res;
|
||||
}
|
||||
|
||||
ParserError tooDeeplyNested() {
|
||||
return parserError("tooDeeplyNested");
|
||||
}
|
||||
|
||||
private ParserError parserError(String messageKey, Object... args) {
|
||||
return new ParserError(ErrorMessages.create(messageKey, args), spanLookahead);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ Unexpected token `{0}`. Expected a type.
|
||||
unexpectedTokenForType2=\
|
||||
Unexpected token `{0}`. Expected a type or `{1}`.
|
||||
|
||||
tooDeeplyNested=\
|
||||
Code is nested too deeply, or contains too many chained operations.
|
||||
|
||||
unexpectedCurlyProbablyAmendsExpression=\
|
||||
Unexpected token: `'{'`.\n\
|
||||
\n\
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed 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
|
||||
*
|
||||
* https://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.pkl.parser
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.DynamicTest
|
||||
import org.junit.jupiter.api.DynamicTest.dynamicTest
|
||||
import org.junit.jupiter.api.TestFactory
|
||||
|
||||
/**
|
||||
* Malicious input must produce a regular parse error instead of a [StackOverflowError], even on a
|
||||
* small thread stack.
|
||||
*/
|
||||
class ParserDepthTest {
|
||||
companion object {
|
||||
private const val STACK_SIZE = 512L * 1024
|
||||
private const val HUGE = 100_000
|
||||
|
||||
private val nesting: Map<String, (Int) -> String> =
|
||||
mapOf(
|
||||
"parens" to { n -> "x = " + "(".repeat(n) + "1" + ")".repeat(n) },
|
||||
"unary minus" to { n -> "x = " + "-".repeat(n) + "1" },
|
||||
"types" to { n -> "x: " + "List<".repeat(n) + "Int" + ">".repeat(n) },
|
||||
"parenthesized types" to { n -> "x: " + "(".repeat(n) + "Int" + ")".repeat(n) },
|
||||
"objects" to { n -> "x " + "{ y ".repeat(n) + "{}" + " }".repeat(n) },
|
||||
"string interpolation" to { n -> "x = " + "\"\\(".repeat(n) + "1" + ")\"".repeat(n) },
|
||||
"if" to { n -> "x = " + "if (true) 1 else ".repeat(n) + "2" },
|
||||
"let" to { n -> "x = " + "let (a = 1) ".repeat(n) + "2" },
|
||||
"function literal" to { n -> "x = " + "(a) -> ".repeat(n) + "2" },
|
||||
"right associative operator" to { n -> "x = " + "a ?? ".repeat(n) + "b" },
|
||||
"pow" to { n -> "x = " + "a ** ".repeat(n) + "b" },
|
||||
)
|
||||
|
||||
// shapes that build deep, left-nested trees without recursing in the parser
|
||||
private val chains: Map<String, (Int) -> String> =
|
||||
mapOf(
|
||||
"binary operator" to { n -> "x = " + "1 + ".repeat(n) + "1" },
|
||||
"qualified access" to { n -> "x = a" + ".b".repeat(n) },
|
||||
"subscript" to { n -> "x = a" + "[1]".repeat(n) },
|
||||
"non-null" to { n -> "x = a" + "!!".repeat(n) },
|
||||
"constrained type" to { n -> "x: Int" + "(isPositive)".repeat(n) },
|
||||
"amends" to { n -> "x = (y) " + "{} ".repeat(n) },
|
||||
)
|
||||
|
||||
private fun parsers(): Map<String, (String) -> Unit> =
|
||||
mapOf(
|
||||
"Parser" to { src -> Parser().parseModule(src) },
|
||||
"GenericParser" to { src -> GenericParser().parseModule(src) },
|
||||
)
|
||||
|
||||
/** Runs [action] on a thread with a small stack, and returns what it threw (if anything). */
|
||||
private fun runOnSmallStack(action: () -> Unit): Throwable? {
|
||||
var thrown: Throwable? = null
|
||||
val thread =
|
||||
Thread(null, { thrown = runCatching(action).exceptionOrNull() }, "test", STACK_SIZE)
|
||||
thread.start()
|
||||
thread.join()
|
||||
return thrown
|
||||
}
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
fun `deep nesting fails with a parse error`(): List<DynamicTest> =
|
||||
nesting.flatMap { (shape, gen) ->
|
||||
parsers().map { (parser, parse) ->
|
||||
dynamicTest("$parser: $shape x $HUGE") {
|
||||
val thrown = runOnSmallStack { parse(gen(HUGE)) }
|
||||
assertThat(thrown).isNotNull
|
||||
assertThat(thrown).isNotInstanceOf(StackOverflowError::class.java)
|
||||
assertThat(thrown).hasMessageContaining("nested too deeply")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
fun `long chains are parsed iteratively`(): List<DynamicTest> = chains.flatMap { (shape, gen) ->
|
||||
parsers().map { (parser, parse) ->
|
||||
dynamicTest("$parser: $shape x $HUGE") {
|
||||
assertThat(runOnSmallStack { parse(gen(HUGE)) }).isNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
fun `flat repetition is unaffected`(): List<DynamicTest> =
|
||||
mapOf(
|
||||
"union type" to "x: " + "Int|".repeat(HUGE) + "Int",
|
||||
"listing elements" to "x = new Listing { " + "1\n".repeat(HUGE) + "}",
|
||||
"properties" to (1..10_000).joinToString("\n") { "p$it = 1" },
|
||||
)
|
||||
.flatMap { (shape, src) ->
|
||||
parsers().map { (parser, parse) ->
|
||||
dynamicTest("$parser: $shape") { assertThat(runOnSmallStack { parse(src) }).isNull() }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user