Avoid ArrayPool.Shared-only finally blocks - #12865
Conversation
|
Claude finished @benaadams's task in 3m 33s —— View job Review: Document
|
There was a problem hiding this comment.
Pull request overview
Updates the repository’s robustness guidelines to clarify resource-management expectations around ArrayPool<T>.Shared.Return(...) in exceptional control flow, aiming to reduce unnecessary try/finally usage while keeping the guidance narrowly scoped to the BCL shared pool.
Changes:
- Adds documentation stating that
ArrayPool<T>.Shared.Return(array)does not requiretry/finallysolely for exceptional-path cleanup. - Explicitly warns not to generalize this exception to custom pools or other ownership/resource-release contracts.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| ## Resource management | ||
|
|
||
| - `IDisposable` / `IAsyncDisposable` objects (especially `IDb`, streams, channels) must be wrapped in `using` — otherwise they leak. | ||
| - Do not add a `try`/`finally` solely to guarantee `ArrayPool<T>.Shared.Return(array)` after an exception. If this exact shared-pool return is skipped, the abandoned array remains GC-reclaimable, and any replacement allocation is dwarfed by the cost of throwing the exception. Return it on the normal path. Do not generalize this exception to custom pools or other `Return`, `Release`, or `Dispose` contracts, which may track ownership, use reference counting, or hold non-GC resources. |
There was a problem hiding this comment.
Bro, why are we throwing frequent exceptions
Do you hate performance?
| ## Resource management | ||
|
|
||
| - `IDisposable` / `IAsyncDisposable` objects (especially `IDb`, streams, channels) must be wrapped in `using` — otherwise they leak. | ||
| - Do not add a `try`/`finally` solely to guarantee `ArrayPool<T>.Shared.Return(array)` after an exception. If this exact shared-pool return is skipped, the abandoned array remains GC-reclaimable, and any replacement allocation is dwarfed by the cost of throwing the exception. Return it on the normal path. Do not generalize this exception to custom pools or other `Return`, `Release`, or `Dispose` contracts, which may track ownership, use reference counting, or hold non-GC resources. |
There was a problem hiding this comment.
Medium — the exception needs two more carve-outs to be safe to apply mechanically in this repo:
SafeArrayPool<T>.Sharedis spelled almost identically and is the dominant idiom here (EvmPooledMemory,Discv5/PacketCodec,TransactionProcessor,ArrayPoolList<T>…). In the standard build it isArrayPool<T>.Shared(SafeArrayPool.std.cs:14), but underEnableZkEvm=trueit becomes a custom retain-forever pool whose own docs say "zkVM is single-threaded with no GC. Allocations are forever" (SafeArrayPool.zkevm.cs:16-21). The "remains GC-reclaimable" rationale is false in that configuration — andNethermind.Core/Nethermind.Trie, which callArrayPool<T>.Shared.Returndirectly, are compiled into it too.- The repo already ships
using-based helpers that are a try/finally around a shared-pool return —ArrayPoolDisposableReturn(Nethermind.Core/Buffers/ArrayPoolDisposableReturn.cs:21, 5 call sites) andArrayPoolList<T>. Bullet 14 mandatesusingforIDisposable; this bullet, read literally, discourages exactly what those helpers do. Unwinding them into hand-rolled returns risks double-return / use-after-return, which corrupts the pool silently — a much worse outcome than the missed return this rule is optimising away.
Also worth stating that the rule is prospective (there are ~49 existing finally-based shared-pool returns; they shouldn't be stripped as churn).
| - Do not add a `try`/`finally` solely to guarantee `ArrayPool<T>.Shared.Return(array)` after an exception. If this exact shared-pool return is skipped, the abandoned array remains GC-reclaimable, and any replacement allocation is dwarfed by the cost of throwing the exception. Return it on the normal path. Do not generalize this exception to custom pools or other `Return`, `Release`, or `Dispose` contracts, which may track ownership, use reference counting, or hold non-GC resources. | |
| - Do not add a `try`/`finally` solely to guarantee `ArrayPool<T>.Shared.Return(array)` after an exception. If this exact shared-pool return is skipped, the abandoned array remains GC-reclaimable, and any replacement allocation is dwarfed by the cost of throwing the exception. Return it on the normal path, leave existing `finally`-based returns alone, and never add a return on a path where the array may already have been returned or handed off — a double return corrupts the pool silently. Do not generalize this exception to `SafeArrayPool<T>.Shared` (the zkEVM build swaps it for a retain-forever pool with no GC behind it), to the `using`-based helpers `ArrayPoolDisposableReturn` / `ArrayPoolList<T>`, or to other `Return`, `Release`, or `Dispose` contracts, which may track ownership, use reference counting, or hold non-GC resources. |
There was a problem hiding this comment.
Partially fair, but legacy shouldn't get a pass
There was a problem hiding this comment.
Updated in 1f60687. I incorporated the EnableZkEvm/SafeArrayPool<T>.Shared carve-out, explicitly permit the using ownership helpers, and added the double-return/hand-off warning.
I also tightened the rule around direct ArrayPool<T>.Shared use: don't add or retain a try/finally solely for Return; return immediately before a deliberate throw while the code still owns the array; and keep the return in a finally when that finally is already required for other cleanup.
Following that rule, I removed 35 ArrayPool-only finally blocks. I retained the zkVM-compatible Core/Trie/SSZ cases, SafeArrayPool<T>.Shared cases, and the existing SocketClient cleanup finally that also disposes the stream.
Return arrays rented from ArrayPool<T>.Shared on successful and handled paths without adding exception regions solely for pool reuse. Keep returns in finally blocks that already perform required cleanup, in using-based ownership helpers, and in code compiled for the no-GC zkEVM runtime. Return before deliberate throws while ownership remains local. Validation: - dotnet build src/Nethermind/Nethermind.slnx -c Release --no-restore (0 warnings, 0 errors) - dotnet build tools/StatelessInputGen/StatelessInputGen.csproj -c Release --no-restore (passes; 5 pre-existing assembly-version warnings) - 2,490 affected tests executed: 2,486 passed, 4 skipped
Clarify that expected failures which are handled, retried, or mapped at a recovery boundary must return their ArrayPool<T>.Shared rental without requiring a finally block. Return from targeted catches for snapshot retries, malformed channel and SSZ input, JSON conversion failures, and per-block extraction failures. Audited all 34 production ArrayPool-only finally removals: six paths cross a recovery boundary and now return from a catch; the remaining 28 abort their operation on unexpected failure. Validation: - dotnet build src/Nethermind/Nethermind.slnx -c Release --no-restore (0 warnings, 0 errors) - dotnet build tools/StatelessInputGen/StatelessInputGen.csproj -c Release --no-restore (passes; 5 pre-existing assembly-version warnings) - 81 focused Optimism, SSZ multi-segment/malformed-input, and SSZ generator tests passed
|
Addressed the remaining handled/repeated-exception edge in 761a2a2. The guidance now distinguishes unexpected failures that abort the operation from expected failures that are handled, retried, or mapped at a recovery boundary. I audited all 34 production finally removals: six cross such a boundary and now return from targeted catches without adding a finally solely for This covers snapshot download retries, malformed channel and multi-segment SSZ input, capped-array JSON failures, and per-block stateless-input extraction failures. The PR description has the audit and validation details. @claude review |
|
Claude finished @benaadams's task in 7m 25s —— View job Review: ArrayPool.Shared finally-block removals
I traced every one of the 35 removals. No double-return, no use-after-return, and no control-flow regression — the two rewrites that changed shape ( The Two of the remaining four don't match their boundary. FindingsMedium —
|
Restore the existing finally-based returns around retry and malformed-input paths instead of replacing them with targeted catches. This keeps exception coverage aligned with each recovery boundary and retains the generated SSZ cleanup used by EnableZkEvm builds. Clarify the guidance not to replace an existing finally with catches solely for Return, preserve the explicit return before InputExtractor's deliberate FormatException, and replace the checksum method's bare scope with an await-using block. Validation: - dotnet format whitespace src/Nethermind/ --folder --verify-no-changes - dotnet format whitespace tools/StatelessInputGen/ --folder --verify-no-changes - dotnet build src/Nethermind/Nethermind.slnx -c Release --no-restore (0 warnings, 0 errors) - dotnet build tools/StatelessInputGen/StatelessInputGen.csproj -c Release --no-restore (5 pre-existing assembly-version warnings, 0 errors)
|
Addressed in d734042. The recoverable/retried paths now keep their original The rule now explicitly says not to replace an existing recovery-path Current-head validation:
@claude review |
|
Claude finished @benaadams's task in 5m 46s —— View job Review: ArrayPool.Shared finally-block removals (head
|
| byte[] rented = ArrayPool<byte>.Shared.Rent((int)size); | ||
| try | ||
| { | ||
| RandomAccess.Read(_handle, rented.AsSpan(0, (int)size), offset); | ||
| action(new ReadOnlySpan<byte>(rented, 0, (int)size), arg); | ||
| return true; | ||
| } | ||
| finally { ArrayPool<byte>.Shared.Return(rented); } | ||
| RandomAccess.Read(_handle, rented.AsSpan(0, (int)size), offset); | ||
| action(new ReadOnlySpan<byte>(rented, 0, (int)size), arg); |
There was a problem hiding this comment.
Low — this is the widest abandonment window the PR opens: size is bounded only by the 64 * MiB guard on line 54, so a single escape drops an up-to-64 MiB LOH rental, and action is a caller-supplied ReadOnlySpanAction<byte, TArg> on a public API, so the guidance's "unexpected and aborts the operation" premise depends on every future caller rather than on code visible here.
For the one caller today it holds by a hair. RecordedBalStore.Get (RecordedBalStore.cs:36-41) wraps the decode in catch (RlpException) inside the callback, so the expected corrupt-slot case never reaches this frame. But BlockAccessListDecoder throws RlpException only for its own semantic check (BlockAccessListDecoder.cs:214) and otherwise relies on Rlp.ValueDecoderContext for bounds, which does not uniformly normalise overruns to RlpException — a truncated slot can surface as IndexOutOfRangeException/ArgumentOutOfRangeException, escape the inner filter, and abandon the rental on a path that repeats per block during replay.
Nothing to change in this PR necessarily — but widening that inner filter to catch (Exception) would both close this and turn "corrupt slot kills replay" back into the logged warning it was clearly meant to be.
There was a problem hiding this comment.
Fixed in 57efe16 by restoring SlotFile.TryRead to its original finally shape. A broad callback catch would hide arbitrary caller bugs, while this public callback boundary and its possible 64 MiB rental make guaranteed return the clearer ownership contract.
Restore SlotFile.TryRead's existing finally because its public callback boundary can propagate failures outside the method and the rental may be as large as 64 MiB. A broad callback catch would hide caller bugs rather than establish a reliable recovery contract. Compress the ArrayPool.Shared guidance from three bullets to two while retaining the expected/unexpected failure split, EnableZkEvm and ownership-helper carve-outs, and double-return warning. Validation: dotnet format whitespace src/Nethermind/ --folder --verify-no-changes; git diff --check. No tests rerun because SlotFile was restored exactly to master and the other change is documentation-only.
|
Addressed the follow-up review in 57efe16.
Formatting and diff checks pass. SlotFile is restored exactly to master, so no additional test was needed for this follow-up. @claude review |
|
Claude finished @benaadams's task in 3m 4s —— View job Review: ArrayPool.Shared finally-block removals (head
|
|
|
||
| - `IDisposable` / `IAsyncDisposable` objects (especially `IDb`, streams, channels) must be wrapped in `using` — otherwise they leak. | ||
| - Outside code compiled with `EnableZkEvm=true`, do not add or retain a `try`/`finally` solely to guarantee `ArrayPool<T>.Shared.Return(array)` after an unexpected exception aborts the operation; the abandoned array remains GC-reclaimable. Return rentals normally and before expected failures are handled, retried, or rethrown, retaining an existing `finally` for recovery or other cleanup rather than replacing it with catches solely for `Return`; if code deliberately throws while it owns the array, return it immediately first. | ||
| - Do not generalize this exception to `SafeArrayPool<T>.Shared`, `ArrayPoolDisposableReturn`, `ArrayPoolList<T>`, custom pools, or other `Return`, `Release`, or `Dispose` contracts; `using`-based helpers remain permitted because other contracts may track ownership, use reference counting, or hold non-GC resources. Never return an array that may already have been returned or handed off — a double return silently corrupts the pool. |
There was a problem hiding this comment.
Low — the compression left a non-sequitur: "using-based helpers remain permitted because other contracts may track ownership, use reference counting, or hold non-GC resources." The ownership/refcount/non-GC clause is the reason for the non-generalisation list, not for permitting the using helpers. Read literally it says the helpers are allowed because they might hold non-GC resources, which inverts the argument. Swapping the clause order fixes it without adding length:
| - Do not generalize this exception to `SafeArrayPool<T>.Shared`, `ArrayPoolDisposableReturn`, `ArrayPoolList<T>`, custom pools, or other `Return`, `Release`, or `Dispose` contracts; `using`-based helpers remain permitted because other contracts may track ownership, use reference counting, or hold non-GC resources. Never return an array that may already have been returned or handed off — a double return silently corrupts the pool. | |
| - Do not generalize this exception to `SafeArrayPool<T>.Shared`, `ArrayPoolDisposableReturn`, `ArrayPoolList<T>`, custom pools, or other `Return`, `Release`, or `Dispose` contracts, which may track ownership, use reference counting, or hold non-GC resources; `using`-based helpers remain permitted. Never return an array that may already have been returned or handed off — a double return silently corrupts the pool. |
There was a problem hiding this comment.
Agreed. I swapped the clause order in 09a236a so the ownership/ref-counting/non-GC rationale clearly applies to the non-generalization rule, while using-based helpers remain permitted.
| { | ||
| ArrayPool<byte>.Shared.Return(rented); | ||
| } | ||
| WriteToBuffer(writer, capability, rented.AsSpan(), protocolByteCount); |
There was a problem hiding this comment.
Low — this is the one removal in the PR that trips the new rule's own "deliberate throw" clause. WriteToBuffer ends in ThrowJsonException() (:87 → :127) when Utf8Formatter.TryFormat fails, so the code deliberately throws while rented is still owned locally, and the rule says to return it immediately first.
Practically unreachable — MaxIntegerDigits is sized for the int version and this branch only runs for a protocol code longer than StackAllocThreshold — so this is an accuracy note rather than a defect. Worth mentioning because the PR body claims the "return before deliberate throws" case is handled everywhere (InputExtractor is cited as the only instance).
There was a problem hiding this comment.
Agreed. In 09a236a, WriteToBuffer is now TryWriteToBuffer. Its known formatting failure returns the rental before calling the throw helper; unrelated encoding or writer exceptions still abandon the array as intended.
flcl42
left a comment
There was a problem hiding this comment.
Found 1 low-severity issue in src/Nethermind/Nethermind.TxPool/Collections/PersistentBlobTxDistinctSortedPool.cs.
Restore the persistent blob lookup finally blocks because malformed and storage failures are recovered at the JSON-RPC request boundary and can recur without terminating the server. Return CapabilityConverter's rental before its deliberate formatting failure, while continuing to abandon it for unrelated unexpected failures. Clarify that non-GC ownership contracts are the reason the ArrayPool.Shared guidance must not be generalized.
|
@claude review |
|
Claude finished @benaadams's task in 4m 5s —— View job Review: ArrayPool.Shared finally-block removals (head
|
| byte[] rented = ArrayPool<byte>.Shared.Rent(totalLength); | ||
| try | ||
| { | ||
| WriteToBuffer(writer, capability, rented.AsSpan(), protocolByteCount); | ||
| } | ||
| finally | ||
| if (!TryWriteToBuffer(writer, capability, rented.AsSpan(), protocolByteCount)) | ||
| { | ||
| ArrayPool<byte>.Shared.Return(rented); | ||
| ThrowJsonException(); | ||
| } | ||
|
|
||
| ArrayPool<byte>.Shared.Return(rented); |
There was a problem hiding this comment.
Low — correct as written, but it's the one shape in this PR that is only correct because of an attribute on another method.
Two Return(rented) calls now exist for one rental. They're mutually exclusive solely because ThrowJsonException carries [DoesNotReturn] (:133). Drop that attribute, or replace the helper with something that logs-and-continues, and this becomes a double return — precisely the "silently corrupts the pool" failure the new rule in robustness.md warns about. Given this file is the worked example the PR body cites for "return before deliberate throws", a single-exit shape is worth preferring:
| byte[] rented = ArrayPool<byte>.Shared.Rent(totalLength); | |
| try | |
| { | |
| WriteToBuffer(writer, capability, rented.AsSpan(), protocolByteCount); | |
| } | |
| finally | |
| if (!TryWriteToBuffer(writer, capability, rented.AsSpan(), protocolByteCount)) | |
| { | |
| ArrayPool<byte>.Shared.Return(rented); | |
| ThrowJsonException(); | |
| } | |
| ArrayPool<byte>.Shared.Return(rented); | |
| byte[] rented = ArrayPool<byte>.Shared.Rent(totalLength); | |
| bool written = TryWriteToBuffer(writer, capability, rented.AsSpan(), protocolByteCount); | |
| ArrayPool<byte>.Shared.Return(rented); | |
| if (!written) | |
| { | |
| ThrowJsonException(); | |
| } |
One Return, one line shorter, still returns before the deliberate throw, and no longer depends on [DoesNotReturn] for pool safety.
There was a problem hiding this comment.
Agreed. c3e9972 now stores the result, returns the rental once, and only then calls the throw helper when needed, so pool safety no longer depends on DoesNotReturn.
Use one Return site before CapabilityConverter's known formatting throw so pool safety does not depend on the throw helper's DoesNotReturn annotation. Restore the unrelated blank line removed from InputExtractor.
|
Claude finished @benaadams's task in 4m 39s —— View job Review: ArrayPool.Shared finally-block removals (head
|
| ## Resource management | ||
|
|
||
| - `IDisposable` / `IAsyncDisposable` objects (especially `IDb`, streams, channels) must be wrapped in `using` — otherwise they leak. | ||
| - Outside code compiled with `EnableZkEvm=true`, do not add or retain a `try`/`finally` solely to guarantee `ArrayPool<T>.Shared.Return(array)` after an unexpected exception aborts the operation; the abandoned array remains GC-reclaimable. Return rentals normally and before expected failures are handled, retried, or rethrown, retaining an existing `finally` for recovery or other cleanup rather than replacing it with catches solely for `Return`; if code deliberately throws while it owns the array, return it immediately first. |
There was a problem hiding this comment.
Low — two of the files this PR touches are compiled with EnableZkEvm=true, so by the letter of this bullet they fall inside its own carve-out.
Nethermind.Stateless.ZiskGuest/Makefile:44 builds with -p:EnableZkEvm=true, and the global property flows through the whole ProjectReference closure:
ZiskGuest→Stateless.Executor→Consensus→Evm→Specs→Serialization.Json(Nethermind.Specs.csproj:10) — that'sHexWriter.csZiskGuest→Stateless.Executor→Consensus→Blockchain→Network.Stats(Nethermind.Blockchain.csproj:14) — that'sCapabilityConverter.cs
This is the same chain that got the 12 SSZ-generator blocks restored in d734042, but the exposure is materially smaller here: neither Nethermind.Serialization.Json.dll nor Nethermind.Network.Stats.dll appears in BFLAT_REFS (Makefile:15-40), so neither is linked into the guest binary — the AOT build would fail if guest-reachable code touched them. Serialization.Ssz.dll and Stateless.Executor.dll, by contrast, are listed.
So the change is fine in practice and it's the rule text that's imprecise: "compiled with EnableZkEvm=true" is broader than "runs in the zkVM". Scoping it to code that reaches the guest binary (i.e. an assembly in BFLAT_REFS) would make the boundary checkable rather than requiring a reader to trace the reference graph — and would stop a future agent from reverting these two hunks on a literal reading.
There was a problem hiding this comment.
Agreed. 7b7eb34 narrows the carve-out to zkVM guest entry-point code and assemblies explicitly listed in BFLAT_REFS, rather than every transitive project compiled with EnableZkEvm. The PR description now uses the same boundary.
Scope the exception to guest entry-point code and assemblies explicitly listed in BFLAT_REFS rather than every transitive project compiled with EnableZkEvm.
|
Claude finished @benaadams's task in 2m 23s —— View job Review: ArrayPool.Shared finally-block removals (head
|
| Touched assembly | In BFLAT_REFS? |
|---|---|
Nethermind.EraE, Init.Snapshot, Kademlia, Merge.Plugin(+.Test), Network, Network.Stats, Serialization.Json, StatelessInputGen |
no |
So all ten hunks are outside the carve-out by the letter of the rule — the two cases I flagged last round (HexWriter.cs in Serialization.Json, CapabilityConverter.cs in Network.Stats, both reachable via the EnableZkEvm=true ProjectReference closure but absent from BFLAT_REFS) are now unambiguously in scope for removal, and the 12 restored SSZ-generator blocks stay covered because Serialization.Ssz.dll and Stateless.Executor.dll are listed. Rule and change now agree without needing a reference-graph trace.
Nothing else in the rule text moved: the expected/unexpected split, "don't replace an existing recovery finally with catches", the return-before-deliberate-throw clause, the SafeArrayPool / ArrayPoolDisposableReturn / ArrayPoolList non-generalisation list with its rationale in the right position, and the double-return warning all survive the rewrite.
Code re-audit at this head
No double return, no use-after-return. One Return per rental on every reachable path, and each consumer completes first: Utf8JsonWriter.WriteRawValue copies the span (HexWriter:452); HexWriter.WriteHexString encodes into IBufferWriter.GetSpan (PayloadBodiesDirectResponseWriter:248); result.Span.ToArray() materialises into nodes before bucketEntries is returned, with the IsReferenceOrContainsReferences clear flag preserved (KBucketTree:190-192); indexBytes is filled from tdOffsets and WriteEntry awaited before the guarded Return (EraWriter:248-251); the SSZ test's decoded is built from encoded, not aliased to cells[0]/proofs[0].
No control-flow change. ComputeChecksumAsync uses the explicit await using (FileStream …) block with checksum assigned as its last statement — definite assignment holds and the stream disposes where the declaration form disposed it. EraWriter.Finalize has no early return/throw between the conditional Rent and the if (needsTd)-guarded Return (tdOffsets is [] otherwise), accumulatorRoot is default-initialised, and moving using ArrayPoolList<byte> indexBytes to method scope only defers its dispose past FinalizeChecksum(), after its last use.
CapabilityConverter (c3e9972) still has the single-Return shape: bool written = TryWriteToBuffer(...) → Return(rented) → if (!written) ThrowJsonException(). [DoesNotReturn, StackTraceHidden] is still on the helper (:132) but pool safety no longer depends on it. The stackalloc branch throws from the same condition it did inside the old WriteToBuffer, with identical writer state on failure.
Abandonment windows. EraExporter.cs:163 calls Finalize once per epoch with no retry (≤64 KB tdOffsets, sub-LOH). ComputeChecksumAsync runs at InitDatabaseSnapshot.cs:84, outside DownloadWithRetryAsync's retry loop — one 64 KiB abandonment per process start at worst. Session.Invoke rents subscriber-count refs and only skips Array.Clear+Return on the throwing path, where the array is unreachable anyway (no delegate rooting → no Session retention); the skipped OnSessionDisconnected on a throwing handler is pre-existing (Session.cs:534).
InputExtractor remains the only case with real aliasing potential and is still correct: DecodeInput returns the buffer immediately before throw new FormatException, and because the tuple deconstruction never runs on that path the caller's local is never bound, so the retained try/finally around the file write cannot double-return. The removed bare catch only covered non-FormatException escapes from Bytes.FromUtf8HexString, which fall through the caller's catch (… IOException or FormatException or UnauthorizedAccessException) filter and abort the tool. File diff is +0 −5, and the blank line before the retained try is back.
Findings — one Low
Low — the carve-out boundary is checkable now, but not self-locating, and it exempts most of the hot core · robustness.md:15 · inline
grep -rl BFLAT_REFS returns exactly two files — Nethermind.Stateless.ZiskGuest/Makefile and this rule — so an agent that doesn't already know the token has nothing to anchor on; naming the path makes the bullet self-contained. Separately, worth being deliberate about the resulting scope: BFLAT_REFS lists Core, Evm, Evm.Precompiles, State, Trie, TxPool, Blockchain, Consensus, Specs, Serialization.Rlp, Serialization.Ssz, Db, Crypto, Abi, Config, Logging — most of the hot path, i.e. where the guidance's own performance argument is strongest. Per-callsite guest reachability isn't statically checkable, so an assembly-granular boundary is the right trade; I'm flagging it so the width is a choice rather than a surprise, not asking for a change.
Notes
Cumulative status across six rounds — nothing outstanding. Restored to master shape: SszGenerator (12 blocks), CappedArrayJsonConverter, ChannelDecoder, SnapshotDownloader, BlockCachePreWarmer, PayloadByNumberProtocol, EthStatsMessageParser, SlotFile, PersistentBlobTxDistinctSortedPool. Fixed: the robustness.md clause order, the CapabilityConverter deliberate-throw case and its single-Return shape, the InputExtractor blank line, and now the zkVM carve-out boundary.
dotnet build is not permitted in this environment, so definite assignment was verified by inspection (checksum in InitDatabaseSnapshot, nodes in KBucketTree, accumulatorRoot/checksum in EraWriter, written in CapabilityConverter — each assigned on every path reaching its use). Your reported clean build is consistent with that.
Verdict: no leak, no pool corruption, no control-flow or consensus risk. No Critical/High/Medium findings. The single Low is a rule-wording precision point and does not block merge.
· feature/arraypool-shared-return-guidance
Point the robustness guidance directly to the BFLAT_REFS definition so its intentionally assembly-granular carve-out is self-locating.
|
@claude review |
|
Claude finished @benaadams's task in 2m 38s —— View job Review: ArrayPool.Shared finally-block removals (head
|
Changes
try/finallysolely forArrayPool<T>.Shared.Return(...)after an unexpected exception aborts the operation.finallyblocks on recoverable/retried paths; do not replace them with catches solely forReturn.finallyblocks whose only cleanup was returning a directly rented shared array on operation-aborting unexpected failures.CapabilityConverterandInputExtractorretain this behavior for their known failure paths.BFLAT_REFSinNethermind.Stateless.ZiskGuest/Makefile,SafeArrayPool<T>.Shared, using-based ownership helpers, custom pools, and other ownership/resource contracts from the guidance. Generated SSZ cleanup remains unchanged because its output is linked through aBFLAT_REFSassembly.finallycleanup for snapshot retries, channel decompression, malformed JSON/SSZ input, public callbacks, and other paths where failure is expected and handled.EraWriterhunk is mechanical de-indentation; its semantic change is only moving a shared-array return fromfinallyto the successful path.Types of changes
What types of changes does your code introduce?
Testing
Requires testing
If yes, did you write tests?
Notes on testing
Current head:
dotnet format whitespacechecks for the affected C# files — passed.dotnet format whitespace tools/StatelessInputGen/ --folder --verify-no-changes— passed.dotnet build src/Nethermind/Nethermind.slnx -c Release --no-restore— 0 warnings, 0 errors.dotnet build tools/StatelessInputGen/StatelessInputGen.csproj -c Release --no-restore— passed with five pre-existing assembly-version warnings.Earlier affected-suite validation executed 2,490 tests: 2,486 passed and four skipped. No pool-identity test was added because
ArrayPool<T>.Shareddoes not guarantee which instance a subsequent rent returns; such a test would be nondeterministic rather than proving ownership.Documentation
Requires documentation update
Requires explanation in Release Notes