From a5e1158c4d2cb18e860621ac005c5c56e6f3e48b Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 5 Jun 2026 02:16:38 +0900 Subject: [PATCH 1/5] Bound extractor regex enumeration for #3053 --- changelog.d/unreleased/3053.security.md | 26 ++++++++++ src/CodeIndex/Indexer/BoundedRegex.cs | 49 +++++++++++++++++++ .../Languages/CobolReferenceExtractor.cs | 4 +- .../Languages/CssReferenceExtractor.cs | 4 +- .../Languages/DartReferenceExtractor.cs | 6 +-- .../Languages/JavaReferenceExtractor.cs | 4 +- .../Languages/PerlReferenceExtractor.cs | 4 +- .../Languages/SqlReferenceExtractor.cs | 6 +-- .../Languages/TerraformReferenceExtractor.cs | 2 +- .../Indexer/References/ReferenceExtractor.cs | 6 +-- .../LanguageReferenceExtractionSupport.cs | 4 +- tests/CodeIndex.Tests/BoundedRegexTests.cs | 11 +++++ .../ReferenceExtractorCssTests.cs | 13 +++++ 13 files changed, 119 insertions(+), 20 deletions(-) create mode 100644 changelog.d/unreleased/3053.security.md diff --git a/changelog.d/unreleased/3053.security.md b/changelog.d/unreleased/3053.security.md new file mode 100644 index 0000000000..797c992c4d --- /dev/null +++ b/changelog.d/unreleased/3053.security.md @@ -0,0 +1,26 @@ +--- +category: security +issues: + - 3053 +affected: + - src/CodeIndex/Indexer/BoundedRegex.cs + - src/CodeIndex/Indexer/References/ReferenceExtractor.cs + - src/CodeIndex/Indexer/References/Support/LanguageReferenceExtractionSupport.cs + - src/CodeIndex/Indexer/References/Languages/CssReferenceExtractor.cs + - src/CodeIndex/Indexer/References/Languages/DartReferenceExtractor.cs + - src/CodeIndex/Indexer/References/Languages/JavaReferenceExtractor.cs + - src/CodeIndex/Indexer/References/Languages/CobolReferenceExtractor.cs + - src/CodeIndex/Indexer/References/Languages/PerlReferenceExtractor.cs + - src/CodeIndex/Indexer/References/Languages/SqlReferenceExtractor.cs + - src/CodeIndex/Indexer/References/Languages/TerraformReferenceExtractor.cs + - tests/CodeIndex.Tests/BoundedRegexTests.cs + - tests/CodeIndex.Tests/ReferenceExtractorCssTests.cs +--- + +## English + +- **Built-in reference extractor regex enumeration is timeout-bounded (#3053)** — shared match enumeration now forces and catches regex timeouts before built-in extractors iterate matches from repository-controlled source text. + +## 日本語 + +- **組み込み reference extractor の正規表現列挙をタイムアウト付きにしました (#3053)** — リポジトリ由来のソース文字列から組み込み extractor が match を列挙する前に、共有の列挙処理で正規表現タイムアウトを強制・捕捉するようにしました。 diff --git a/src/CodeIndex/Indexer/BoundedRegex.cs b/src/CodeIndex/Indexer/BoundedRegex.cs index b606403956..f92dc11fd5 100644 --- a/src/CodeIndex/Indexer/BoundedRegex.cs +++ b/src/CodeIndex/Indexer/BoundedRegex.cs @@ -43,6 +43,18 @@ public BoundedRegex(string pattern, RegexOptions options, TimeSpan matchTimeout) } } + public static BclMatch Match(BclRegex regex, string input) + { + try + { + return regex.Match(input); + } + catch (RegexMatchTimeoutException) + { + return BclMatch.Empty; + } + } + public static new MatchCollection Matches(string input, string pattern) => Matches(input, pattern, RegexOptions.None); @@ -60,6 +72,43 @@ public BoundedRegex(string pattern, RegexOptions options, TimeSpan matchTimeout) } } + public static IEnumerable EnumerateMatches(BclRegex regex, string input) + { + MatchCollection matches; + try + { + matches = regex.Matches(input); + _ = matches.Count; + } + catch (RegexMatchTimeoutException) + { + yield break; + } + + foreach (BclMatch match in matches) + yield return match; + } + + public static IEnumerable EnumerateMatches(string input, string pattern) => + EnumerateMatches(input, pattern, RegexOptions.None); + + public static IEnumerable EnumerateMatches(string input, string pattern, RegexOptions options) + { + MatchCollection matches; + try + { + matches = BclRegex.Matches(input, pattern, options, DefaultMatchTimeout); + _ = matches.Count; + } + catch (RegexMatchTimeoutException) + { + yield break; + } + + foreach (BclMatch match in matches) + yield return match; + } + public static new bool IsMatch(string input, string pattern) => IsMatch(input, pattern, RegexOptions.None); diff --git a/src/CodeIndex/Indexer/References/Languages/CobolReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/CobolReferenceExtractor.cs index 5651a825fd..dc3bb17e8c 100644 --- a/src/CodeIndex/Indexer/References/Languages/CobolReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/CobolReferenceExtractor.cs @@ -165,7 +165,7 @@ public static void Emit( foreach (var pattern in StatementPatterns) EmitMatches(pattern, rawLine, references, seen, fileId, context, lineNumber, container); - foreach (Match match in CobolPerformRegex.Matches(rawLine)) + foreach (Match match in BoundedRegex.EnumerateMatches(CobolPerformRegex, rawLine)) { var endName = match.Groups["end"].Value; if (!string.IsNullOrWhiteSpace(endName) @@ -197,7 +197,7 @@ private static void EmitMatches( int lineNumber, SymbolRecord? container) { - foreach (Match match in pattern.Regex.Matches(rawLine)) + foreach (Match match in BoundedRegex.EnumerateMatches(pattern.Regex, rawLine)) EmitNamedReference(references, seen, fileId, match, pattern.ReferenceKind, context, lineNumber, container); } diff --git a/src/CodeIndex/Indexer/References/Languages/CssReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/CssReferenceExtractor.cs index 3054ad61ae..cdd6162a2c 100644 --- a/src/CodeIndex/Indexer/References/Languages/CssReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/CssReferenceExtractor.cs @@ -159,7 +159,7 @@ private static void EmitMatches( HashSet? definitionNames, SymbolRecord? container) { - foreach (Match match in pattern.Regex.Matches(preparedLine)) + foreach (Match match in BoundedRegex.EnumerateMatches(pattern.Regex, preparedLine)) { var nameGroup = match.Groups["name"]; if (definitionNames != null && definitionNames.Contains(nameGroup.Value)) @@ -443,7 +443,7 @@ private static void EmitCssSelectorMatches( HashSet? definitionNames, SymbolRecord? container) { - foreach (Match match in regex.Matches(selectorPartBody)) + foreach (Match match in BoundedRegex.EnumerateMatches(regex, selectorPartBody)) { var nameGroup = match.Groups["name"]; var prefixIndex = nameGroup.Index - 1; diff --git a/src/CodeIndex/Indexer/References/Languages/DartReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/DartReferenceExtractor.cs index a6532addd9..30754b8c6b 100644 --- a/src/CodeIndex/Indexer/References/Languages/DartReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/DartReferenceExtractor.cs @@ -62,7 +62,7 @@ private static void EmitSpecialReferences( void EmitSingleMatch(Regex regex, string referenceKind) { - var match = regex.Match(preparedLine); + var match = BoundedRegex.Match(regex, preparedLine); if (!match.Success) return; @@ -76,13 +76,13 @@ void EmitMixinReferences() return; var names = match.Groups["names"]; - foreach (Match name in Regex.Matches(names.Value, @"[A-Za-z_]\w*")) + foreach (Match name in BoundedRegex.EnumerateMatches(names.Value, @"[A-Za-z_]\w*")) Add(name.Value, names.Index + name.Index, "mixin_in"); } void EmitNamedConstructorCalls() { - foreach (Match match in NamedConstructorCallRegex.Matches(preparedLine)) + foreach (Match match in BoundedRegex.EnumerateMatches(NamedConstructorCallRegex, preparedLine)) { var name = match.Groups["name"]; Add(name.Value, name.Index, "named_ctor_call"); diff --git a/src/CodeIndex/Indexer/References/Languages/JavaReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/JavaReferenceExtractor.cs index a88e878000..369b9504a6 100644 --- a/src/CodeIndex/Indexer/References/Languages/JavaReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/JavaReferenceExtractor.cs @@ -1137,7 +1137,7 @@ public static void EmitModuleDirectiveReferences( lineNumber, resolveContainerForColumn); - foreach (Match match in ModuleProvidesDirectiveReferenceRegex.Matches(preparedLine)) + foreach (Match match in BoundedRegex.EnumerateMatches(ModuleProvidesDirectiveReferenceRegex, preparedLine)) { var serviceGroup = match.Groups["service"]; ReferenceExtractor.AddTypeReferenceSegment( @@ -1185,7 +1185,7 @@ private static void EmitModuleDirectiveReference( int lineNumber, Func resolveContainerForColumn) { - foreach (Match match in regex.Matches(preparedLine)) + foreach (Match match in BoundedRegex.EnumerateMatches(regex, preparedLine)) { var nameGroup = match.Groups["name"]; ReferenceExtractor.AddTypeReferenceSegment( diff --git a/src/CodeIndex/Indexer/References/Languages/PerlReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/PerlReferenceExtractor.cs index b41a3c8997..54acc53606 100644 --- a/src/CodeIndex/Indexer/References/Languages/PerlReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/PerlReferenceExtractor.cs @@ -191,7 +191,7 @@ private static void AddQuotedModuleReferences( { var args = argsGroup.Value; var argsStart = argsGroup.Index; - foreach (Match moduleMatch in QuotedModuleRegex.Matches(args)) + foreach (Match moduleMatch in BoundedRegex.EnumerateMatches(QuotedModuleRegex, args)) { if (moduleMatch.Groups["name"].Success) { @@ -204,7 +204,7 @@ private static void AddQuotedModuleReferences( var names = namesGroup.Value; var namesStart = argsStart + namesGroup.Index; - foreach (Match nameMatch in Regex.Matches(names, @"[\p{L}_][\w:]*", RegexOptions.CultureInvariant)) + foreach (Match nameMatch in BoundedRegex.EnumerateMatches(names, @"[\p{L}_][\w:]*", RegexOptions.CultureInvariant)) AddBaseModuleReference(nameMatch.Value, namesStart + nameMatch.Index, references, seen, fileId, context, lineNumber, resolveContainerForCall); } } diff --git a/src/CodeIndex/Indexer/References/Languages/SqlReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/SqlReferenceExtractor.cs index d543c119e7..bddb1b27df 100644 --- a/src/CodeIndex/Indexer/References/Languages/SqlReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/SqlReferenceExtractor.cs @@ -2104,7 +2104,7 @@ private static void EmitQualifiedColumnReferences( Func shouldIgnoreName, string referenceKind) { - foreach (Match match in QualifiedColumnReferenceRegex.Matches(text)) + foreach (Match match in BoundedRegex.EnumerateMatches(QualifiedColumnReferenceRegex, text)) { if (IsInsideDoubleQuotedRegion(text, match.Index)) continue; @@ -2159,7 +2159,7 @@ private static void EmitMergeColumnReference( rawIndex += leafIndex; rawName = rawName[leafIndex..].TrimStart(); - var match = Regex.Match( + var match = BoundedRegex.Match( rawName, $"^(?{QuotedIdentifierPattern}|{BareIdentifierPattern})", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); @@ -3121,7 +3121,7 @@ private static bool TryFindDefinitionLeafSpan(string line, string qualifiedName, pattern.Append(escaped); } - var match = Regex.Match(line, pattern.ToString(), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + var match = BoundedRegex.Match(line, pattern.ToString(), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); if (!match.Success) return false; diff --git a/src/CodeIndex/Indexer/References/Languages/TerraformReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/TerraformReferenceExtractor.cs index 766b0e054d..6509922443 100644 --- a/src/CodeIndex/Indexer/References/Languages/TerraformReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/TerraformReferenceExtractor.cs @@ -69,7 +69,7 @@ private static void EmitMatches( HashSet? definitionNames, SymbolRecord? container) { - foreach (Match match in pattern.Regex.Matches(preparedLine)) + foreach (Match match in BoundedRegex.EnumerateMatches(pattern.Regex, preparedLine)) { var nameGroup = match.Groups["name"]; if (definitionNames != null && definitionNames.Contains(nameGroup.Value)) diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.cs index 9a16c38289..24c270d8a8 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.cs @@ -1276,7 +1276,7 @@ private static void EmitCSharpLambdaCaptureReferences( return; } - foreach (Match lambda in CSharpLambdaRegex.Matches(preparedLine)) + foreach (Match lambda in BoundedRegex.EnumerateMatches(CSharpLambdaRegex, preparedLine)) { var body = lambda.Groups["body"].Value; if (string.IsNullOrWhiteSpace(body)) @@ -1308,7 +1308,7 @@ private static void EmitCSharpLambdaCaptureReferences( private static HashSet CollectCSharpLambdaParameterNames(string parameterText) { var names = new HashSet(StringComparer.Ordinal); - foreach (Match match in Regex.Matches(parameterText, CSharpIdentifierPattern)) + foreach (Match match in BoundedRegex.EnumerateMatches(parameterText, CSharpIdentifierPattern)) { var name = NormalizeAtPrefixedIdentifier(match.Value); if (!IsIgnoredCallName("csharp", name)) @@ -1322,7 +1322,7 @@ private static bool ContainsCSharpIdentifier(string text, string name, out int i { index = -1; var normalizedName = NormalizeAtPrefixedIdentifier(name); - foreach (Match match in Regex.Matches(text, CSharpIdentifierPattern)) + foreach (Match match in BoundedRegex.EnumerateMatches(text, CSharpIdentifierPattern)) { if (string.Equals(NormalizeAtPrefixedIdentifier(match.Value), normalizedName, StringComparison.Ordinal)) { diff --git a/src/CodeIndex/Indexer/References/Support/LanguageReferenceExtractionSupport.cs b/src/CodeIndex/Indexer/References/Support/LanguageReferenceExtractionSupport.cs index 1167ea1358..482183df71 100644 --- a/src/CodeIndex/Indexer/References/Support/LanguageReferenceExtractionSupport.cs +++ b/src/CodeIndex/Indexer/References/Support/LanguageReferenceExtractionSupport.cs @@ -1994,7 +1994,7 @@ private static void EmitGoTypeReferences( foreach (var regex in new[] { GoVarTypeRegex, GoFieldTypeRegex, GoTypeAliasRegex }) { - foreach (Match match in regex.Matches(preparedLine)) + foreach (Match match in BoundedRegex.EnumerateMatches(regex, preparedLine)) { var group = match.Groups["type"]; EmitGoTypeExpression(group.Value, group.Index, references, seen, fileId, context, lineNumber, resolveContainerForColumn); @@ -5355,7 +5355,7 @@ private static int LastWhitespaceSeparatedTokenStart(string value) private static IEnumerable EnumerateMatches(Regex regex, string input) { - foreach (Match match in regex.Matches(input)) + foreach (Match match in BoundedRegex.EnumerateMatches(regex, input)) yield return match; } diff --git a/tests/CodeIndex.Tests/BoundedRegexTests.cs b/tests/CodeIndex.Tests/BoundedRegexTests.cs index 13e9550410..e0e53d93c4 100644 --- a/tests/CodeIndex.Tests/BoundedRegexTests.cs +++ b/tests/CodeIndex.Tests/BoundedRegexTests.cs @@ -21,4 +21,15 @@ public void DefaultMatchTimeout_MatchesRuntimeSafetyTimeout() { Assert.Equal(RuntimeSafety.RegexMatchTimeout, BoundedRegex.DefaultMatchTimeout); } + + [Fact] + public void EnumerateMatches_InstanceRegexTimeout_ReturnsEmpty() + { + var regex = new BoundedRegex("(a+)+$", default, TimeSpan.FromMilliseconds(1)); + var input = new string('a', 10_000) + "!"; + + var matches = BoundedRegex.EnumerateMatches(regex, input); + + Assert.Empty(matches); + } } diff --git a/tests/CodeIndex.Tests/ReferenceExtractorCssTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorCssTests.cs index ea828a396c..2df2050e97 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorCssTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorCssTests.cs @@ -146,6 +146,19 @@ public void Extract_Css_MixedSelectorLists_KeepClassReferencesVisible() && reference.ReferenceKind == "reference")); } + [Fact] + public void Extract_Css_LongSelectorLine_UsesBoundedMatchEnumeration() + { + var className = new string('a', 20_000); + var content = $".{className} {{ color: red; }}"; + + var symbols = SymbolExtractor.Extract(1, "css", content); + + var exception = Record.Exception(() => ReferenceExtractor.Extract(1, "css", content, symbols)); + + Assert.Null(exception); + } + [Fact] public void Extract_Css_DescendantSelectors_KeepClassReferencesVisible() { From b78eee7a6e428fba303c610b633b0c86c12804fe Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 5 Jun 2026 02:26:40 +0900 Subject: [PATCH 2/5] Scan TypeScript namespace aliases for #3054 --- changelog.d/unreleased/3054.security.md | 16 +++++++ .../Languages/TypeScriptReferenceExtractor.cs | 43 ++++++++++++++++--- .../ReferenceExtractorTests.cs | 38 ++++++++++++++++ 3 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 changelog.d/unreleased/3054.security.md diff --git a/changelog.d/unreleased/3054.security.md b/changelog.d/unreleased/3054.security.md new file mode 100644 index 0000000000..bf61ec6420 --- /dev/null +++ b/changelog.d/unreleased/3054.security.md @@ -0,0 +1,16 @@ +--- +category: security +issues: + - 3054 +affected: + - src/CodeIndex/Indexer/References/Languages/TypeScriptReferenceExtractor.cs + - tests/CodeIndex.Tests/ReferenceExtractorTests.cs +--- + +## English + +- **TypeScript namespace alias references no longer build per-line dynamic regexes (#3054)** — namespace alias qualified usages are now found with a bounded scanner that preserves identifier boundaries without compiling one regex per alias per line. + +## 日本語 + +- **TypeScript namespace alias 参照で行ごとの動的正規表現を生成しないようにしました (#3054)** — namespace alias の qualified usage は、alias ごと・行ごとの regex を組み立てず、識別子境界を保つ bounded scanner で検出するようになりました。 diff --git a/src/CodeIndex/Indexer/References/Languages/TypeScriptReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/TypeScriptReferenceExtractor.cs index e18138065c..c593391978 100644 --- a/src/CodeIndex/Indexer/References/Languages/TypeScriptReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/TypeScriptReferenceExtractor.cs @@ -1141,24 +1141,57 @@ private static void EmitNamespaceAliasQualifiedReferences( continue; } - foreach (Match match in Regex.Matches( - preparedLine, - $@"(? EnumerateNamespaceAliasQualifiedReferenceStarts(string text, string alias) + { + if (string.IsNullOrEmpty(alias)) + yield break; + + var searchIndex = 0; + while (searchIndex < text.Length) + { + var aliasIndex = text.IndexOf(alias, searchIndex, StringComparison.Ordinal); + if (aliasIndex < 0) + yield break; + + searchIndex = aliasIndex + Math.Max(1, alias.Length); + if (aliasIndex > 0 && IsTypeScriptIdentifierPart(text[aliasIndex - 1])) + continue; + + var afterAlias = aliasIndex + alias.Length; + if (afterAlias < text.Length && IsTypeScriptIdentifierPart(text[afterAlias])) + continue; + + var dotIndex = SkipWhitespace(text, afterAlias); + if (dotIndex >= text.Length || text[dotIndex] != '.') + continue; + + var memberIndex = SkipWhitespace(text, dotIndex + 1); + if (memberIndex >= text.Length || !IsTypeScriptNamespaceMemberStart(text[memberIndex])) + continue; + + yield return aliasIndex; + } + } + + private static bool IsTypeScriptNamespaceMemberStart(char ch) => + ch == '_' || ch == '$' || ch is >= 'A' and <= 'Z' || ch is >= 'a' and <= 'z'; + private static int? FindShadowLine(IReadOnlyList preparedLines, string alias, int bindingLine) { for (var index = bindingLine; index < preparedLines.Count; index++) diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index 8804647c12..81aaace9ea 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -15339,6 +15339,44 @@ export function after() { Assert.Contains(references, r => r.SymbolName == "./public-api" && r.ReferenceKind == "reference" && r.Line == 16); } + [Fact] + public void Extract_TypeScriptNamespaceImportQualifiedUsage_ManyAliasesAcrossManyLines() + { + var imports = string.Join( + "\n", + Enumerable.Range(0, 12).Select(index => $"import * as Api{index} from \"./api{index}\";")); + var calls = string.Join( + "\n", + Enumerable.Range(0, 12).Select(index => $" Api{index}.Client.connect();")); + var content = $$""" + {{imports}} + + export function render() { + {{calls}} + Api1Extra.Client.connect(); + OtherApi1.Client.connect(); + } + """; + + var symbols = SymbolExtractor.Extract(1, "typescript", content); + var references = ReferenceExtractor.Extract(1, "typescript", content, symbols); + + for (var index = 0; index < 12; index++) + { + var expectedLine = 15 + index; + Assert.Contains(references, r => + r.SymbolName == $"./api{index}" + && r.ReferenceKind == "reference" + && r.Line == expectedLine + && r.ContainerName == "render"); + } + + Assert.DoesNotContain(references, r => + r.SymbolName == "./api1" + && r.ReferenceKind == "reference" + && r.Line is 27 or 28); + } + From cb03612105e54ed260ae56148a309e331d1a731f Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 5 Jun 2026 02:32:52 +0900 Subject: [PATCH 3/5] Bound CSharp import alias regexes for #3101 --- changelog.d/unreleased/3101.security.md | 16 ++++++++++++++++ src/CodeIndex/Database/DbWriter.cs | 23 +++++++++++++++++------ tests/CodeIndex.Tests/DatabaseTests.cs | 22 ++++++++++++++++++++++ 3 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 changelog.d/unreleased/3101.security.md diff --git a/changelog.d/unreleased/3101.security.md b/changelog.d/unreleased/3101.security.md new file mode 100644 index 0000000000..948c27e38a --- /dev/null +++ b/changelog.d/unreleased/3101.security.md @@ -0,0 +1,16 @@ +--- +category: security +issues: + - 3101 +affected: + - src/CodeIndex/Database/DbWriter.cs + - tests/CodeIndex.Tests/DatabaseTests.cs +--- + +## English + +- **C# import alias registration now uses timeout-bounded signature regexes (#3101)** — `DbWriter` no longer calls the BCL regex APIs directly when classifying C# `using` signatures from indexed content. + +## 日本語 + +- **C# import alias 登録で timeout 付き signature regex を使うようにしました (#3101)** — `DbWriter` は indexed content 由来の C# `using` signature を分類するとき、BCL の regex API を直接呼び出さなくなりました。 diff --git a/src/CodeIndex/Database/DbWriter.cs b/src/CodeIndex/Database/DbWriter.cs index 3ca347389c..957aad4007 100644 --- a/src/CodeIndex/Database/DbWriter.cs +++ b/src/CodeIndex/Database/DbWriter.cs @@ -3,6 +3,7 @@ using CodeIndex.Indexer; using CodeIndex.Models; using System.Text; +using System.Text.RegularExpressions; namespace CodeIndex.Database; @@ -35,6 +36,18 @@ public class DbWriter private const int DeleteFilesBatchSize = 500; private const int MaxSqlVariables = 999; private const int SqliteConstraintErrorCode = 19; + private static readonly BoundedRegex CSharpExternAliasSignatureRegex = new( + @"^\s*extern\s+alias\b", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly BoundedRegex CSharpGlobalUsingSignatureRegex = new( + @"^\s*global\s+using\b", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly BoundedRegex CSharpUsingStaticSignatureRegex = new( + @"^\s*(?:global\s+)?using\s+static\b", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly BoundedRegex CSharpUsingAliasSignatureRegex = new( + @"^\s*(?:global\s+)?using\s+(?@?\w+)\s*=\s*(?[^;]+?)\s*;", + RegexOptions.Compiled | RegexOptions.CultureInvariant); private int _rowSkipSavepointCounter; private long _batchRowsSkipped; private int _transactionDepth; @@ -2361,14 +2374,14 @@ private static void RegisterCSharpImport(FileImportSet perFile, FileImportSet gl // `extern alias X;` も import 行として現れるがアセンブリ別名でしかなく resolver 側の // qualified 索引には載らないので対象外。 if (signature != null && signature.IndexOf("extern", StringComparison.Ordinal) >= 0 - && System.Text.RegularExpressions.Regex.IsMatch(signature, @"^\s*extern\s+alias\b")) + && CSharpExternAliasSignatureRegex.IsMatch(signature)) { return; } bool isGlobal = signature != null - && System.Text.RegularExpressions.Regex.IsMatch(signature, @"^\s*global\s+using\b"); + && CSharpGlobalUsingSignatureRegex.IsMatch(signature); bool isStatic = signature != null - && System.Text.RegularExpressions.Regex.IsMatch(signature, @"^\s*(?:global\s+)?using\s+static\b"); + && CSharpUsingStaticSignatureRegex.IsMatch(signature); // `using static Foo.Bar;` imports the static members of `Foo.Bar` into the file's // scope — NOT a namespace that a base clause `class X : Base` could pull from. // Drop it so we don't confuse the alias/namespace paths. @@ -2384,9 +2397,7 @@ private static void RegisterCSharpImport(FileImportSet perFile, FileImportSet gl // the alias enters the per-file map. // SymbolExtractor 側と同じく verbatim 識別子も `@?\w+` で受け、下の正規化で // 先頭 `@` を剥がしてから alias map に載せる。 - var m = System.Text.RegularExpressions.Regex.Match( - signature, - @"^\s*(?:global\s+)?using\s+(?@?\w+)\s*=\s*(?[^;]+?)\s*;"); + var m = CSharpUsingAliasSignatureRegex.Match(signature); if (m.Success) { aliasName = m.Groups["alias"].Value.Trim(); diff --git a/tests/CodeIndex.Tests/DatabaseTests.cs b/tests/CodeIndex.Tests/DatabaseTests.cs index 2b48550725..2ee6408da5 100644 --- a/tests/CodeIndex.Tests/DatabaseTests.cs +++ b/tests/CodeIndex.Tests/DatabaseTests.cs @@ -26,6 +26,28 @@ public DatabaseTests() _writer = new DbWriter(_db.Connection); } + [Fact] + public void InsertSymbols_CSharpLongMalformedUsingAliasSignature_DoesNotThrow() + { + var fileId = UpsertTestFile("src/Alias.cs", checksum: "alias"); + var signature = "using Alias = " + new string('A', 50_000); + + var exception = Record.Exception(() => _writer.InsertSymbols([ + new SymbolRecord + { + FileId = fileId, + Kind = "import", + Name = "Alias", + Line = 1, + StartLine = 1, + EndLine = 1, + Signature = signature, + }, + ])); + + Assert.Null(exception); + } + [Fact] public void Search_ExactSymbolBoostPrefersChunkContainingSymbol_Issue1977() { From 1e79a368550e0d4499fc88545f7c418e2f9de829 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 5 Jun 2026 02:39:11 +0900 Subject: [PATCH 4/5] Cap Python logical remap buffers for #3141 --- changelog.d/unreleased/3141.security.md | 16 +++++++ .../Indexer/References/ReferenceExtractor.cs | 44 ++++++++++++++----- .../ReferenceExtractorPythonTests.cs | 24 ++++++++++ 3 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 changelog.d/unreleased/3141.security.md diff --git a/changelog.d/unreleased/3141.security.md b/changelog.d/unreleased/3141.security.md new file mode 100644 index 0000000000..ab6c1760ba --- /dev/null +++ b/changelog.d/unreleased/3141.security.md @@ -0,0 +1,16 @@ +--- +category: security +issues: + - 3141 +affected: + - src/CodeIndex/Indexer/References/ReferenceExtractor.cs + - tests/CodeIndex.Tests/ReferenceExtractorPythonTests.cs +--- + +## English + +- **Python logical reference remapping now caps header and statement maps (#3141)** — oversized multiline Python headers or continuation statements now skip logical remapping instead of growing unbounded text and line/column arrays. + +## 日本語 + +- **Python logical reference remap の header / statement map に上限を設けました (#3141)** — 巨大な複数行 header や continuation statement では、text と line/column 配列を無制限に増やさず logical remap をスキップします。 diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.cs index 24c270d8a8..7f1c9923ef 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.cs @@ -1417,6 +1417,8 @@ private static string NormalizeReferenceCycleName(string name) return colon >= 0 && colon + 2 < trimmed.Length ? trimmed[(colon + 2)..] : trimmed; } + private const int MaxPythonLogicalReferenceLineLength = 32_768; + private readonly record struct PythonLogicalHeaderReferenceLine(string Text, int[] PhysicalLines, int[] PhysicalColumns); private static bool TryBuildPythonLogicalHeaderReferenceLine( @@ -1442,9 +1444,8 @@ private static bool TryBuildPythonLogicalHeaderReferenceLine( { if (builder.Length > 0) { - builder.Append(' '); - physicalLines.Add(lineIndex); - physicalColumns.Add(column); + if (!TryAppendPythonLogicalReferenceChar(builder, physicalLines, physicalColumns, ' ', lineIndex, column, out header)) + return false; } for (var fragmentColumn = column; fragmentColumn < fragmentEndColumn; fragmentColumn++) @@ -1453,9 +1454,8 @@ private static bool TryBuildPythonLogicalHeaderReferenceLine( if (fragmentChar == '\\' && fragmentColumn == fragmentEndColumn - 1) break; - builder.Append(fragmentChar); - physicalLines.Add(lineIndex); - physicalColumns.Add(fragmentColumn); + if (!TryAppendPythonLogicalReferenceChar(builder, physicalLines, physicalColumns, fragmentChar, lineIndex, fragmentColumn, out header)) + return false; } } @@ -1530,9 +1530,8 @@ private static bool TryBuildPythonLogicalStatementReferenceLine( { if (builder.Length > 0) { - builder.Append(' '); - physicalLines.Add(lineIndex); - physicalColumns.Add(column); + if (!TryAppendPythonLogicalReferenceChar(builder, physicalLines, physicalColumns, ' ', lineIndex, column, out header)) + return false; } for (var fragmentColumn = column; fragmentColumn < fragmentEndColumn; fragmentColumn++) @@ -1541,9 +1540,8 @@ private static bool TryBuildPythonLogicalStatementReferenceLine( if (fragmentChar == '\\' && fragmentColumn == fragmentEndColumn - 1) break; - builder.Append(fragmentChar); - physicalLines.Add(lineIndex); - physicalColumns.Add(fragmentColumn); + if (!TryAppendPythonLogicalReferenceChar(builder, physicalLines, physicalColumns, fragmentChar, lineIndex, fragmentColumn, out header)) + return false; } } @@ -1590,6 +1588,28 @@ private static bool TryBuildPythonLogicalStatementReferenceLine( return header.Text.Length > 0; } + private static bool TryAppendPythonLogicalReferenceChar( + StringBuilder builder, + List physicalLines, + List physicalColumns, + char value, + int physicalLine, + int physicalColumn, + out PythonLogicalHeaderReferenceLine header) + { + if (builder.Length >= MaxPythonLogicalReferenceLineLength) + { + header = default; + return false; + } + + builder.Append(value); + physicalLines.Add(physicalLine); + physicalColumns.Add(physicalColumn); + header = default; + return true; + } + private static int FindPythonCommentColumn(string line, int startColumn) { var inString = false; diff --git a/tests/CodeIndex.Tests/ReferenceExtractorPythonTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorPythonTests.cs index fde36800a3..2726365085 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorPythonTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorPythonTests.cs @@ -457,6 +457,30 @@ def build( && reference.Line == 4); } + [Fact] + public void Extract_PythonOversizedLogicalHeaderAndStatement_DoesNotThrow() + { + var longTypeName = new string('A', 40_000); + var longValueName = new string('B', 40_000); + var content = $$""" + def build( + value: {{longTypeName}}, + ): + result = ( + {{longValueName}} + ) + return result + """; + + var exception = Record.Exception(() => + { + var symbols = SymbolExtractor.Extract(1, "python", content); + ReferenceExtractor.Extract(1, "python", content, symbols); + }); + + Assert.Null(exception); + } + [Fact] public void Extract_PythonClassHook_AssignsReferencesToHookContainer() { From 685b2258344e6a5091723ed83ea2b5dec7b66073 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 5 Jun 2026 03:29:22 +0900 Subject: [PATCH 5/5] Cover BoundedRegex instance timeouts for #3053 --- tests/CodeIndex.Tests/BoundedRegexTests.cs | 33 ++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/CodeIndex.Tests/BoundedRegexTests.cs b/tests/CodeIndex.Tests/BoundedRegexTests.cs index e0e53d93c4..7ee6666f26 100644 --- a/tests/CodeIndex.Tests/BoundedRegexTests.cs +++ b/tests/CodeIndex.Tests/BoundedRegexTests.cs @@ -32,4 +32,37 @@ public void EnumerateMatches_InstanceRegexTimeout_ReturnsEmpty() Assert.Empty(matches); } + + [Fact] + public void InstanceMatch_Timeout_ReturnsEmpty() + { + var regex = new BoundedRegex("(a+)+$", default, TimeSpan.FromMilliseconds(1)); + var input = new string('a', 10_000) + "!"; + + var match = regex.Match(input); + + Assert.False(match.Success); + } + + [Fact] + public void InstanceMatches_Timeout_ReturnsEmpty() + { + var regex = new BoundedRegex("(a+)+$", default, TimeSpan.FromMilliseconds(1)); + var input = new string('a', 10_000) + "!"; + + var matches = regex.Matches(input); + + Assert.Empty(matches); + } + + [Fact] + public void InstanceIsMatch_Timeout_ReturnsFalse() + { + var regex = new BoundedRegex("(a+)+$", default, TimeSpan.FromMilliseconds(1)); + var input = new string('a', 10_000) + "!"; + + var isMatch = regex.IsMatch(input); + + Assert.False(isMatch); + } }