@@ -878,9 +878,100 @@ impl<'f, 'tcx> Coerce<'f, 'tcx> {
878878 debug ! ( ?fn_ty_a, ?b, "coerce_from_fn_pointer" ) ;
879879 debug_assert ! ( self . shallow_resolve( b) == b) ;
880880
881+ // Check implied bounds for HRTB function pointer coercions (issue #25860)
882+ if let ty:: FnPtr ( sig_tys_b, hdr_b) = b. kind ( ) {
883+ let target_sig = sig_tys_b. with ( * hdr_b) ;
884+ self . check_hrtb_implied_bounds ( fn_ty_a, target_sig) ?;
885+ }
886+
881887 self . coerce_from_safe_fn ( fn_ty_a, b, None )
882888 }
883889
890+ /// Validates that implied bounds from nested references in the source
891+ /// function signature are satisfied when coercing to an HRTB function pointer.
892+ ///
893+ /// This prevents the soundness hole in issue #25860 where lifetime bounds
894+ /// can be circumvented through HRTB function pointer coercion.
895+ ///
896+ /// For example, a function with signature `fn<'a, 'b>(_: &'a &'b (), v: &'b T) -> &'a T`
897+ /// has an implied bound `'b: 'a` from the type `&'a &'b ()`. When coercing to
898+ /// `for<'x> fn(_, &'x T) -> &'static T`, we must ensure that the implied bound
899+ /// can be satisfied, which it cannot in this case.
900+ fn check_hrtb_implied_bounds (
901+ & self ,
902+ source_sig : ty:: PolyFnSig < ' tcx > ,
903+ target_sig : ty:: PolyFnSig < ' tcx > ,
904+ ) -> Result < ( ) , TypeError < ' tcx > > {
905+ use rustc_infer:: infer:: outlives:: implied_bounds;
906+
907+ // Only check if target has HRTB (bound variables)
908+ if target_sig. bound_vars ( ) . is_empty ( ) {
909+ return Ok ( ( ) ) ;
910+ }
911+
912+ // Extract implied bounds from the source signature's input types and return type
913+ let source_sig_unbound = source_sig. skip_binder ( ) ;
914+ let target_sig_unbound = target_sig. skip_binder ( ) ;
915+ let source_inputs = source_sig_unbound. inputs ( ) ;
916+ let target_inputs = target_sig_unbound. inputs ( ) ;
917+ let source_output = source_sig_unbound. output ( ) ;
918+ let target_output = target_sig_unbound. output ( ) ;
919+
920+ // If the number of inputs differs, let normal type checking handle this
921+ if source_inputs. len ( ) != target_inputs. len ( ) {
922+ return Ok ( ( ) ) ;
923+ }
924+
925+ // Check inputs: whether the source carries nested-reference implied bounds
926+ let source_has_nested = source_inputs
927+ . iter ( )
928+ . any ( |ty| implied_bounds:: has_nested_reference_implied_bounds ( self . tcx , * ty) ) ;
929+
930+ // Check if target inputs also have nested references with the same structure.
931+ // If both source and target preserve the nested reference structure, the coercion is
932+ // sound.
933+ // The unsoundness only occurs when we're "collapsing" nested lifetimes.
934+ let target_has_nested_refs = target_inputs
935+ . iter ( )
936+ . any ( |ty| implied_bounds:: has_nested_reference_implied_bounds ( self . tcx , * ty) ) ;
937+
938+ if source_has_nested && !target_has_nested_refs {
939+ // Source inputs have implied bounds from nested refs but target inputs don't
940+ // preserve them. This is the unsound case (e.g., cve-rs: fn(&'a &'b T) -> for<'x> fn(&'x T)).
941+ return Err ( TypeError :: Mismatch ) ;
942+ }
943+
944+ // Additionally, validate RETURN types. If the source return has nested references
945+ // with distinct regions (implying an outlives relation), then the target return must
946+ // preserve that distinguishing structure; otherwise lifetimes can be "collapsed" away.
947+ let source_ret_nested_distinct =
948+ implied_bounds:: has_nested_reference_with_distinct_regions ( self . tcx , source_output) ;
949+ if source_ret_nested_distinct {
950+ let target_ret_nested_distinct =
951+ implied_bounds:: has_nested_reference_with_distinct_regions ( self . tcx , target_output) ;
952+ if !target_ret_nested_distinct {
953+ // Reject: collapsing nested return structure (e.g., &'a &'b -> &'x &'x or no nested)
954+ return Err ( TypeError :: Mismatch ) ;
955+ }
956+ } else {
957+ // If there is nested structure in the source return (not necessarily distinct),
958+ // require the target to keep nested structure too.
959+ let source_ret_nested =
960+ implied_bounds:: has_nested_reference_implied_bounds ( self . tcx , source_output) ;
961+ if source_ret_nested {
962+ let target_ret_nested =
963+ implied_bounds:: has_nested_reference_implied_bounds ( self . tcx , target_output) ;
964+ if !target_ret_nested {
965+ return Err ( TypeError :: Mismatch ) ;
966+ }
967+ }
968+ }
969+
970+ // Source inputs had implied bounds but target did not preserve them (handled above), or
971+ // return types collapsed. If neither condition triggered, accept the coercion.
972+ Ok ( ( ) )
973+ }
974+
884975 fn coerce_from_fn_item ( & self , a : Ty < ' tcx > , b : Ty < ' tcx > ) -> CoerceResult < ' tcx > {
885976 debug ! ( "coerce_from_fn_item(a={:?}, b={:?})" , a, b) ;
886977 debug_assert ! ( self . shallow_resolve( a) == a) ;
@@ -890,8 +981,12 @@ impl<'f, 'tcx> Coerce<'f, 'tcx> {
890981 self . at ( & self . cause , self . param_env ) . normalize ( b) ;
891982
892983 match b. kind ( ) {
893- ty:: FnPtr ( _ , b_hdr) => {
984+ ty:: FnPtr ( sig_tys_b , b_hdr) => {
894985 let mut a_sig = a. fn_sig ( self . tcx ) ;
986+
987+ // Check implied bounds for HRTB function pointer coercions (issue #25860)
988+ let target_sig = sig_tys_b. with ( * b_hdr) ;
989+ self . check_hrtb_implied_bounds ( a_sig, target_sig) ?;
895990 if let ty:: FnDef ( def_id, _) = * a. kind ( ) {
896991 // Intrinsics are not coercible to function pointers
897992 if self . tcx . intrinsic ( def_id) . is_some ( ) {
0 commit comments