@@ -303,10 +303,22 @@ pub fn build_stream_pair_graph(
303303// legal bidirectional-pipe pattern (two independent streams
304304// in opposite directions, each individually acyclic).
305305//
306- // Checks **(ii) bounded-channel capacity** and **(iv) resource
307- // lifetime across async boundaries** remain TODO — they need
308- // information beyond `CanonicalEntry` / `ParsedComponent` (capacity
309- // flags + per-handle lifetime tracking).
306+ // (iv) Resource lifetime across async boundaries — a resource
307+ // handle (`own<R>` / `borrow<R>`) carried as a `stream<T>` /
308+ // `future<T>` element type. A `borrow<R>` is only valid within
309+ // its lending call, but a stream/future is read *after* that
310+ // call returns across the async boundary → use-after-scope; an
311+ // `own<R>` transferred into a stream and then dropped by the
312+ // producer while the consumer still holds it is the
313+ // drop-while-referenced hazard #142 names. See
314+ // [`resource_lifetime_issues`].
315+ //
316+ // Check **(ii) bounded-channel capacity** is **not applicable**: the
317+ // Component-Model canonical ABI has no bounded-channel / capacity
318+ // concept — `stream.new` (`CanonicalEntry::StreamNew { ty }`) takes no
319+ // capacity, and streams are unbounded by construction. There is nothing
320+ // in the component binary to validate; "declare a capacity" presupposes
321+ // an annotation mechanism the ABI does not provide (#142).
310322
311323/// A validation issue raised by [`validate_stream_pair_graph`].
312324#[ derive( Debug , Clone , PartialEq , Eq ) ]
@@ -329,6 +341,20 @@ pub enum StreamValidationIssue {
329341 /// ≥ 3. Could deadlock at runtime if every component is waiting on
330342 /// inbound stream data before producing outbound data.
331343 Cycle { component_cycle : Vec < usize > } ,
344+ /// Check (iv): a `stream<T>` / `future<T>` carries a resource handle
345+ /// as its element type. `owned == false` is a `borrow<R>` —
346+ /// definitely invalid, the borrow cannot outlive its lending call
347+ /// across the async boundary. `owned == true` is an `own<R>` —
348+ /// permitted only if the producer never drops the handle while the
349+ /// consumer still references it (the drop-while-referenced hazard
350+ /// meld cannot rule out statically). `descriptor` is the offending
351+ /// `stream<…>` / `future<…>` type descriptor.
352+ ResourceLifetime {
353+ component : usize ,
354+ descriptor : String ,
355+ resource_type_id : u32 ,
356+ owned : bool ,
357+ } ,
332358}
333359
334360/// Return every `stream<T>` element type reachable from the given
@@ -685,6 +711,64 @@ pub fn validate_stream_pair_graph(
685711 issues
686712}
687713
714+ /// Check (iv): flag any `stream<T>` / `future<T>` whose element type is
715+ /// a resource handle (`own<R>` / `borrow<R>`).
716+ ///
717+ /// The P3Async descriptor records the element type as the Debug form of
718+ /// the wasmparser `ComponentValType`, so a handle element appears as
719+ /// `stream<Type(N)>` where component type index `N` resolves to a
720+ /// `Defined(Own(R))` / `Defined(Borrow(R))`. We parse the `Type(N)`
721+ /// index out and reuse [`ParsedComponent::resolve_to_resource`] — the
722+ /// same `Type(idx)` → handle chase meld already applies to function
723+ /// params — rather than trusting the Debug text for the handle kind.
724+ ///
725+ /// Limitations (acknowledged, not targets): only a *direct* handle
726+ /// element is detected. A handle nested inside a composite element
727+ /// (`stream<list<own<R>>>` → `stream<List(..)>`) is not flagged, the
728+ /// same boundary as [`stream_elements_in_valtype`].
729+ pub fn resource_lifetime_issues ( components : & [ ParsedComponent ] ) -> Vec < StreamValidationIssue > {
730+ let mut issues = Vec :: new ( ) ;
731+ for ( ci, comp) in components. iter ( ) . enumerate ( ) {
732+ for ty in & comp. types {
733+ let ComponentTypeKind :: P3Async ( desc) = & ty. kind else {
734+ continue ;
735+ } ;
736+ let Some ( inner) = desc
737+ . strip_prefix ( "stream<" )
738+ . or_else ( || desc. strip_prefix ( "future<" ) )
739+ . and_then ( |s| s. strip_suffix ( '>' ) )
740+ else {
741+ continue ;
742+ } ;
743+ let Some ( idx) = parse_type_index ( inner. trim ( ) ) else {
744+ continue ;
745+ } ;
746+ if let Some ( ( resource_type_id, owned) ) =
747+ comp. resolve_to_resource ( & ComponentValType :: Type ( idx) )
748+ {
749+ issues. push ( StreamValidationIssue :: ResourceLifetime {
750+ component : ci,
751+ descriptor : desc. clone ( ) ,
752+ resource_type_id,
753+ owned,
754+ } ) ;
755+ }
756+ }
757+ }
758+ issues
759+ }
760+
761+ /// Parse the wasmparser `ComponentValType::Type(N)` Debug form
762+ /// (`"Type(N)"`) back to its index. Returns `None` for any other shape
763+ /// (e.g. `"Primitive(U8)"`, `"List(..)"`).
764+ fn parse_type_index ( s : & str ) -> Option < u32 > {
765+ s. strip_prefix ( "Type(" ) ?
766+ . strip_suffix ( ')' ) ?
767+ . trim ( )
768+ . parse ( )
769+ . ok ( )
770+ }
771+
688772/// Cycle-only sub-pass — exposed so tests that only care about (iii)
689773/// don't have to construct `ParsedComponent` fixtures.
690774pub fn cycle_issues_from_pairs ( graph : & StreamPairGraph ) -> Vec < StreamValidationIssue > {
@@ -1544,4 +1628,98 @@ mod tests {
15441628 "matching stream types on resolved edge must not raise; got {issues:?}"
15451629 ) ;
15461630 }
1631+
1632+ // ─── (iv) resource lifetime across async boundaries ──────────────
1633+
1634+ /// Pin the wasmparser `ComponentValType::Type(N)` Debug form that
1635+ /// `parse_type_index` depends on. If a wasmparser upgrade changes
1636+ /// this, the resource-lifetime check would silently stop detecting
1637+ /// handle elements — this test fails first.
1638+ #[ test]
1639+ fn wasmparser_type_debug_form_is_stable ( ) {
1640+ assert_eq ! (
1641+ format!( "{:?}" , wasmparser:: ComponentValType :: Type ( 7 ) ) ,
1642+ "Type(7)"
1643+ ) ;
1644+ assert_eq ! ( parse_type_index( "Type(7)" ) , Some ( 7 ) ) ;
1645+ assert_eq ! ( parse_type_index( "Primitive(U8)" ) , None ) ;
1646+ assert_eq ! ( parse_type_index( "List(..)" ) , None ) ;
1647+ }
1648+
1649+ /// Build a component whose type table is all defined types, with a
1650+ /// matching all-`Defined` `component_type_defs` so
1651+ /// `get_type_definition(idx)` (and thus `resolve_to_resource`)
1652+ /// resolves `Type(idx)` to `types[idx]`.
1653+ fn comp_with_defined_types ( types : Vec < ComponentType > ) -> ParsedComponent {
1654+ let defs = vec ! [ crate :: parser:: ComponentTypeDef :: Defined ; types. len( ) ] ;
1655+ let mut c = make_component ( vec ! [ ] , vec ! [ ] , types, vec ! [ ] ) ;
1656+ c. component_type_defs = defs;
1657+ c
1658+ }
1659+
1660+ fn defined ( vt : ComponentValType ) -> ComponentType {
1661+ ComponentType {
1662+ kind : ComponentTypeKind :: Defined ( vt) ,
1663+ }
1664+ }
1665+
1666+ #[ test]
1667+ fn ls_r_14_borrow_handle_in_stream_flagged ( ) {
1668+ // types[0] = stream<Type(1)>; types[1] = borrow<resource 42>.
1669+ let comp = comp_with_defined_types ( vec ! [
1670+ stream_type( "Type(1)" ) ,
1671+ defined( ComponentValType :: Borrow ( 42 ) ) ,
1672+ ] ) ;
1673+ let issues = resource_lifetime_issues ( & [ comp] ) ;
1674+ assert_eq ! ( issues. len( ) , 1 , "borrow-in-stream must flag: {issues:?}" ) ;
1675+ match & issues[ 0 ] {
1676+ StreamValidationIssue :: ResourceLifetime {
1677+ component,
1678+ resource_type_id,
1679+ owned,
1680+ ..
1681+ } => {
1682+ assert_eq ! ( * component, 0 ) ;
1683+ assert_eq ! ( * resource_type_id, 42 ) ;
1684+ assert ! ( !owned, "borrow ⇒ owned = false" ) ;
1685+ }
1686+ other => panic ! ( "expected ResourceLifetime, got {other:?}" ) ,
1687+ }
1688+ }
1689+
1690+ #[ test]
1691+ fn ls_r_14_own_handle_in_future_flagged_as_owned ( ) {
1692+ // types[0] = future<Type(1)>; types[1] = own<resource 5>.
1693+ let future_ty = ComponentType {
1694+ kind : ComponentTypeKind :: P3Async ( "future<Type(1)>" . to_string ( ) ) ,
1695+ } ;
1696+ let comp = comp_with_defined_types ( vec ! [ future_ty, defined( ComponentValType :: Own ( 5 ) ) ] ) ;
1697+ let issues = resource_lifetime_issues ( & [ comp] ) ;
1698+ assert_eq ! ( issues. len( ) , 1 , "own-in-future must flag: {issues:?}" ) ;
1699+ assert ! (
1700+ matches!(
1701+ & issues[ 0 ] ,
1702+ StreamValidationIssue :: ResourceLifetime {
1703+ owned: true ,
1704+ resource_type_id: 5 ,
1705+ ..
1706+ }
1707+ ) ,
1708+ "own ⇒ owned = true; got {:?}" ,
1709+ issues[ 0 ]
1710+ ) ;
1711+ }
1712+
1713+ #[ test]
1714+ fn ls_r_14_primitive_element_stream_not_flagged ( ) {
1715+ // A plain data stream carries no handle — must not flag.
1716+ let comp = comp_with_defined_types ( vec ! [
1717+ stream_type( "Primitive(U8)" ) ,
1718+ defined( ComponentValType :: Primitive ( PrimitiveValType :: U8 ) ) ,
1719+ ] ) ;
1720+ assert ! (
1721+ resource_lifetime_issues( & [ comp] ) . is_empty( ) ,
1722+ "primitive-element stream must not flag (iv)"
1723+ ) ;
1724+ }
15471725}
0 commit comments