Skip to content

fix null ref in SqlDataReader#4159

Open
SimonCropp wants to merge 2 commits intodotnet:mainfrom
SimonCropp:fix-null-ref-in-SqlDataReader.GetChars
Open

fix null ref in SqlDataReader#4159
SimonCropp wants to merge 2 commits intodotnet:mainfrom
SimonCropp:fix-null-ref-in-SqlDataReader.GetChars

Conversation

@SimonCropp
Copy link
Copy Markdown
Contributor

The bug: At SqlDataReader.cs:2140, when buffer is null and bufferIndex < 0, the condition (bufferIndex < 0) is true, so execution enters the block. But buffer.Length in the exception constructor throws NullReferenceException since buffer is null.

This only affects the PLP + SequentialAccess path in GetChars. The other GetChars/GetBytes paths at lines 1760 and 1891 don't have the buffer != null && guard, so they'd NRE on the condition itself (different issue, but buffer is expected non-null there).

The fix (SqlDataReader.cs:2140): Changed buffer.Length to buffer?.Length ?? 0 so it reports size 0 when buffer is null.

The test (DataReaderTest.cs): Added GetCharsSequentialAccess_NullBufferNegativeBufferIndex_ThrowsArgumentOutOfRange which calls reader.GetChars(0, 0, null, -1, 0) with CommandBehavior.SequentialAccess on an NVARCHAR(MAX) column and asserts it throws ArgumentOutOfRangeException with paramName: "bufferIndex" — without the fix this throws NullReferenceException instead.

The bug: At SqlDataReader.cs:2140, when buffer is null and bufferIndex < 0, the condition (bufferIndex < 0) is true, so execution enters the block. But buffer.Length in the exception constructor throws NullReferenceException since buffer is null.

This only affects the PLP + SequentialAccess path in GetChars. The other GetChars/GetBytes paths at lines 1760 and 1891 don't have the buffer != null && guard, so they'd NRE on the condition itself (different issue, but buffer is expected non-null there).

The fix (SqlDataReader.cs:2140): Changed buffer.Length to buffer?.Length ?? 0 so it reports size 0 when buffer is null.

The test (DataReaderTest.cs): Added GetCharsSequentialAccess_NullBufferNegativeBufferIndex_ThrowsArgumentOutOfRange which calls reader.GetChars(0, 0, null, -1, 0) with CommandBehavior.SequentialAccess on an NVARCHAR(MAX) column and asserts it throws ArgumentOutOfRangeException with paramName: "bufferIndex" — without the fix this throws NullReferenceException instead.
Copilot AI review requested due to automatic review settings April 8, 2026 03:57
@SimonCropp SimonCropp requested a review from a team as a code owner April 8, 2026 03:57
@github-project-automation github-project-automation bot moved this to To triage in SqlClient Board Apr 8, 2026
@apoorvdeshmukh apoorvdeshmukh self-assigned this Apr 8, 2026
@apoorvdeshmukh apoorvdeshmukh added this to the 7.1.0 milestone Apr 8, 2026
@apoorvdeshmukh
Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 2 pipeline(s).

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a NullReferenceException in SqlDataReader.GetChars when called in the PLP + CommandBehavior.SequentialAccess path with buffer == null and a negative bufferIndex, ensuring the intended ArgumentOutOfRangeException is thrown instead.

Changes:

  • Update SqlDataReader.GetChars (PLP + SequentialAccess) to avoid dereferencing buffer.Length when buffer is null while constructing the argument exception.
  • Add a regression test covering GetChars with SequentialAccess + PLP, buffer == null, and negative bufferIndex.
  • Add additional tests for non-sequential GetChars and GetBytes null-buffer + negative bufferIndex behavior (currently inconsistent with implementation).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs Prevents NRE when building the invalid bufferIndex exception in the PLP + SequentialAccess GetChars path; also adds some redundant null-guards in exception-path validation.
src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs Adds a regression test for the sequential PLP scenario and two additional tests that currently don’t match product behavior when buffer == null.

Comment on lines +769 to +773
using var reader = await command.ExecuteReaderAsync();
Assert.True(await reader.ReadAsync());

var ex = Assert.Throws<ArgumentOutOfRangeException>(() => reader.GetChars(0, 0, null, -1, 4));
Assert.Equal("bufferIndex", ex.ParamName);
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test expects GetChars(..., buffer: null, bufferIndex: -1, ...) to throw, but the current GetChars implementation returns the field length early when buffer is null (see SqlDataReader.GetChars:2205-2209), so bufferIndex is never validated on this path. Either update the test to assert the returned length when buffer is null, or change GetChars to validate bufferIndex even when buffer is null (and ensure behavior is consistent across sequential/non-sequential paths).

Copilot uses AI. Check for mistakes.
Comment on lines +785 to +789
using var reader = await command.ExecuteReaderAsync();
Assert.True(await reader.ReadAsync());

var ex = Assert.Throws<ArgumentOutOfRangeException>(() => reader.GetBytes(0, 0, null, -1, 4));
Assert.Equal("bufferIndex", ex.ParamName);
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test expects GetBytes(..., buffer: null, bufferIndex: -1, ...) to throw, but GetBytes returns the total byte count immediately when buffer is null (see SqlDataReader.TryGetBytesInternal:1728-1738 and 1849-1854). As written, bufferIndex isn't validated on this path, so this assertion will fail unless the product code is changed intentionally.

Copilot uses AI. Check for mistakes.
Comment on lines 1891 to 1899
// if bad buffer index, throw
if (bufferIndex < 0 || bufferIndex >= buffer.Length)
if (bufferIndex < 0 || (buffer != null && bufferIndex >= buffer.Length))
{
throw ADP.InvalidDestinationBufferIndex(buffer.Length, bufferIndex, nameof(bufferIndex));
throw ADP.InvalidDestinationBufferIndex(buffer?.Length ?? 0, bufferIndex, nameof(bufferIndex));
}

// if there is not enough room in the buffer for data
if (cbytes + bufferIndex > buffer.Length)
if (buffer != null && cbytes + bufferIndex > buffer.Length)
{
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this catch block, buffer cannot be null because the method returns early when buffer == null (line 1850). The added buffer != null && guards and buffer?.Length ?? 0 are therefore redundant and make the validation logic harder to follow. Consider simplifying back to direct buffer.Length checks here and keeping the null-handling fix scoped to the PLP + SequentialAccess GetChars path where buffer can actually be null.

Copilot uses AI. Check for mistakes.
Comment on lines 2248 to 2256
// if bad buffer index, throw
if (bufferIndex < 0 || bufferIndex >= buffer.Length)
if (bufferIndex < 0 || (buffer != null && bufferIndex >= buffer.Length))
{
throw ADP.InvalidDestinationBufferIndex(buffer.Length, bufferIndex, nameof(bufferIndex));
throw ADP.InvalidDestinationBufferIndex(buffer?.Length ?? 0, bufferIndex, nameof(bufferIndex));
}

// if there is not enough room in the buffer for data
if (cchars + bufferIndex > buffer.Length)
if (buffer != null && cchars + bufferIndex > buffer.Length)
{
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly here, buffer cannot be null because this code path returns early when buffer == null (line 2205). The null-guards in the exception-path validation are redundant; consider removing them to reduce diff noise and keep the validation logic consistent.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: To triage

Development

Successfully merging this pull request may close these issues.

3 participants