You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Append with interpolation may return corrupt pool-rented array if the interpolation expression throws #324
When Append($"...") or AppendLine($"...") is called with an interpolated string and an expression inside {...} throws, ValueStringBuilder can return its rented array to ArrayPool<char>.Shared twice. This is done if the array needs to be expanded at
File-based program, run with dotnet run Repro.cs (.NET 10 SDK):
#:packageLinkDotNet.StringBuilder@3.7.1usingSystem.Buffers;usingLinkDotNet.StringBuilder;varcustomers=newDictionary<int,string>();varorderId=42;try{// Rents a 32-char array from ArrayPool<char>.Shared.usingvarsb=newValueStringBuilder();// The text needs more than 32 chars, so the builder has to grow.// customers[7] does not exist, so the lookup throws in the middle of the Append.sb.Append($"Order {orderId} for customer {customers[7]}");}catch(KeyNotFoundException){}// Two arrays that are rented at the same time must be two different arrays.varfirst=ArrayPool<char>.Shared.Rent(32);varsecond=ArrayPool<char>.Shared.Rent(32);Console.WriteLine(ReferenceEquals(first,second));
Actual: prints True. Both variables point to the same array.
Expected: prints False. The builder returns its array to the pool exactly once.
Cause
All line numbers refer to ValueStringBuilder.InterpolatedStringHandler.cs.
The constructor of AppendInterpolatedStringHandler receives the builder by value (line 43). Builder is therefore a copy, and the copy and the original both hold the same rented array.
Builder.EnsureCapacity(...) on line 48 grows the copy. Growing rents a larger array and returns the old one to the pool. The original builder still has the old array in arrayFromPool. Any later append on the copy that grows it has the same effect.
Normally Append(ref handler) then runs this = handler.Builder (line 14), which brings the original up to date.
If an expression inside {...} throws, step 3 never runs. When the original builder is disposed, it returns the old array a second time.
All of these have to be true:
The builder holds a rented array, not only a caller-supplied buffer.
The interpolated string is long enough to make the builder grow.
An expression inside {...}, or its ToString / TryFormat, throws.
The builder is disposed afterwards, for example through using.
Activity
linkdotnet commentedon Oct 3, 2026
Hey @dhhoang - thanks for the report. And indeed the issue is holding on the builder. I will fix that.
Thanks for that!
fix: avoid double return of pooled array when an interpolation hole t…