Skip to content

Fix ArrayOf.ToArrayOf(IEnumerable<T>) overrun when the source's count is stale - #4565

Closed
biancode wants to merge 2 commits into
OPCFoundation:masterfrom
biancode:spec/arrayof-concurrent-count-race
Closed

biancode wants to merge 2 commits into
OPCFoundation:masterfrom
biancode:spec/arrayof-concurrent-count-race

Conversation

@biancode

@biancode biancode commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Description

The NET8_0_OR_GREATER fast path of ToArrayOf<T>(IEnumerable<T>) sizes its buffer from
TryGetNonEnumeratedCount and then fills it with a foreach. The count read and the enumeration
are not atomic, so a source that changes size between them — a ConcurrentQueue<T> a producer is
still enqueuing to is the concrete case here — makes the copy overrun the buffer
(IndexOutOfRangeException) or, if the source shrank, return an array padded with default(T).

Found through a fork CI failure of SlowActivationRecoveryBelowDeadlineAvoidsOuterPolicyAsync,
which reads ManagedSessionReconnectHarness.Requests — a ConcurrentQueue<IServiceRequest>.ToArrayOf()
— while a scripted transport on another thread is still enqueuing.

Changes:

  • ToArrayOf<T>(IEnumerable<T>?): grow the buffer (doubling, minimum 4) instead of indexing past
    it, and trim it to the actual enumerated length before returning.
  • Two deterministic tests reproducing both directions with a type mirroring
    ConcurrentQueue<T>'s own interfaces (ICollection + IReadOnlyCollection<T>) whose Count
    disagrees with its enumeration, which is exactly what TryGetNonEnumeratedCount takes at face
    value. Both fail on master (one throws, one returns wrong data).

Related Issues

Checklist

  • I have signed the CLA and read the CONTRIBUTING doc.
  • I have added tests that prove my fix is effective or that my feature works and increased code coverage.
  • I have added all necessary documentation.
  • I have verified that my changes do not introduce (new) build or analyzer warnings.
  • I ran all tests locally using the UA.slnx solution against at least .net framework and .net 10, and all passed.
  • I fixed all failing and flaky tests in the CI pipelines and all CodeQL warnings.
  • I have addressed all PR feedback received.

Test evidence:

Full disclosure, AI tooling (Claude Code) was used to investigate this issue, implement the fix and tests, and validate the build/format/tests locally and on the fork CI before opening this PR.

🤖 Generated with Claude Code

…g source

The NET8_0_OR_GREATER fast path reads TryGetNonEnumeratedCount, then
copies into a T[count] buffer. The count is a live Count read (e.g. a
ConcurrentQueue<T> a producer enqueues to), so it is not atomic with
the foreach below: a source that grows after the count check indexes
past the buffer with IndexOutOfRangeException, and one that shrinks
returns an array padded with default(T) at the tail instead of its
actual length.

Found via a failing fork CI run of a managed-session reconnect test
(ManagedSessionReconnectHarness.Requests snapshotting a
ConcurrentQueue<IServiceRequest> shared with the scripted transport).

Grow the buffer instead of indexing past it, and trim it if the source
enumerated shorter than its reported count. Reproduced deterministically
with an ICollection<T> whose Count does not match its enumeration
(TryGetNonEnumeratedCount only takes the fast path for ICollection<T>,
not the read-only interface).
…double

ConcurrentQueue<T> implements the non-generic ICollection and
IReadOnlyCollection<T>, not the generic ICollection<T>. The double
implemented ICollection<T> instead, which made the net48/net472 LINQ
fallback (Enumerable.ToArray, used outside NET8_0_OR_GREATER) call its
CopyTo(T[], int) and throw, failing the test for the wrong reason on
those frameworks (confirmed on the fork CI's windows-net48 shard).
@CLAassistant

CLAassistant commented Sep 28, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟢 Approval recommended

The fix and regression tests fully address the stale-count scenarios.

Review effort: Lite
Findings: None

What changed in this PR

Fixes ArrayOf.ToArrayOf(IEnumerable<T>) handling when a source’s reported count differs from its enumeration.

Changes:

  • Dynamically grows buffers during enumeration.
  • Trims buffers to the actual item count.
  • Adds regression tests for growth and shrink scenarios.
File Description
tests/​Opc.Ua.Types.Tests/​BuiltIn/​ArrayOfTests.cs Adds stale-count regression coverage.
src/​Opc.Ua.Types/​BuiltIn/​ArrayOf.cs Corrects buffer sizing and trimming.

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

@biancode

Copy link
Copy Markdown
Contributor Author

Closing in favour of #4541, which fixed the same race in ArrayOf.ToArrayOf (e1a994a) with the same grow-and-trim approach and equivalent tests. Thanks!

@biancode biancode closed this Sep 28, 2026
@biancode
biancode deleted the spec/arrayof-concurrent-count-race branch September 30, 2026 09:12
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.

ArrayOf.ToArrayOf(IEnumerable<T>) throws or returns padded data when the source's Count is stale

3 participants