Conversation
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.
|
/azp run |
|
Azure Pipelines successfully started running 2 pipeline(s). |
There was a problem hiding this comment.
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 dereferencingbuffer.Lengthwhenbufferis null while constructing the argument exception. - Add a regression test covering
GetCharswith SequentialAccess + PLP,buffer == null, and negativebufferIndex. - Add additional tests for non-sequential
GetCharsandGetBytesnull-buffer + negativebufferIndexbehavior (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. |
| 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); |
There was a problem hiding this comment.
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).
| 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); |
There was a problem hiding this comment.
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.
| // 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) | ||
| { |
There was a problem hiding this comment.
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.
| // 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) | ||
| { |
There was a problem hiding this comment.
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.
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.