@@ -947,20 +947,31 @@ static bool DisposedGuardBefore(BlockSyntax body, int before) =>
947947 || ( ifs . Statement is BlockSyntax gb
948948 && gb . Statements . FirstOrDefault ( ) is ReturnStatementSyntax ) ) ) ;
949949
950- // The FIRST direct read of a disposed field (`_f.Member`, `_f`/`this._f` only) in `body` that is
951- // NOT protected by an opening disposed-guard — the unit the field-UAF pass keys on. Returns the
952- // member-access node and the field name, or null when the body has no such read (or its first
953- // disposed-field read is already guarded, the canonical-fix shape). Shared by the direct handler
954- // scan and the one-hop helper scan (an indirect use through a private helper).
950+ // The released OWNER field reached by a read of field `f`: `f` itself when it is a released owner
951+ // (an IDisposable or `IMemoryOwner<T>` field released in Dispose), or — when `f` is a `Memory` VIEW
952+ // field — the owner it aliases, provided that owner is released. Null when `f` reaches no released
953+ // owner. Unifies the direct disposed-field read (#75/#76) with the pooled-view-in-a-field dangle:
954+ // reading `_view` (a borrow of `_owner`) after `_owner`'s release is a use of `_owner` after release.
955+ static string ? ReleasedOwner ( string f , Dictionary < string , int > releasedAt ,
956+ Dictionary < string , string > viewFieldOwner ) =>
957+ releasedAt . ContainsKey ( f ) ? f
958+ : viewFieldOwner . TryGetValue ( f , out var owner ) && releasedAt . ContainsKey ( owner ) ? owner
959+ : null ;
960+
961+ // The FIRST read of a released owner — directly (`_f.Member`) or through a Memory VIEW field that
962+ // aliases it — in `body` that is NOT protected by an opening disposed-guard. Returns the member-
963+ // access node and the OWNER field, or null when the body has no such read (or its first such read is
964+ // already guarded, the canonical-fix shape). Shared by the direct handler scan and the one-hop
965+ // helper scan (an indirect use through a private helper).
955966static ( MemberAccessExpressionSyntax Use , string Field ) ? FirstUnguardedDisposedRead (
956- BlockSyntax body , Dictionary < string , int > releasedAt )
967+ BlockSyntax body , Dictionary < string , int > releasedAt , Dictionary < string , string > viewFieldOwner )
957968{
958969 foreach ( var ma in body . DescendantNodes ( ) . OfType < MemberAccessExpressionSyntax > ( ) )
959970 {
960971 if ( ma . Name . Identifier . Text is "Dispose" or "DisposeAsync" )
961972 continue ;
962- if ( ThisFieldName ( ma . Expression ) is { } uf && releasedAt . ContainsKey ( uf ) )
963- return DisposedGuardBefore ( body , ma . SpanStart ) ? null : ( ma , uf ) ;
973+ if ( ThisFieldName ( ma . Expression ) is { } f && ReleasedOwner ( f , releasedAt , viewFieldOwner ) is { } owner )
974+ return DisposedGuardBefore ( body , ma . SpanStart ) ? null : ( ma , owner ) ;
964975 }
965976 return null ;
966977}
@@ -1098,6 +1109,25 @@ e is InvocationExpressionSyntax i
10981109 return null ;
10991110}
11001111
1112+ // Is `t` System.Buffers.IMemoryOwner<T> — the interface itself, or a concrete type that implements
1113+ // it? Resolved on the SYMBOL, so a fully-qualified `System.Buffers.IMemoryOwner<T>`, a type alias, or
1114+ // a concrete owner type is recognised — not only the bare `IMemoryOwner` spelling (CodeRabbit/Codex).
1115+ static bool IsMemoryOwnerType ( ITypeSymbol ? t ) =>
1116+ t is INamedTypeSymbol nt
1117+ && ( ( nt . Name == "IMemoryOwner" && IsInNamespace ( nt , "System" , "Buffers" ) )
1118+ || nt . AllInterfaces . Any ( i => i . Name == "IMemoryOwner" && IsInNamespace ( i , "System" , "Buffers" ) ) ) ;
1119+
1120+ // The owner FIELD a `Memory` view assignment aliases — `_owner.Memory` / `this._owner.Memory` of an
1121+ // IMemoryOwner<T> field. Uses the this/bare receiver restriction (ThisFieldName), so `other._owner.
1122+ // Memory` (another instance's owner) is NOT recorded and the this-qualified spelling IS (Codex). The
1123+ // borrow is recognised by the resolved `IMemoryOwner<T>.Memory` property symbol, not by name.
1124+ static string ? FieldViewOwner ( ExpressionSyntax e , SemanticModel model ) =>
1125+ e is MemberAccessExpressionSyntax { Name . Identifier . Text : "Memory" } mem
1126+ && model . GetSymbolInfo ( mem ) . Symbol is IPropertySymbol { ContainingType : { Name : "IMemoryOwner" } ict }
1127+ && IsInNamespace ( ict , "System" , "Buffers" )
1128+ ? ThisFieldName ( mem . Expression )
1129+ : null ;
1130+
11011131// The tracked owner buffer a FULL-LENGTH view spans, else null. POOL005: `buf.AsSpan()` /
11021132// `buf.AsMemory()` with NO arguments span `[0, Length)` — the WHOLE backing array — and so does
11031133// `buf.AsSpan(0, buf.Length)` / `new Span<T>(buf, 0, buf.Length)`, whose length argument is the
@@ -1972,13 +2002,22 @@ or ImplicitObjectCreationExpressionSyntax
19722002 // Gated on --flow-locals like the rest of the synthetic-flow emission.
19732003 if ( flowLocals )
19742004 {
1975- // IDisposable fields -> declaration line (the synthetic `acquire`).
2005+ // Owner fields -> declaration line (the synthetic `acquire`): IDisposable fields AND
2006+ // `IMemoryOwner<T>` MemoryPool rentals, whose pooled buffer is released by Dispose() — so a
2007+ // read after that Dispose (including through a Memory VIEW field of the owner) dangles.
19762008 var dispoFieldLine = new Dictionary < string , int > ( StringComparer . Ordinal ) ;
19772009 foreach ( var fd in cls . Members . OfType < FieldDeclarationSyntax > ( ) )
19782010 {
19792011 if ( fd . Modifiers . Any ( mm => mm . IsKind ( SyntaxKind . StaticKeyword ) ) )
19802012 continue ;
1981- if ( ! IsDisposableType ( fd . Declaration . Type . ToString ( ) ) )
2013+ // IDisposable is matched syntactically (so a project's own unresolved `…Stream` etc.
2014+ // still counts); IMemoryOwner is matched on the resolved SYMBOL so a qualified
2015+ // `System.Buffers.IMemoryOwner<T>` / alias / concrete owner type counts too (CodeRabbit/
2016+ // Codex). The cheap `StartsWith` short-circuits the common unqualified spelling first.
2017+ var ftype = fd . Declaration . Type . ToString ( ) ;
2018+ if ( ! IsDisposableType ( ftype )
2019+ && ! ftype . StartsWith ( "IMemoryOwner" , StringComparison . Ordinal )
2020+ && ! IsMemoryOwnerType ( model . GetTypeInfo ( fd . Declaration . Type ) . Type ) )
19822021 continue ;
19832022 foreach ( var v in fd . Declaration . Variables )
19842023 dispoFieldLine [ v . Identifier . Text ] = LineOf ( v ) ;
@@ -2002,6 +2041,18 @@ or ImplicitObjectCreationExpressionSyntax
20022041 }
20032042 if ( releasedAt . Count > 0 )
20042043 {
2044+ // a Memory/ReadOnlyMemory VIEW field that aliases an owner field — `_view = _owner.Memory`
2045+ // (an IMemoryOwner rental) or `_view = _buf.AsMemory(...)` — mapped to its OWNER, so a read
2046+ // of the VIEW field after the owner's release is a use of the released owner (the pooled-
2047+ // buffer view-in-a-field dangle). `ViewOwner` returns the owner name for a bare-field
2048+ // receiver; only owners actually released (in `releasedAt`) fire, checked at the read.
2049+ var viewFieldOwner = new Dictionary < string , string > ( StringComparer . Ordinal ) ;
2050+ foreach ( var a in assigns )
2051+ if ( a . IsKind ( SyntaxKind . SimpleAssignmentExpression )
2052+ && ThisFieldName ( a . Left ) is { } vf
2053+ && FieldViewOwner ( a . Right , model ) is { } vo )
2054+ viewFieldOwner [ vf ] = vo ;
2055+
20052056 // handler method names that are LIVE subscription targets. `+=` subscriptions are
20062057 // keyed by SOURCE|handler so a `-=` removes only the MATCHING one — a handler still
20072058 // `+=`'d to another live source stays live (a name-only set would let one `-=` drop
@@ -2039,7 +2090,7 @@ or ImplicitObjectCreationExpressionSyntax
20392090 && hm . Identifier . Text is not ( "Dispose" or "DisposeAsync" )
20402091 && IsPrivateInstanceHelper ( hm )
20412092 && model . GetDeclaredSymbol ( hm ) is { } hsym
2042- && FirstUnguardedDisposedRead ( b , releasedAt ) is { } r )
2093+ && FirstUnguardedDisposedRead ( b , releasedAt , viewFieldOwner ) is { } r )
20432094 helperReads [ hsym ] = ( r . Field , LineOf ( r . Use ) ) ;
20442095
20452096 foreach ( var hm in cls . Members . OfType < MethodDeclarationSyntax > ( ) )
@@ -2062,9 +2113,10 @@ or ImplicitObjectCreationExpressionSyntax
20622113 {
20632114 if ( node is MemberAccessExpressionSyntax ma
20642115 && ma . Name . Identifier . Text is not ( "Dispose" or "DisposeAsync" )
2065- && ThisFieldName ( ma . Expression ) is { } uf && releasedAt . ContainsKey ( uf ) )
2116+ && ThisFieldName ( ma . Expression ) is { } uf
2117+ && ReleasedOwner ( uf , releasedAt , viewFieldOwner ) is { } owner )
20662118 {
2067- useField = uf ; useLine = LineOf ( ma ) ; triggerPos = ma . SpanStart ;
2119+ useField = owner ; useLine = LineOf ( ma ) ; triggerPos = ma . SpanStart ;
20682120 break ;
20692121 }
20702122 if ( node is InvocationExpressionSyntax call
0 commit comments