Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,7 @@ public static ContainsExpression contains(ValueExpression<?> target, ValueExpres
return new ContainsExpression(target, value);
}

public static StringPredicateExpression matches(
ValueExpression<?> valueExpression, StringValue str) {
public static MatchesExpression matches(ValueExpression<?> valueExpression, StringValue str) {
return new MatchesExpression(valueExpression, str);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import com.datadog.debugger.el.expressions.BooleanExpression;
import com.datadog.debugger.el.expressions.ContainsExpression;
import com.datadog.debugger.el.expressions.StringPredicateExpression;
import com.datadog.debugger.el.expressions.ValueExpression;
import com.datadog.debugger.el.values.StringValue;
import com.squareup.moshi.JsonReader;
Expand Down Expand Up @@ -433,9 +432,9 @@ public static ValueExpression<?> asValueExpression(JsonReader reader) throws IOE
}
}

private static StringPredicateExpression createStringPredicateExpression(
private static BooleanExpression createStringPredicateExpression(
JsonReader reader,
BiFunction<ValueExpression<?>, StringValue, StringPredicateExpression> predicateFunc)
BiFunction<ValueExpression<?>, StringValue, ? extends BooleanExpression> predicateFunc)
throws IOException {
JsonReader.Token token = reader.peek();
if (token != BEGIN_ARRAY) {
Expand All @@ -445,7 +444,7 @@ private static StringPredicateExpression createStringPredicateExpression(
reader.beginArray();
ValueExpression<?> source = asValueExpression(reader);
StringValue str = new StringValue(reader.nextString());
StringPredicateExpression expr = predicateFunc.apply(source, str);
BooleanExpression expr = predicateFunc.apply(source, str);
reader.endArray();
return expr;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,11 @@ public String visit(LenExpression lenExpression) {

@Override
public String visit(MatchesExpression matchesExpression) {
return stringPredicateExpression(matchesExpression);
return "matches("
+ nullSafeAccept(matchesExpression.getSourceString())
+ ", "
+ nullSafeAccept(matchesExpression.getStr())
+ ")";
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,113 @@
package com.datadog.debugger.el.expressions;

import com.datadog.debugger.el.EvalContext;
import com.datadog.debugger.el.EvaluationException;
import com.datadog.debugger.el.Generated;
import com.datadog.debugger.el.PrettyPrintVisitor;
import com.datadog.debugger.el.Value;
import com.datadog.debugger.el.Visitor;
import com.datadog.debugger.el.values.StringValue;
import datadog.trace.bootstrap.debugger.util.TimeoutChecker;
import java.util.Objects;
import java.util.regex.Pattern;

public class MatchesExpression implements BooleanExpression {
private final ValueExpression<?> sourceString;
private final StringValue str;
private final Pattern pattern;

public class MatchesExpression extends StringPredicateExpression {
public MatchesExpression(ValueExpression<?> sourceString, StringValue str) {
super(sourceString, str, String::matches, "matches");
this.sourceString = sourceString;
this.str = str;
pattern = str != null && str.getValue() != null ? Pattern.compile(str.getValue()) : null;
}

@Override
public <R> R accept(Visitor<R> visitor) {
return visitor.visit(this);
}

@Override
public Boolean evaluate(EvalContext evalContext) {
Value<?> sourceValue =
sourceString != null ? sourceString.evaluate(evalContext) : Value.nullValue();
if (sourceValue.isUndefined()) {
throw new EvaluationException(
"Cannot evaluate the expression for undefined value", PrettyPrintVisitor.print(this));
}
if (sourceValue.isNull()) {
throw new EvaluationException(
"Cannot evaluate the expression for null value", PrettyPrintVisitor.print(this));
}
if (pattern != null && sourceValue.getValue() instanceof String) {
String sourceStr = (String) sourceValue.getValue();
TimeoutCharSequence timeoutCharSequence =
new TimeoutCharSequence(evalContext.getTimeoutChecker(), this, sourceStr);
boolean result = pattern.matcher(timeoutCharSequence).matches();
ExpressionHelper.checkTimeout(evalContext.getTimeoutChecker(), this);
return result;
}
return Boolean.FALSE;
}

public ValueExpression<?> getSourceString() {
return sourceString;
}

public StringValue getStr() {
return str;
}

/** Wraps a String to check the timeout on each char access while the regex is matching. */
private static class TimeoutCharSequence implements CharSequence {
private final TimeoutChecker timeoutChecker;
private final MatchesExpression matchesExpression;
private final String s;
private int charAccessCount;

TimeoutCharSequence(
TimeoutChecker timeoutChecker, MatchesExpression matchesExpression, String s) {
this.timeoutChecker = timeoutChecker;
this.matchesExpression = matchesExpression;
this.s = s;
}

@Override
public int length() {
return s.length();
}

@Override
public char charAt(int index) {
if ((charAccessCount++ & 0xFFF) == 0) {
// checks every 4096 char accesses
ExpressionHelper.checkTimeout(timeoutChecker, matchesExpression);
}
return s.charAt(index);
}
Comment thread
jpbempel marked this conversation as resolved.

@Override
public CharSequence subSequence(int start, int end) {
return s.subSequence(start, end);
}

@Override
public String toString() {
return s;
}
}

@Generated
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
MatchesExpression that = (MatchesExpression) o;
return Objects.equals(sourceString, that.sourceString) && Objects.equals(str, that.str);
}

@Generated
@Override
public int hashCode() {
return Objects.hash(sourceString, str);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.datadog.debugger.el.DSL;
import com.datadog.debugger.el.EvalContext;
import com.datadog.debugger.el.EvaluationException;
import com.datadog.debugger.el.EvaluationTimeOutException;
import com.datadog.debugger.el.values.StringValue;
import datadog.trace.bootstrap.debugger.el.Values;
import java.net.URI;
import java.time.Duration;
import org.junit.jupiter.api.Test;

class MatchesExpressionTest {
Expand Down Expand Up @@ -63,4 +66,29 @@ void stringPrimitives() {
assertTrue(expression.evaluate(evalContext));
assertEquals("matches(uri, \"^https?://w{3}\\.datadoghq\\.com$\")", print(expression));
}

@Test
void catastrophicBacktrackingTimeout() {
// nested quantifiers against a string of 'a's without trailing 'b' backtracks exponentially.
// the backreference prevents the JDK (9+) from memoizing the group loop, as it does for (a+)+b
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 40; i++) {
sb.append('a');
}
sb.append('!');
MatchesExpression expression =
new MatchesExpression(DSL.value(sb.toString()), new StringValue("((a+)\\2?)+b"));
// guard against the regex not being interrupted: without timeout check this would run forever
EvaluationException exception =
assertTimeoutPreemptively(
Duration.ofSeconds(5),
() -> {
// create the context in the evaluating thread (CPU timeout checker is per thread)
EvalContext timeoutEvalContext = createEvalContext(this, Duration.ofMillis(10));
return assertThrows(
EvaluationTimeOutException.class, () -> expression.evaluate(timeoutEvalContext));
});
assertEquals("timeout (10ms)", exception.getMessage());
assertEquals("matches(\"" + sb + "\", \"((a+)\\2?)+b\")", print(expression));
}
}
Loading