From cd0e8debf5a54a671ed5571c81a9d400e6d3f39f Mon Sep 17 00:00:00 2001 From: martincostello Date: Thu, 16 Apr 2026 14:23:25 +0100 Subject: [PATCH 1/3] [SqlClient] Fix IndexOutOfRangeException Fix `IndexOutOfRangeException` if trailing whitespace in an SQL statement aligns with an `ArrayPool` bucket boundary. --- .../CHANGELOG.md | 3 + .../CHANGELOG.md | 3 + src/Shared/SqlProcessor.cs | 66 ++++++++++++++++--- .../SqlProcessorTests.cs | 15 +++++ .../SqlProcessorTests.cs | 11 ++++ 5 files changed, 88 insertions(+), 10 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/CHANGELOG.md index 05797ddeab..12b76c1047 100644 --- a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/CHANGELOG.md @@ -5,6 +5,9 @@ * Updated OpenTelemetry core component version(s) to `1.15.2`. ([#4080](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4080)) +* Fix `IndexOutOfRangeException` when parsing SQL statements. + ([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/TODO)) + ## 1.15.0-beta.1 Released 2026-Jan-21 diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md index b2e2f3728c..71bc1bad7b 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md @@ -5,6 +5,9 @@ * Updated OpenTelemetry core component version(s) to `1.15.2`. ([#4080](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4080)) +* Fix `IndexOutOfRangeException` when parsing SQL statements. + ([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/TODO)) + ## 1.15.1 Released 2026-Mar-04 diff --git a/src/Shared/SqlProcessor.cs b/src/Shared/SqlProcessor.cs index 09928e9653..5bce9757d7 100644 --- a/src/Shared/SqlProcessor.cs +++ b/src/Shared/SqlProcessor.cs @@ -306,12 +306,59 @@ private static SqlStatementInfo SanitizeSql(string sql) sanitizedSql, summary.Slice(0, summaryLength).ToString()); + if (state.RentedSummaryBuffer != null) + { + ArrayPool.Shared.Return(state.RentedSummaryBuffer); + } + // We don't clear the buffer as we know the content has been sanitized ArrayPool.Shared.Return(rentedBuffer); return sqlStatementInfo; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AppendSummaryChar(char value, ref ParseState state) + { + EnsureSummaryCapacity(checked(state.SummaryPosition + 1), ref state); + + state.SummaryBuffer[state.SummaryPosition++] = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AppendSummaryToken(ReadOnlySpan value, ref ParseState state) + { + EnsureSummaryCapacity(checked(state.SummaryPosition + value.Length), ref state); + + value.CopyTo(state.SummaryBuffer.Slice(state.SummaryPosition)); + + state.SummaryPosition += value.Length; + } + + private static void EnsureSummaryCapacity(int requiredCapacity, ref ParseState state) + { + if (requiredCapacity <= state.SummaryBuffer.Length) + { + return; + } + + var doubledCapacity = state.SummaryBuffer.Length <= (int.MaxValue / 2) + ? state.SummaryBuffer.Length * 2 + : int.MaxValue; + + var newBuffer = ArrayPool.Shared.Rent(Math.Max(requiredCapacity, doubledCapacity)); + + state.SummaryBuffer.Slice(0, state.SummaryPosition).CopyTo(newBuffer); + + if (state.RentedSummaryBuffer != null) + { + ArrayPool.Shared.Return(state.RentedSummaryBuffer); + } + + state.RentedSummaryBuffer = newBuffer; + state.SummaryBuffer = newBuffer.AsSpan(); + } + private static void ParseNextTokenFast( ReadOnlySpan sql, Span buffer, @@ -436,11 +483,10 @@ private static void ParseNextToken( state.FirstSummaryKeyword = potentialKeywordInfo.SqlKeyword; } - sql.Slice(start, keywordLength).CopyTo(state.SummaryBuffer.Slice(state.SummaryPosition)); - state.SummaryPosition += keywordLength; + AppendSummaryToken(sql.Slice(start, keywordLength), ref state); // Add a space after the keyword. The trailing space will be trimmed later. - state.SummaryBuffer[state.SummaryPosition++] = ' '; + AppendSummaryChar(SpaceChar, ref state); state.PreviousSummaryKeyword = potentialKeywordInfo.SqlKeyword; } @@ -521,11 +567,10 @@ private static void ParseNextToken( // Optionally copy to summary buffer. if (state.CaptureNextNonKeywordTokenAsIdentifier) { - sql.Slice(start, length).CopyTo(state.SummaryBuffer.Slice(state.SummaryPosition)); - state.SummaryPosition += length; + AppendSummaryToken(sql.Slice(start, length), ref state); // Add a space after the identifier. The trailing space will be trimmed later. - state.SummaryBuffer[state.SummaryPosition++] = SpaceChar; + AppendSummaryChar(SpaceChar, ref state); } } @@ -548,16 +593,16 @@ private static void ParseNextToken( // Remove the space we added after the identifier in the summary buffer before we write the closing bracket. state.SummaryPosition--; - state.SummaryBuffer[state.SummaryPosition++] = CloseSquareBracketChar; + AppendSummaryChar(CloseSquareBracketChar, ref state); var nextPos = state.ParsePosition + 1; if (nextPos >= sql.Length || sql[nextPos] != DotChar) { - state.SummaryBuffer[state.SummaryPosition++] = SpaceChar; + AppendSummaryChar(SpaceChar, ref state); } else { - state.SummaryBuffer[state.SummaryPosition++] = DotChar; // write the dot to summary + AppendSummaryChar(DotChar, ref state); // write the dot to summary } } @@ -569,7 +614,7 @@ private static void ParseNextToken( if (state.CaptureNextNonKeywordTokenAsIdentifier && currentChar is OpenSquareBracketChar) { state.InEscapedIdentifier = true; - state.SummaryBuffer[state.SummaryPosition++] = OpenSquareBracketChar; + AppendSummaryChar(OpenSquareBracketChar, ref state); } buffer[state.SanitizedPosition++] = currentChar; @@ -908,6 +953,7 @@ private ref struct ParseState // Stored in state to avoid slicing repeatedly. public Span SummaryBuffer; + public char[]? RentedSummaryBuffer; /// /// Will be set if a keyword has been matched by the parser. diff --git a/test/OpenTelemetry.Contrib.Shared.FuzzTests/SqlProcessorTests.cs b/test/OpenTelemetry.Contrib.Shared.FuzzTests/SqlProcessorTests.cs index 41fc64fa50..a7d3f28d6e 100644 --- a/test/OpenTelemetry.Contrib.Shared.FuzzTests/SqlProcessorTests.cs +++ b/test/OpenTelemetry.Contrib.Shared.FuzzTests/SqlProcessorTests.cs @@ -152,6 +152,21 @@ public static void GetSanitizedSql_Summary_Length_Limited(NonEmptyString input) Assert.True(actual.DbQuerySummary.Length <= 255); } + [Property(MaxTest = MaxValue)] + public static void GetSanitizedSql_SqlStatement_AlignedToArrayPoolBuckets_DoesNotThrow(PositiveInt seed) + { + // Choose statement lengths that align with common ArrayPool buckets. Before the fix, + // these lengths can make the initial summary slice exactly as long as the SQL text. + var sqlLength = 1 << ((seed.Get % 5) + 4); + var prefix = "CREATE TABLE "; + var identifier = new string('X', sqlLength - prefix.Length); + var sql = prefix + identifier; + + var exception = Record.Exception(() => SqlProcessor.GetSanitizedSql(sql)); + + Assert.Null(exception); + } + [Property(MaxTest = MaxValue)] public static void GetSanitizedSql_In_Clause_Optimizes_Sanitization(PositiveInt input) { diff --git a/test/OpenTelemetry.Contrib.Shared.Tests/SqlProcessorTests.cs b/test/OpenTelemetry.Contrib.Shared.Tests/SqlProcessorTests.cs index 53e7baa07b..9832c15742 100644 --- a/test/OpenTelemetry.Contrib.Shared.Tests/SqlProcessorTests.cs +++ b/test/OpenTelemetry.Contrib.Shared.Tests/SqlProcessorTests.cs @@ -17,6 +17,17 @@ public SqlProcessorTests(ITestOutputHelper output) public static TheoryData TestData => SqlProcessorTestCases.GetSemanticConventionsTestCases(); + [Fact] + public void GetSanitizedSql_CreateTableWithTrailingIdentifier_DoesNotThrow() + { + var sql = "CREATE TABLE XXX"; + + var sqlStatementInfo = SqlProcessor.GetSanitizedSql(sql); + + Assert.Equal(sql, sqlStatementInfo.SanitizedSql); + Assert.Equal(sql, sqlStatementInfo.DbQuerySummary); + } + [SkippableTheory] [MemberData(nameof(TestData))] public void TestGetSanitizedSql(SqlProcessorTestCases.TestCase testCase) From dfc6fc993e975b29bb5efbe9fb2c975471026f37 Mon Sep 17 00:00:00 2001 From: martincostello Date: Thu, 16 Apr 2026 14:38:29 +0100 Subject: [PATCH 2/3] [Redis] Remove unused code Remove unused code related to parsing SQL queries from OpenTelemetry.Instrumentation.StackExchangeRedis. --- ...OpenTelemetry.Instrumentation.EntityFrameworkCore.csproj | 4 ++++ .../OpenTelemetry.Instrumentation.SqlClient.csproj | 4 ++++ .../OpenTelemetry.Instrumentation.StackExchangeRedis.csproj | 2 -- src/Shared/DatabaseSemanticConventionHelper.cs | 6 ++++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/OpenTelemetry.Instrumentation.EntityFrameworkCore.csproj b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/OpenTelemetry.Instrumentation.EntityFrameworkCore.csproj index f7992f5958..9ba0b97f78 100644 --- a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/OpenTelemetry.Instrumentation.EntityFrameworkCore.csproj +++ b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/OpenTelemetry.Instrumentation.EntityFrameworkCore.csproj @@ -19,6 +19,10 @@ + + $(DefineConstants);INCLUDE_SQL_QUERY_PARSER + + diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/OpenTelemetry.Instrumentation.SqlClient.csproj b/src/OpenTelemetry.Instrumentation.SqlClient/OpenTelemetry.Instrumentation.SqlClient.csproj index e0d9013457..2f668e2cdb 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/OpenTelemetry.Instrumentation.SqlClient.csproj +++ b/src/OpenTelemetry.Instrumentation.SqlClient/OpenTelemetry.Instrumentation.SqlClient.csproj @@ -15,6 +15,10 @@ false + + $(DefineConstants);INCLUDE_SQL_QUERY_PARSER + + diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/OpenTelemetry.Instrumentation.StackExchangeRedis.csproj b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/OpenTelemetry.Instrumentation.StackExchangeRedis.csproj index 19135ec605..bfd6fb23b5 100644 --- a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/OpenTelemetry.Instrumentation.StackExchangeRedis.csproj +++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/OpenTelemetry.Instrumentation.StackExchangeRedis.csproj @@ -25,8 +25,6 @@ - - diff --git a/src/Shared/DatabaseSemanticConventionHelper.cs b/src/Shared/DatabaseSemanticConventionHelper.cs index 1722f2cedd..6c40334b9b 100644 --- a/src/Shared/DatabaseSemanticConventionHelper.cs +++ b/src/Shared/DatabaseSemanticConventionHelper.cs @@ -1,11 +1,15 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#if INCLUDE_SQL_QUERY_PARSER using System.Diagnostics; +#endif using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Configuration; +#if INCLUDE_SQL_QUERY_PARSER using OpenTelemetry.Instrumentation; using OpenTelemetry.Trace; +#endif namespace OpenTelemetry.Internal; @@ -59,6 +63,7 @@ public static DatabaseSemanticConvention GetSemanticConventionOptIn(IConfigurati return DatabaseSemanticConvention.Old; } +#if INCLUDE_SQL_QUERY_PARSER public static void ApplyConventionsForQueryText( Activity activity, string? commandText, @@ -151,6 +156,7 @@ public static void AddTagsForSamplingAndUpdateActivityNameForStoredProcedure( tagsList.Add(SemanticConventions.AttributeDbQuerySummary, dbQuerySummary); activityName = dbQuerySummary; } +#endif private static bool TryGetConfiguredValues(IConfiguration configuration, [NotNullWhen(true)] out HashSet? values) { From 3893ebc2fe2fb2f6bae3183f046a0606d6d01e86 Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Thu, 16 Apr 2026 15:41:37 +0100 Subject: [PATCH 3/3] [SqlClient] Update CHANGELOGs Add PR number. --- .../CHANGELOG.md | 2 +- src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/CHANGELOG.md index 12b76c1047..b36b4f5045 100644 --- a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/CHANGELOG.md @@ -6,7 +6,7 @@ ([#4080](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4080)) * Fix `IndexOutOfRangeException` when parsing SQL statements. - ([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/TODO)) + ([#4139](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4139)) ## 1.15.0-beta.1 diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md index 71bc1bad7b..072b82d39e 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md @@ -6,7 +6,7 @@ ([#4080](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4080)) * Fix `IndexOutOfRangeException` when parsing SQL statements. - ([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/TODO)) + ([#4139](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4139)) ## 1.15.1