Skip to content

fix: Saturate NativeBuffer growth instead of overflowing past half of int.MaxValue - #419

Merged
CurtHagenlocher merged 3 commits into
apache:mainfrom
CurtHagenlocher:fix/native-buffer-grow-overflow
Aug 22, 2026
Merged

fix: Saturate NativeBuffer growth instead of overflowing past half of int.MaxValue#419
CurtHagenlocher merged 3 commits into
apache:mainfrom
CurtHagenlocher:fix/native-buffer-grow-overflow

Conversation

@CurtHagenlocher

Copy link
Copy Markdown
Contributor

What's Changed

Grow doubled the current length in a checked context without saturating:

int newCount = Math.Max(newElementCount, checked(Length * 2));

So once a buffer passed half of the addressable maximum, its next grow threw OverflowException
however small the requested increase, and even though the requested size still fit. For a
NativeBuffer<byte, …> that is a hard ceiling near 1 GiB, with no way for a caller to work around it:
asking for a smaller increment does not help, because the overflow is in the doubling rather than in
the request.

Growth now saturates at the largest addressable element count, so it stays amortised right up to the
ceiling. A request that genuinely cannot be addressed still fails at the byte-size calculation, as it
did before — behaviour is unchanged for anything that could not have worked.

This is what the TODO those lines carried proposed:

There might be a size that's big enough to work for this case but not too big to overflow. We could
use that instead of blindly doubling.

On testing it

Reaching the boundary through Grow means allocating more than a gigabyte, which does not belong in
a unit test. The count arithmetic is extracted to ComputeGrowCount so the boundary can be tested
directly and exhaustively, including the per-element-size ceiling — the limit is a byte count, so a
wider element type saturates at proportionally fewer elements.

Verified the new tests fail against the previous arithmetic before fixing it: with checked(Length * 2) restored, ComputeGrowCountSaturatesInsteadOfOverflowing and
ComputeGrowCountSaturatesPerElementSize both fail; the rest pass either way.

Apache.Arrow.Tests is green on net8.0: 1870 passed, 28 skipped (the Python interop cases).

Scope

This does not change the 2 GiB ceiling on ArrowBuffer itself (ReadOnlyMemory<byte>, int Length)
— it only stops buffers failing at half of it.

Closes #418.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes NativeBuffer<TItem, TTracker>.Grow so its exponential (2x) growth strategy saturates at the largest addressable element count instead of throwing an OverflowException once the buffer length exceeds int.MaxValue / 2. It keeps amortized growth behavior up to the existing size ceiling, while preserving the prior failure mode for genuinely unaddressable requests (overflow at the byte-size calculation).

Changes:

  • Refactors growth count arithmetic into NativeBuffer<,>.ComputeGrowCount(...) to avoid overflow and saturate at the maximum addressable element count.
  • Updates Grow to use the new saturating arithmetic before computing byte size in a checked context.
  • Adds focused unit tests for boundary behavior without allocating multi-gigabyte buffers.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/Apache.Arrow/Memory/NativeBuffer.cs Introduces saturating growth-count calculation and updates Grow to use it.
test/Apache.Arrow.Tests/NativeBufferTests.cs Adds unit tests covering doubling behavior, saturation near limits, and per-element-size ceilings.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +120 to +125
internal static int ComputeGrowCount(int length, int newElementCount, int elementSize)
{
int maxCount = int.MaxValue / elementSize;
long doubled = (long)length * 2;
return (int)Math.Max(newElementCount, Math.Min(doubled, maxCount));
}
Comment thread src/Apache.Arrow/Memory/NativeBuffer.cs
Comment thread src/Apache.Arrow/Memory/NativeBuffer.cs Outdated
/// past the largest buffer that can be addressed, and never below what the caller asked for.
/// </summary>
/// <remarks>
/// Doubling used to be unconditional and checked, so once the buffer passed half of the maximum

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Okay, that's just a dumb comment. I should have told the AI not to submit the PR until after I'd reviewed it.

@CurtHagenlocher

Copy link
Copy Markdown
Contributor Author

The failing Integration check looks pre-existing rather than related to this change: the same job
fails on main at c5a82e5, the commit this branch is based on
(run). It fails in Execute Docker
Build
in the Archery cross-language harness, which this change does not touch.

All the C# checks are green — Lint, Source, macos-latest, ubuntu-latest, windows-latest,
Verify (8.0.x) on all three, Package, Documentation and Upload.

Happy to rebase if main goes green and you would like a clean run.

CurtHagenlocher and others added 3 commits August 22, 2026 07:24
… int.MaxValue

Grow doubled the current length in a checked context without saturating, so once a buffer passed half
of the addressable maximum its next grow threw OverflowException however small the requested increase
and even though the requested size still fit. For a byte buffer that was a hard ceiling near 1 GiB.
The TODO those lines carried described exactly this.

Growth now saturates at the largest addressable element count, keeping it amortised right up to the
ceiling. A request that genuinely cannot be addressed still fails at the byte-size calculation, as
before, so behaviour is unchanged for anything that could not have worked.

The count arithmetic is extracted so it can be tested at the boundary without allocating more than a
gigabyte in a unit test, including the per-element-size ceiling: the limit is a byte count, so a
wider element type saturates at proportionally fewer elements.

Closes apache#418.
Addresses review feedback on apache#419. `elementSize` is only ever
`Unsafe.SizeOf<TItem>()` for an unmanaged `TItem`, so it cannot be zero or
negative and the division cannot fault; the parameter exists so the boundary
can be tested without allocating a buffer of that size. A `Debug.Assert` states
that invariant for callers of the internal helper without adding a runtime
check to the growth path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…remarks

Grow already throws ObjectDisposedException on entry, so re-checking _owner
before Reallocate was dead code. The <remarks> block narrated the history of the
bug being fixed, which belongs in the commit message and the pull request rather
than in the API documentation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@CurtHagenlocher
CurtHagenlocher force-pushed the fix/native-buffer-grow-overflow branch from 62effc1 to bedad88 Compare August 22, 2026 14:34
@CurtHagenlocher
CurtHagenlocher merged commit db7f1e4 into apache:main Aug 22, 2026
14 checks passed
@CurtHagenlocher
CurtHagenlocher deleted the fix/native-buffer-grow-overflow branch August 22, 2026 15:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NativeBuffer.Grow throws OverflowException instead of growing once the buffer passes half of int.MaxValue

2 participants