[ 
https://issues.apache.org/jira/browse/GROOVY-12122?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18093769#comment-18093769
 ] 

ASF GitHub Bot commented on GROOVY-12122:
-----------------------------------------

Copilot commented on code in PR #2668:
URL: https://github.com/apache/groovy/pull/2668#discussion_r3524401202


##########
src/main/java/groovy/util/regex/RegexGuard.java:
##########
@@ -0,0 +1,214 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF 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 groovy.util.regex;
+
+import org.apache.groovy.lang.annotation.Incubating;
+import org.codehaus.groovy.runtime.FormatHelper;
+import org.codehaus.groovy.runtime.RegexSupport;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Deadline-guarded regular expression evaluation, protecting against
+ * Regular Expression Denial of Service (ReDoS) when patterns or inputs
+ * come from untrusted sources. Java's regex engine has no native timeout,
+ * so catastrophic backtracking can hang a thread indefinitely.
+ * <p>
+ * The guard works by wrapping the input {@code CharSequence} so that
+ * {@code charAt} periodically checks a deadline and throws
+ * {@link RegexTimeoutException} once it has passed. Backtracking repeatedly
+ * re-reads input characters, so a runaway match is cut off shortly after the
+ * deadline without needing watchdog threads or thread interruption.
+ * <pre class="groovyTestCase">
+ * import groovy.util.regex.RegexGuard
+ * import groovy.util.regex.RegexTimeoutException
+ * import java.time.Duration
+ *
+ * assert RegexGuard.matches(/gro+vy/, 'groovy', 200)
+ * def m = RegexGuard.matcher(/(\d+)/, 'abc 123', Duration.ofMillis(200))
+ * assert m.find() && m.group(1) == '123'
+ *
+ * try {
+ *     RegexGuard.matches(/(.*,){16}X/, '1,' * 40, 200) // catastrophic 
backtracking
+ *     assert false, 'should have timed out'
+ * } catch (RegexTimeoutException expected) {
+ * }
+ * </pre>
+ * Notes and limitations:
+ * <ul>
+ * <li>The deadline starts when the guarded matcher is created and covers all
+ * subsequent use of it, e.g. repeated {@code find()} calls.</li>
+ * <li>The clock is only consulted while the engine reads input characters,
+ * every 512 reads; evaluations finishing in fewer reads
+ * never pay for a clock call. Pathological patterns perform millions of reads
+ * per second, so overshoot past the deadline is negligible in practice.</li>
+ * <li>{@code Pattern.compile} itself is not guarded; pattern compilation is
+ * not subject to backtracking.</li>
+ * </ul>
+ *
+ * @see groovy.transform.SafeRegex
+ * @since 6.0.0
+ */
+@Incubating
+public final class RegexGuard {
+
+    private RegexGuard() {
+    }
+
+    /**
+     * Matches the whole input against the pattern like the {@code ==~} 
operator,
+     * giving up once the timeout has elapsed. Follows the operator's 
conventions:
+     * a {@code null} pattern or input yields {@code false}, non-{@code 
Pattern}
+     * patterns and non-{@code String} inputs are converted via their default
+     * string representation, and the matcher is stored for
+     * {@code Matcher.lastMatcher}.
+     *
+     * @param pattern the pattern, a {@link Pattern} or an object whose string 
representation is the regex
+     * @param input   the text to match
+     * @param millis  the timeout in milliseconds (must be positive)
+     * @return {@code true} if the whole input matches the pattern
+     * @throws RegexTimeoutException if evaluation exceeds the timeout
+     */
+    public static boolean matches(Object pattern, Object input, long millis) {
+        return doMatches(pattern, input, toDeadlineNanos(millis), millis + " 
ms");
+    }
+
+    /**
+     * Variant of {@link #matches(Object, Object, long)} taking the timeout as 
a {@link Duration}.
+     */
+    public static boolean matches(Object pattern, Object input, Duration 
timeout) {
+        return doMatches(pattern, input, toDeadlineNanos(timeout), 
timeout.toString());
+    }
+
+    /**
+     * Creates a matcher of the pattern over deadline-guarded input, like the
+     * {@code =~} operator. Matcher operations that read the input, e.g.
+     * {@code find()} or {@code matches()}, throw {@link RegexTimeoutException}
+     * once the deadline, measured from this call, has passed.
+     *
+     * @param pattern the pattern, a {@link Pattern} or an object whose string 
representation is the regex
+     * @param input   the text to match
+     * @param millis  the timeout in milliseconds (must be positive)
+     * @return a matcher over the guarded input
+     */
+    public static Matcher matcher(Object pattern, Object input, long millis) {
+        return doMatcher(pattern, input, toDeadlineNanos(millis), millis + " 
ms");
+    }
+
+    /**
+     * Variant of {@link #matcher(Object, Object, long)} taking the timeout as 
a {@link Duration}.
+     */
+    public static Matcher matcher(Object pattern, Object input, Duration 
timeout) {
+        return doMatcher(pattern, input, toDeadlineNanos(timeout), 
timeout.toString());
+    }
+
+    /**
+     * Wraps a character sequence so that reads beyond the deadline throw
+     * {@link RegexTimeoutException}. Useful for guarding regex APIs not 
covered
+     * by the other helpers, e.g. {@code Pattern.split} or manual matcher 
creation.
+     *
+     * @param input  the sequence to guard
+     * @param millis the timeout in milliseconds (must be positive)
+     * @return a guarded view of the input
+     */
+    public static CharSequence guard(CharSequence input, long millis) {
+        return new GuardedCharSequence(input, toDeadlineNanos(millis), millis 
+ " ms");
+    }
+
+    /**
+     * Variant of {@link #guard(CharSequence, long)} taking the timeout as a 
{@link Duration}.
+     */
+    public static CharSequence guard(CharSequence input, Duration timeout) {
+        return new GuardedCharSequence(input, toDeadlineNanos(timeout), 
timeout.toString());
+    }
+
+    private static boolean doMatches(Object pattern, Object input, long 
deadline, String timeoutText) {
+        if (pattern == null || input == null) return false;
+        Matcher matcher = toPattern(pattern).matcher(new 
GuardedCharSequence(toCharSequence(input), deadline, timeoutText));
+        RegexSupport.setLastMatcher(matcher);
+        return matcher.matches();
+    }
+
+    private static Matcher doMatcher(Object pattern, Object input, long 
deadline, String timeoutText) {
+        return toPattern(pattern).matcher(new 
GuardedCharSequence(toCharSequence(input), deadline, timeoutText));
+    }
+
+    private static long toDeadlineNanos(long millis) {
+        if (millis <= 0) throw new IllegalArgumentException("timeout must be 
positive but was " + millis);
+        return System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(millis);
+    }
+
+    private static long toDeadlineNanos(Duration timeout) {
+        if (timeout == null || timeout.isZero() || timeout.isNegative())
+            throw new IllegalArgumentException("timeout must be positive but 
was " + timeout);
+        return System.nanoTime() + timeout.toNanos();
+    }

Review Comment:
   `toDeadlineNanos(Duration)` has the same potential overflow issue as the 
`long` overload: adding a very large duration to `System.nanoTime()` can wrap 
negative and make the guard time out immediately. Saturating to 
`Long.MAX_VALUE` when overflow is detected keeps large timeouts from 
misbehaving.



##########
src/main/java/groovy/util/regex/RegexGuard.java:
##########
@@ -0,0 +1,214 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF 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 groovy.util.regex;
+
+import org.apache.groovy.lang.annotation.Incubating;
+import org.codehaus.groovy.runtime.FormatHelper;
+import org.codehaus.groovy.runtime.RegexSupport;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Deadline-guarded regular expression evaluation, protecting against
+ * Regular Expression Denial of Service (ReDoS) when patterns or inputs
+ * come from untrusted sources. Java's regex engine has no native timeout,
+ * so catastrophic backtracking can hang a thread indefinitely.
+ * <p>
+ * The guard works by wrapping the input {@code CharSequence} so that
+ * {@code charAt} periodically checks a deadline and throws
+ * {@link RegexTimeoutException} once it has passed. Backtracking repeatedly
+ * re-reads input characters, so a runaway match is cut off shortly after the
+ * deadline without needing watchdog threads or thread interruption.
+ * <pre class="groovyTestCase">
+ * import groovy.util.regex.RegexGuard
+ * import groovy.util.regex.RegexTimeoutException
+ * import java.time.Duration
+ *
+ * assert RegexGuard.matches(/gro+vy/, 'groovy', 200)
+ * def m = RegexGuard.matcher(/(\d+)/, 'abc 123', Duration.ofMillis(200))
+ * assert m.find() && m.group(1) == '123'
+ *
+ * try {
+ *     RegexGuard.matches(/(.*,){16}X/, '1,' * 40, 200) // catastrophic 
backtracking
+ *     assert false, 'should have timed out'
+ * } catch (RegexTimeoutException expected) {
+ * }
+ * </pre>
+ * Notes and limitations:
+ * <ul>
+ * <li>The deadline starts when the guarded matcher is created and covers all
+ * subsequent use of it, e.g. repeated {@code find()} calls.</li>
+ * <li>The clock is only consulted while the engine reads input characters,
+ * every 512 reads; evaluations finishing in fewer reads
+ * never pay for a clock call. Pathological patterns perform millions of reads
+ * per second, so overshoot past the deadline is negligible in practice.</li>
+ * <li>{@code Pattern.compile} itself is not guarded; pattern compilation is
+ * not subject to backtracking.</li>
+ * </ul>
+ *
+ * @see groovy.transform.SafeRegex
+ * @since 6.0.0
+ */
+@Incubating
+public final class RegexGuard {
+
+    private RegexGuard() {
+    }
+
+    /**
+     * Matches the whole input against the pattern like the {@code ==~} 
operator,
+     * giving up once the timeout has elapsed. Follows the operator's 
conventions:
+     * a {@code null} pattern or input yields {@code false}, non-{@code 
Pattern}
+     * patterns and non-{@code String} inputs are converted via their default
+     * string representation, and the matcher is stored for
+     * {@code Matcher.lastMatcher}.
+     *
+     * @param pattern the pattern, a {@link Pattern} or an object whose string 
representation is the regex
+     * @param input   the text to match
+     * @param millis  the timeout in milliseconds (must be positive)
+     * @return {@code true} if the whole input matches the pattern
+     * @throws RegexTimeoutException if evaluation exceeds the timeout
+     */
+    public static boolean matches(Object pattern, Object input, long millis) {
+        return doMatches(pattern, input, toDeadlineNanos(millis), millis + " 
ms");
+    }
+
+    /**
+     * Variant of {@link #matches(Object, Object, long)} taking the timeout as 
a {@link Duration}.
+     */
+    public static boolean matches(Object pattern, Object input, Duration 
timeout) {
+        return doMatches(pattern, input, toDeadlineNanos(timeout), 
timeout.toString());
+    }
+
+    /**
+     * Creates a matcher of the pattern over deadline-guarded input, like the
+     * {@code =~} operator. Matcher operations that read the input, e.g.
+     * {@code find()} or {@code matches()}, throw {@link RegexTimeoutException}
+     * once the deadline, measured from this call, has passed.
+     *
+     * @param pattern the pattern, a {@link Pattern} or an object whose string 
representation is the regex
+     * @param input   the text to match
+     * @param millis  the timeout in milliseconds (must be positive)
+     * @return a matcher over the guarded input
+     */
+    public static Matcher matcher(Object pattern, Object input, long millis) {
+        return doMatcher(pattern, input, toDeadlineNanos(millis), millis + " 
ms");
+    }
+
+    /**
+     * Variant of {@link #matcher(Object, Object, long)} taking the timeout as 
a {@link Duration}.
+     */
+    public static Matcher matcher(Object pattern, Object input, Duration 
timeout) {
+        return doMatcher(pattern, input, toDeadlineNanos(timeout), 
timeout.toString());
+    }
+
+    /**
+     * Wraps a character sequence so that reads beyond the deadline throw
+     * {@link RegexTimeoutException}. Useful for guarding regex APIs not 
covered
+     * by the other helpers, e.g. {@code Pattern.split} or manual matcher 
creation.
+     *
+     * @param input  the sequence to guard
+     * @param millis the timeout in milliseconds (must be positive)
+     * @return a guarded view of the input
+     */
+    public static CharSequence guard(CharSequence input, long millis) {
+        return new GuardedCharSequence(input, toDeadlineNanos(millis), millis 
+ " ms");
+    }
+
+    /**
+     * Variant of {@link #guard(CharSequence, long)} taking the timeout as a 
{@link Duration}.
+     */
+    public static CharSequence guard(CharSequence input, Duration timeout) {
+        return new GuardedCharSequence(input, toDeadlineNanos(timeout), 
timeout.toString());
+    }
+
+    private static boolean doMatches(Object pattern, Object input, long 
deadline, String timeoutText) {
+        if (pattern == null || input == null) return false;
+        Matcher matcher = toPattern(pattern).matcher(new 
GuardedCharSequence(toCharSequence(input), deadline, timeoutText));
+        RegexSupport.setLastMatcher(matcher);
+        return matcher.matches();
+    }
+
+    private static Matcher doMatcher(Object pattern, Object input, long 
deadline, String timeoutText) {
+        return toPattern(pattern).matcher(new 
GuardedCharSequence(toCharSequence(input), deadline, timeoutText));
+    }
+
+    private static long toDeadlineNanos(long millis) {
+        if (millis <= 0) throw new IllegalArgumentException("timeout must be 
positive but was " + millis);
+        return System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(millis);
+    }

Review Comment:
   `toDeadlineNanos(long)` can overflow when adding a very large timeout to 
`System.nanoTime()`, producing a negative deadline and causing an immediate 
`RegexTimeoutException` (or other incorrect timing). Consider saturating the 
computed deadline to `Long.MAX_VALUE` when the addition overflows so large 
timeouts behave as “effectively no timeout”.



##########
src/main/java/groovy/util/regex/RegexGuard.java:
##########
@@ -0,0 +1,214 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF 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 groovy.util.regex;
+
+import org.apache.groovy.lang.annotation.Incubating;
+import org.codehaus.groovy.runtime.FormatHelper;
+import org.codehaus.groovy.runtime.RegexSupport;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Deadline-guarded regular expression evaluation, protecting against
+ * Regular Expression Denial of Service (ReDoS) when patterns or inputs
+ * come from untrusted sources. Java's regex engine has no native timeout,
+ * so catastrophic backtracking can hang a thread indefinitely.
+ * <p>
+ * The guard works by wrapping the input {@code CharSequence} so that
+ * {@code charAt} periodically checks a deadline and throws
+ * {@link RegexTimeoutException} once it has passed. Backtracking repeatedly
+ * re-reads input characters, so a runaway match is cut off shortly after the
+ * deadline without needing watchdog threads or thread interruption.
+ * <pre class="groovyTestCase">
+ * import groovy.util.regex.RegexGuard
+ * import groovy.util.regex.RegexTimeoutException
+ * import java.time.Duration
+ *
+ * assert RegexGuard.matches(/gro+vy/, 'groovy', 200)
+ * def m = RegexGuard.matcher(/(\d+)/, 'abc 123', Duration.ofMillis(200))
+ * assert m.find() && m.group(1) == '123'
+ *
+ * try {
+ *     RegexGuard.matches(/(.*,){16}X/, '1,' * 40, 200) // catastrophic 
backtracking
+ *     assert false, 'should have timed out'
+ * } catch (RegexTimeoutException expected) {
+ * }
+ * </pre>
+ * Notes and limitations:
+ * <ul>
+ * <li>The deadline starts when the guarded matcher is created and covers all
+ * subsequent use of it, e.g. repeated {@code find()} calls.</li>
+ * <li>The clock is only consulted while the engine reads input characters,
+ * every 512 reads; evaluations finishing in fewer reads
+ * never pay for a clock call. Pathological patterns perform millions of reads
+ * per second, so overshoot past the deadline is negligible in practice.</li>
+ * <li>{@code Pattern.compile} itself is not guarded; pattern compilation is
+ * not subject to backtracking.</li>
+ * </ul>
+ *
+ * @see groovy.transform.SafeRegex
+ * @since 6.0.0
+ */
+@Incubating
+public final class RegexGuard {
+
+    private RegexGuard() {
+    }
+
+    /**
+     * Matches the whole input against the pattern like the {@code ==~} 
operator,
+     * giving up once the timeout has elapsed. Follows the operator's 
conventions:
+     * a {@code null} pattern or input yields {@code false}, non-{@code 
Pattern}
+     * patterns and non-{@code String} inputs are converted via their default
+     * string representation, and the matcher is stored for
+     * {@code Matcher.lastMatcher}.
+     *
+     * @param pattern the pattern, a {@link Pattern} or an object whose string 
representation is the regex
+     * @param input   the text to match
+     * @param millis  the timeout in milliseconds (must be positive)
+     * @return {@code true} if the whole input matches the pattern
+     * @throws RegexTimeoutException if evaluation exceeds the timeout
+     */
+    public static boolean matches(Object pattern, Object input, long millis) {
+        return doMatches(pattern, input, toDeadlineNanos(millis), millis + " 
ms");
+    }
+
+    /**
+     * Variant of {@link #matches(Object, Object, long)} taking the timeout as 
a {@link Duration}.
+     */
+    public static boolean matches(Object pattern, Object input, Duration 
timeout) {
+        return doMatches(pattern, input, toDeadlineNanos(timeout), 
timeout.toString());
+    }
+
+    /**
+     * Creates a matcher of the pattern over deadline-guarded input, like the
+     * {@code =~} operator. Matcher operations that read the input, e.g.
+     * {@code find()} or {@code matches()}, throw {@link RegexTimeoutException}
+     * once the deadline, measured from this call, has passed.
+     *
+     * @param pattern the pattern, a {@link Pattern} or an object whose string 
representation is the regex
+     * @param input   the text to match
+     * @param millis  the timeout in milliseconds (must be positive)
+     * @return a matcher over the guarded input
+     */
+    public static Matcher matcher(Object pattern, Object input, long millis) {
+        return doMatcher(pattern, input, toDeadlineNanos(millis), millis + " 
ms");
+    }
+
+    /**
+     * Variant of {@link #matcher(Object, Object, long)} taking the timeout as 
a {@link Duration}.
+     */
+    public static Matcher matcher(Object pattern, Object input, Duration 
timeout) {
+        return doMatcher(pattern, input, toDeadlineNanos(timeout), 
timeout.toString());
+    }
+
+    /**
+     * Wraps a character sequence so that reads beyond the deadline throw
+     * {@link RegexTimeoutException}. Useful for guarding regex APIs not 
covered
+     * by the other helpers, e.g. {@code Pattern.split} or manual matcher 
creation.
+     *
+     * @param input  the sequence to guard
+     * @param millis the timeout in milliseconds (must be positive)
+     * @return a guarded view of the input
+     */
+    public static CharSequence guard(CharSequence input, long millis) {
+        return new GuardedCharSequence(input, toDeadlineNanos(millis), millis 
+ " ms");
+    }
+
+    /**
+     * Variant of {@link #guard(CharSequence, long)} taking the timeout as a 
{@link Duration}.
+     */
+    public static CharSequence guard(CharSequence input, Duration timeout) {
+        return new GuardedCharSequence(input, toDeadlineNanos(timeout), 
timeout.toString());
+    }
+
+    private static boolean doMatches(Object pattern, Object input, long 
deadline, String timeoutText) {
+        if (pattern == null || input == null) return false;
+        Matcher matcher = toPattern(pattern).matcher(new 
GuardedCharSequence(toCharSequence(input), deadline, timeoutText));
+        RegexSupport.setLastMatcher(matcher);
+        return matcher.matches();
+    }
+
+    private static Matcher doMatcher(Object pattern, Object input, long 
deadline, String timeoutText) {
+        return toPattern(pattern).matcher(new 
GuardedCharSequence(toCharSequence(input), deadline, timeoutText));
+    }
+
+    private static long toDeadlineNanos(long millis) {
+        if (millis <= 0) throw new IllegalArgumentException("timeout must be 
positive but was " + millis);
+        return System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(millis);
+    }
+
+    private static long toDeadlineNanos(Duration timeout) {
+        if (timeout == null || timeout.isZero() || timeout.isNegative())
+            throw new IllegalArgumentException("timeout must be positive but 
was " + timeout);
+        return System.nanoTime() + timeout.toNanos();
+    }
+
+    private static Pattern toPattern(Object pattern) {
+        if (pattern instanceof Pattern p) return p;
+        return Pattern.compile(pattern instanceof String s ? s : 
FormatHelper.toString(pattern));
+    }
+
+    private static CharSequence toCharSequence(Object input) {
+        // convert like the regex operators do; also avoids guarding sequences
+        // with costly charAt implementations (e.g. GString)
+        return input instanceof String s ? s : FormatHelper.toString(input);
+    }
+
+    private static final int CHECK_INTERVAL = 512;
+
+    private static final class GuardedCharSequence implements CharSequence {
+        private final CharSequence delegate;
+        private final long deadline;
+        private final String timeoutText;
+        private int reads; // single matching thread; racy updates only affect 
check cadence
+
+        private GuardedCharSequence(CharSequence delegate, long deadline, 
String timeoutText) {
+            this.delegate = delegate;
+            this.deadline = deadline;
+            this.timeoutText = timeoutText;
+        }
+
+        @Override
+        public char charAt(int index) {
+            if (++reads % CHECK_INTERVAL == 0 && System.nanoTime() - deadline 
> 0) {
+                throw new RegexTimeoutException("regex evaluation exceeded 
timeout of " + timeoutText);
+            }
+            return delegate.charAt(index);

Review Comment:
   `GuardedCharSequence.charAt` is on the hot path of regex evaluation; using 
`% CHECK_INTERVAL` introduces a division/modulo on every character read. Since 
`CHECK_INTERVAL` is a power of two (512), this can be made cheaper with a 
bitmask check.





> Provide a Regex timeout facility
> --------------------------------
>
>                 Key: GROOVY-12122
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12122
>             Project: Groovy
>          Issue Type: Improvement
>          Components: regex
>            Reporter: Paul King
>            Priority: Major
>              Labels: GEP
>             Fix For: 7.x
>
>
> h2. Summary
> Add an opt-in, thread-free runtime guard against Regular Expression Denial of 
> Service
> (ReDoS / catastrophic backtracking) for Groovy's regex features, offered as a 
> runtime
> helper ({{RegexGuard}}), a scoped annotation ({{@SafeRegex}}), and hardening 
> of Groovy's
> own internal regex call sites.
> h2. Motivation
> Groovy makes regex frictionless -- {{=~}}, {{==~}}, {{~/.../}}, 
> {{String#matches}},
> {{replaceAll}}, {{split}}, and the {{Matcher}} sugar -- which also makes 
> ReDoS easy to
> reach when a pattern (or its input) comes from outside the program: scripts, 
> build files,
> web handlers, {{ConfigSlurper}}, templating.
> {{java.util.regex}} has *no native timeout*, so a crafted input against a
> backtracking-prone pattern can hang the calling thread indefinitely. Nothing 
> in Groovy
> guards against this today.
> JLine 4.3.1 shipped a {{SafeRegex}} utility using the well-known 
> {{TimeoutCharSequence}}
> technique to close four CVEs (GHSA-r2xf-8xr9-62gw, GHSA-2v9w-34q6-wpqx,
> GHSA-ph9c-7hw9-vhhw, GHSA-5q95-hrpc-m3w3). We can apply the same idea.
> h2. Proposal
> Bound regex matching by a wall-clock deadline, without a watchdog thread. The 
> input
> {{CharSequence}} is wrapped so that {{charAt()}} checks a deadline and throws 
> a
> {{RegexTimeoutException}} once exceeded; because the regex engine calls 
> {{charAt()}}
> continuously while backtracking, a runaway match is interrupted with no extra 
> thread and
> no {{Thread.interrupt}} plumbing.
> Three deliverables, from lowest-level to most ergonomic:
> * *(a) Internal hardening* -- wrap regex matches against user-supplied input 
> in Groovy's
>   own runtime and tools (templating, {{groovysh}} completion, builders), 
> exactly as JLine
>   did across its modules. Invisible; no public API change.
> * *(b) Runtime helper ({{RegexGuard}})* -- an explicit guarded-match API for 
> user code and
>   for anything the annotation cannot reach:
> {code:groovy}
> RegexGuard.matches(pattern, input, Duration.ofMillis(200))   // -> boolean, 
> throws on timeout
> RegexGuard.matcher(pattern, input, Duration.ofMillis(200))   // -> Matcher 
> over a guarded input
> {code}
> * *(c) Scoped annotation ({{@SafeRegex}})* -- at method / class / package 
> level; an AST
>   transform rewrites the regex match operations lexically within scope to the 
> {{RegexGuard}}
>   path, so no call-site changes are needed. Fits the existing 
> {{@TimedInterrupt}} /
>   {{@ThreadInterrupt}} / {{@ConditionalInterrupt}} family in 
> {{groovy.transform}}.
> {code:groovy}
> @SafeRegex(millis = 200)
> class Handler {
>     boolean check(String input) {
>         input ==~ /(a+)+$/       // rewritten -> 
> RegexGuard.matches(~/(a+)+$/, input, 200ms)
>     }
> }
> {code}
> The guard is on *matching*, not on {{~/.../}} compilation, so the transform 
> rewrites the
> match operations ({{=~}}, {{==~}}, {{String#matches}}, 
> {{replaceAll}}/{{replaceFirst}},
> {{split}}, {{Matcher}} operations), not {{Pattern.compile}}.
> h3. Naming
> || Concern || Name || Notes ||
> | Scoped annotation | {{@SafeRegex}} | in {{groovy.transform}}, alongside 
> {{@TimedInterrupt}} et al. |
> | Runtime helper | {{RegexGuard}} | distinct from the annotation to avoid a 
> clash |
> | Timeout exception | {{RegexTimeoutException}} | unchecked; lets callers 
> distinguish a ReDoS abort from a normal non-match |
> h2. Scope
> *In scope*
> * Runtime deadline guard ({{TimeoutCharSequence}}-style), thread-free
> * Internal hardening of Groovy's own user-input regex sites
> * {{RegexGuard}} helper and {{@SafeRegex}} scoped annotation for user code
> * {{RegexTimeoutException}} type
> *Out of scope / deferred*
> * Making the guard the default for {{=~}} / {{==~}} (behaviour change + 
> overhead)
> * Static ReDoS pattern detection -- a possible future {{RegexChecker}} 
> extension, not this ticket
> * Rewriting the regex engine or adopting a non-backtracking (RE2-style) engine
> h2. Limitations
> * {{@SafeRegex}} rewrites only *lexically-visible* regex ops -- scoped 
> hardening, not
>   whole-program. A regex executed inside a called library method is not 
> rewritten; use
>   {{RegexGuard}} explicitly there.
> * The deadline fires only while the engine is calling {{charAt}} (the 
> backtracking phase) --
>   true of catastrophic cases but not a hard guarantee for every pathological 
> state.
> * Each {{charAt}} pays a small deadline check ({{System.nanoTime}}), so the 
> guard is opt-in /
>   internal-only, never a blanket default.
> * Reduces hang risk; it is not a correctness or a data-at-rest control.
> h2. Relationship to existing features
> || Layer || Mechanism || Status || Covers ReDoS? ||
> | Compile-time validity | {{RegexChecker}} (groovy-typecheckers) | ships | No 
> -- pattern *syntax* only |
> | Compile-time ReDoS lint | possible {{RegexChecker}} extension | idea | 
> Partially |
> | Untrusted-input tracking | GEP-25 {{@Tainted}} -> regex sink | draft | 
> Identifies risky matches |
> | *Runtime guard (this ticket)* | {{RegexGuard}} / {{@SafeRegex}} | *missing* 
> | *Yes -- the actual backstop* |
> {{RegexChecker}} only validates that a literal pattern compiles; it says 
> nothing about
> backtracking. This ticket fills the runtime layer none of the others cover. 
> It also
> composes with GEP-25: a regex applied to a {{@Tainted}} string is a ReDoS 
> sink, and a
> {{RegexGuard}}-guarded (or {{@SafeRegex}}-scoped) match is its runtime 
> sanitizer.
> Here's the section with `GROOVY-12123` slotted in — ready to paste into 
> GROOVY-12122.
> h2. Relates to / composes with GROOVY-12123 (constant regex hoisting)
> GROOVY-12123 hoists effectively-constant regexes to synthetic {{static final 
> Pattern}}
> fields so they compile once. The two tickets touch the same regex AST surface 
> and sit on
> opposite ends of the same operation -- that ticket optimizes *Pattern 
> construction*, this
> one guards *matching* -- so they compose, but the transforms must be 
> ordering-aware:
> * When {{@SafeRegex}} rewrites {{input ==~ /(a+)+$/}} to
>   {{RegexGuard.matches(~/(a+)+$/, input, ...)}}, the hoisting pass must still 
> recognize the
>   {{~/(a+)+$/}} argument as a hoistable constant and lift it to a {{static 
> final}} field.
> * When hoisting runs first and produces a {{static final Pattern P}}, 
> {{@SafeRegex}} must
>   still recognize a match against {{P}} and wrap the *input* in the deadline 
> guard.
> Desired combined result: the constant {{Pattern}} is compiled once into a 
> {{static final}}
> field *and* each match wraps the input in the deadline guard. Whoever 
> implements the second
> of the two should account for the first.
> h2. Licensing / reuse
> JLine is BSD-3 (ASF category A) and already on the {{groovysh}} classpath
> (jline-builtins/jansi), so {{groovysh}} may use JLine's {{SafeRegex}} 
> directly. For *core*
> Groovy the technique is ~20 lines and well-known, so reimplement the idea 
> rather than take
> a core dependency on JLine. Do not copy code; implement from the technique.
> h2. References
> * JLine 4.3.1 release notes -- SafeRegex / TimeoutCharSequence: 
> https://github.com/jline/jline3/releases/tag/4.3.1
> * CWE-1333: Inefficient Regular Expression Complexity: 
> https://cwe.mitre.org/data/definitions/1333.html
> * OWASP -- Regular expression Denial of Service (ReDoS): 
> https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
> * Related: GEP-25 (Data-flow Security -- taint tracking); {{RegexChecker}} in 
> groovy-typecheckers; {{@TimedInterrupt}} / {{@ThreadInterrupt}} / 
> {{@ConditionalInterrupt}} in groovy.transform
> * Relates to: GROOVY-12123 (constant regex hoisting -- compile-once static 
> final Pattern fields): https://issues.apache.org/jira/browse/GROOVY-12123



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to