@@ -1037,6 +1037,21 @@ e is InvocationExpressionSyntax i
10371037 && i . ArgumentList . Arguments [ 0 ] . Expression is IdentifierNameSyntax buf
10381038 ? buf . Identifier . Text : null ;
10391039
1040+ // Is `idn` a pooled buffer passed as an argument to a CONSTRUCTOR whose result ESCAPES this method —
1041+ // `return new Wrapper(…, buf, …)` or `_field = new Wrapper(…, buf, …)`? Such a `new` hands the buffer
1042+ // to the constructed object, which becomes responsible for Return, so the buffer leaves the method
1043+ // inside the escaping object (an ownership transfer, not a borrow). One level only: the `new` must be
1044+ // the direct return value or assigned to a real FIELD — a `new` stored in a LOCAL stays a borrow (the
1045+ // local is method-scoped; a wrapper that never leaves the method and never Returns the buffer is a
1046+ // real leak, Codex), as does a `new` buried in another expression. The field check is symbol-based
1047+ // (`IFieldSymbol`) so an assignment to a same-named LOCAL is not mistaken for a field.
1048+ static bool PassedToEscapingCtor ( IdentifierNameSyntax idn , SemanticModel model ) =>
1049+ idn . Parent is ArgumentSyntax { Parent : ArgumentListSyntax
1050+ { Parent : BaseObjectCreationExpressionSyntax oce } }
1051+ && ( oce . Parent is ReturnStatementSyntax
1052+ || ( oce . Parent is AssignmentExpressionSyntax a && a . Right == oce
1053+ && model . GetSymbolInfo ( a . Left ) . Symbol is IFieldSymbol ) ) ;
1054+
10401055// Is `t` the System.Buffers.MemoryPool<T> type — the Dispose-based pool. Mirrors IsArrayPoolType
10411056// (checked on the resolved symbol, so an aliased/injected `MemoryPool<T>` receiver binds and a
10421057// look-alike does not).
@@ -2405,8 +2420,16 @@ or ImplicitObjectCreationExpressionSyntax } init
24052420 if ( ! usingMemoryOwners . Contains ( nm ) )
24062421 escapedLocals . Add ( nm ) ;
24072422 }
2423+ // A pooled buffer handed as an argument is normally a BORROW (the renter Returns it),
2424+ // NOT an escape — so `pool.Return(buf); Work(buf)` still trips use-after-return. But a
2425+ // pooled buffer passed to a CONSTRUCTOR whose result ESCAPES this method (`return new
2426+ // Wrapper(buf)` / `_field = new Wrapper(buf)`) transfers ownership to that object, which
2427+ // becomes responsible for Return — so the buffer is NOT leaked here. Treat that as an
2428+ // escape (mined FP on Pipelines.Sockets.Unofficial: ArrayPoolBufferWriter.CreateNewSegment
2429+ // returns `new ArrayPoolRefCountedSegment(pool, array, prev)`).
24082430 else if ( ( idn . Parent is AssignmentExpressionSyntax asg && asg . Right == idn )
2409- || ( idn . Parent is ArgumentSyntax && ! poolBuffers . Contains ( nm ) && ! consumedArg ) )
2431+ || ( idn . Parent is ArgumentSyntax && ! poolBuffers . Contains ( nm ) && ! consumedArg )
2432+ || ( poolBuffers . Contains ( nm ) && PassedToEscapingCtor ( idn , model ) ) )
24102433 escapedLocals . Add ( nm ) ;
24112434 }
24122435 var tracked = new HashSet < string > ( candidates ) ;
0 commit comments