CODE Polyglot / 備忘録

Java の正規表現で ( )* の繰り返しが数千回になると、StackOverflowError で止まる

Java の matches で「[a-z]+(-[a-z]+)*」のような ( ) の繰り返しに長い文字列を渡すと、繰り返しが3,000回ほどでスタックがあふれて止まる。区切りの文字で先に分けて、部分ごとに繰り返しのない正規表現で確かめると直る。

起きたこと

問い合わせフォームのメールアドレス・電話番号の確かめに、[A-Za-z0-9_+-]+(\.[A-Za-z0-9_+-]+)* や [0-9]+(-[0-9]+)* のような正規表現を使っていた。a.a.a.… と「.」で区切った部分を4500個並べた入力を送ると、10言語のうち Java と Kotlin のプログラムだけが StackOverflowError で止まった。Kotlin の Regex は Java の正規表現を使うので、同じように止まる。ほかの8言語(PHP・Python・Go・JavaScript・C#・Rust・Ruby・Swift)は、同じ入力で止まらなかった。

正規表現が遅くなる話(ReDoS)とは違い、時間はかからずにすぐ止まる。

再現の手順

public class Re {
    public static void main(String[] args) {
        int n = Integer.parseInt(args[0]);
        String s = "a" + "-a".repeat(n);
        System.out.println(n + " 回: " + s.matches("[a-z]+(-[a-z]+)*"));
    }
}
$ for n in 1000 2000 3000 5000; do java Re.java $n 2>&1 | head -2; done
1000 回: true
2000 回: true
Exception in thread "main" java.lang.StackOverflowError
	at java.base/java.util.regex.Pattern$BmpCharProperty.match(Pattern.java:4133)
Exception in thread "main" java.lang.StackOverflowError
	at java.base/java.util.regex.Pattern$GroupTail.match(Pattern.java:5000)

Python では、同じ形の正規表現に10万回の繰り返しを渡しても止まらなかった。

$ python3 -c "import re;print(bool(re.fullmatch(r'[a-z]+(-[a-z]+)*', 'a'+'-a'*100000)))"
True

原因

Java の正規表現(java.util.regex)は、( ) を * で繰り返す部分を、繰り返し1回ごとに再帰で調べる。繰り返しが数千回になると、スレッドのスタックの深さの上限を超える。

直し方

区切りの文字で先に分けてから、部分ごとに、( ) の繰り返しを含まない正規表現で確かめる。split の2つ目の引数に -1 を渡して、空の部分(「--」や先頭・最後の「-」)も残し、空なら合わないとする。

boolean ok = true;
for (String part : s.split("-", -1)) ok = ok && part.matches("[a-z]+");
$ java Fix.java 5000
5000 回: true
$ java Fix.java 100000
100000 回: true

java -Xss16m でスタックを大きくすると、5000回は通った。ただし20万回ではまた止まったので、上限が上がるだけで直しにはならない。

$ java -Xss16m Re.java 5000
5000 回: true
$ java -Xss16m Re.java 200000
Exception in thread "main" java.lang.StackOverflowError

確かめたこと

  • この Mac(macOS 26.6.2・arm64・OpenJDK 21.0.12)で、上の出力のとおり。止まり始めたのは2000回と3000回のあいだ
  • 入門の第16回・第17回の Java と Kotlin を、この直し方に書き換えた。長い入力のケースを正解表に足し、この Mac と Linux のコンテナ(毎朝の検証と同じもの)の両方で、10言語とも全件が正解と一致した