Skip to content

Commit 6ba65b7

Browse files
Auto merge of #147543 - kstrafe:cve-rs, r=<try>
cve-rs: Fix unsound lifetime extension in HRTB function pointer coercion
2 parents 9725c4b + 16fb05e commit 6ba65b7

11 files changed

+334
-5
lines changed

compiler/rustc_hir_typeck/src/coercion.rs

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -878,9 +878,80 @@ 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
913+
let source_inputs = source_sig.skip_binder().inputs();
914+
let target_inputs = target_sig.skip_binder().inputs();
915+
916+
// If the number of inputs differs, something unusual is happening
917+
if source_inputs.len() != target_inputs.len() {
918+
return Ok(()); // Let normal type checking handle this
919+
}
920+
921+
let mut all_implied_bounds = Vec::new();
922+
923+
for input_ty in source_inputs.iter() {
924+
let bounds = implied_bounds::extract_nested_reference_bounds(self.tcx, *input_ty);
925+
all_implied_bounds.extend(bounds);
926+
}
927+
928+
// If there are no implied bounds, the coercion is safe
929+
if all_implied_bounds.is_empty() {
930+
return Ok(());
931+
}
932+
933+
// Check if target inputs also have nested references with the same structure.
934+
// If both source and target preserve the nested reference structure, the coercion is safe.
935+
// The unsoundness only occurs when we're "collapsing" nested lifetimes.
936+
let target_has_nested_refs = target_inputs
937+
.iter()
938+
.any(|ty| !implied_bounds::extract_nested_reference_bounds(self.tcx, *ty).is_empty());
939+
940+
if target_has_nested_refs {
941+
// Target also has nested references, so the implied bounds structure is preserved.
942+
// This is safe (e.g., fn(&'a &'b T) -> fn(for<'x, 'y> &'x &'y T)).
943+
return Ok(());
944+
}
945+
946+
// Source has implied bounds from nested refs but target doesn't preserve them.
947+
// This is the unsound case (e.g., cve-rs: fn(&'a &'b T) -> for<'x> fn(&'x T)).
948+
debug!(
949+
"check_hrtb_implied_bounds: source has implied bounds but target doesn't preserve nested refs, rejecting coercion"
950+
);
951+
952+
Err(TypeError::Mismatch)
953+
}
954+
884955
fn coerce_from_fn_item(&self, a: Ty<'tcx>, b: Ty<'tcx>) -> CoerceResult<'tcx> {
885956
debug!("coerce_from_fn_item(a={:?}, b={:?})", a, b);
886957
debug_assert!(self.shallow_resolve(a) == a);
@@ -890,8 +961,12 @@ impl<'f, 'tcx> Coerce<'f, 'tcx> {
890961
self.at(&self.cause, self.param_env).normalize(b);
891962

892963
match b.kind() {
893-
ty::FnPtr(_, b_hdr) => {
964+
ty::FnPtr(sig_tys_b, b_hdr) => {
894965
let mut a_sig = a.fn_sig(self.tcx);
966+
967+
// Check implied bounds for HRTB function pointer coercions (issue #25860)
968+
let target_sig = sig_tys_b.with(*b_hdr);
969+
self.check_hrtb_implied_bounds(a_sig, target_sig)?;
895970
if let ty::FnDef(def_id, _) = *a.kind() {
896971
// Intrinsics are not coercible to function pointers
897972
if self.tcx.intrinsic(def_id).is_some() {
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//! Extraction of implied bounds from nested references.
2+
//!
3+
//! This module provides utilities for extracting outlives constraints that are
4+
//! implied by the structure of types, particularly nested references.
5+
//!
6+
//! For example, the type `&'a &'b T` implies that `'b: 'a`, because the outer
7+
//! reference with lifetime `'a` must not outlive the data it points to, which
8+
//! has lifetime `'b`.
9+
//!
10+
//! This is relevant for issue #25860, where the combination of variance and
11+
//! implied bounds on nested references can create soundness holes in HRTB
12+
//! function pointer coercions.
13+
14+
use rustc_middle::ty::{self, ArgOutlivesPredicate, Ty, TyCtxt};
15+
16+
/// Extract implied outlives bounds from a type.
17+
///
18+
/// This function identifies outlives relationships that are structurally
19+
/// required by a type. For example:
20+
/// - `&'a &'b T` implies `'b: 'a`
21+
/// - `&'a (&'b T, &'c U)` implies `'b: 'a` and `'c: 'a`
22+
///
23+
/// These bounds are important for checking the validity of function pointer
24+
/// coercions with higher-rank trait bounds (HRTBs).
25+
#[allow(dead_code)]
26+
pub fn extract_nested_reference_bounds<'tcx>(
27+
tcx: TyCtxt<'tcx>,
28+
ty: Ty<'tcx>,
29+
) -> Vec<ArgOutlivesPredicate<'tcx>> {
30+
let mut bounds = Vec::new();
31+
extract_bounds_recursive(tcx, ty, &mut bounds);
32+
bounds
33+
}
34+
35+
/// Recursively extract implied bounds from a type.
36+
fn extract_bounds_recursive<'tcx>(
37+
tcx: TyCtxt<'tcx>,
38+
ty: Ty<'tcx>,
39+
bounds: &mut Vec<ArgOutlivesPredicate<'tcx>>,
40+
) {
41+
match ty.kind() {
42+
ty::Ref(r_outer, inner_ty, _) => {
43+
// For &'a T, check if T itself is a reference
44+
if let ty::Ref(r_inner, nested_ty, _) = inner_ty.kind() {
45+
// &'a &'b T requires 'b: 'a
46+
// The inner reference must outlive the outer reference
47+
bounds.push(ty::OutlivesPredicate((*r_inner).into(), *r_outer));
48+
49+
// Continue recursively in case of deeper nesting
50+
extract_bounds_recursive(tcx, *nested_ty, bounds);
51+
} else {
52+
// Still recurse for tuples, structs, etc.
53+
extract_bounds_recursive(tcx, *inner_ty, bounds);
54+
}
55+
}
56+
ty::Tuple(tys) => {
57+
// Check each element of the tuple
58+
for ty in tys.iter() {
59+
extract_bounds_recursive(tcx, ty, bounds);
60+
}
61+
}
62+
ty::Adt(_, args) => {
63+
// Check type arguments (could contain references)
64+
for arg in args.iter() {
65+
if let ty::GenericArgKind::Type(ty) = arg.kind() {
66+
extract_bounds_recursive(tcx, ty, bounds);
67+
}
68+
}
69+
}
70+
_ => {
71+
// For other types, no implied bounds
72+
}
73+
}
74+
}
75+
76+
#[cfg(test)]
77+
mod tests {
78+
// Note: These are conceptual tests. Actual tests would need a TyCtxt
79+
// which is only available in integration tests.
80+
}

compiler/rustc_infer/src/infer/outlives/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::infer::region_constraints::ConstraintKind;
1414

1515
pub mod env;
1616
pub mod for_liveness;
17+
pub mod implied_bounds;
1718
pub mod obligations;
1819
pub mod test_type_match;
1920
pub(crate) mod verify;

tests/ui/dropck/issue-28498-ugeh-with-passed-to-fn.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
//@ run-pass
22

3-
// Demonstrate the use of the unguarded escape hatch with a type param in negative position
4-
// to assert that destructor will not access any dead data.
3+
// After fixing issue #25860 with a refined check, this test passes again.
4+
// The coercion from `fn(&'a &'b T)` to `for<'r> fn(&'r &T)` is safe because
5+
// both preserve the nested reference structure.
56
//
67
// Compare with ui/span/issue28498-reject-lifetime-param.rs
78

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//@ check-pass
2+
// Test that implied bounds from type parameters are properly tracked in HRTB contexts.
3+
// The type `&'b &'a ()` implies `'a: 'b`, and this constraint should be preserved
4+
// when deriving supertrait bounds.
5+
6+
trait Subtrait<'a, 'b, R>: Supertrait<'a, 'b> {}
7+
8+
trait Supertrait<'a, 'b> {}
9+
10+
struct MyStruct;
11+
12+
// This implementation is valid: we only implement Supertrait for 'a: 'b
13+
impl<'a: 'b, 'b> Supertrait<'a, 'b> for MyStruct {}
14+
15+
// This implementation is also valid: the type parameter &'b &'a () implies 'a: 'b
16+
impl<'a, 'b> Subtrait<'a, 'b, &'b &'a ()> for MyStruct {}
17+
18+
// This function requires the HRTB on Subtrait
19+
fn need_hrtb_subtrait<S>()
20+
where
21+
S: for<'a, 'b> Subtrait<'a, 'b, &'b &'a ()>,
22+
{
23+
// This should work - the bound on Subtrait with the type parameter
24+
// &'b &'a () implies 'a: 'b, which matches what Supertrait requires
25+
need_hrtb_supertrait::<S>()
26+
}
27+
28+
// This function requires a weaker HRTB on Supertrait
29+
fn need_hrtb_supertrait<S>()
30+
where
31+
S: for<'a, 'b> Supertrait<'a, 'b>,
32+
{
33+
}
34+
35+
fn main() {
36+
need_hrtb_subtrait::<MyStruct>();
37+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// This test demonstrates the unsoundness in issue #84591
2+
// where HRTB on subtraits can imply HRTB on supertraits without
3+
// preserving necessary outlives constraints, allowing unsafe lifetime extension.
4+
//
5+
// This test should FAIL to compile once the fix is implemented.
6+
7+
trait Subtrait<'a, 'b, R>: Supertrait<'a, 'b> {}
8+
9+
trait Supertrait<'a, 'b> {
10+
fn convert<T: ?Sized>(x: &'a T) -> &'b T;
11+
}
12+
13+
fn need_hrtb_subtrait<S, T: ?Sized>(x: &T) -> &T
14+
where
15+
S: for<'a, 'b> Subtrait<'a, 'b, &'b &'a ()>,
16+
{
17+
need_hrtb_supertrait::<S, T>(x)
18+
}
19+
20+
fn need_hrtb_supertrait<S, T: ?Sized>(x: &T) -> &T
21+
where
22+
S: for<'a, 'b> Supertrait<'a, 'b>,
23+
{
24+
S::convert(x)
25+
}
26+
27+
struct MyStruct;
28+
29+
impl<'a: 'b, 'b> Supertrait<'a, 'b> for MyStruct {
30+
fn convert<T: ?Sized>(x: &'a T) -> &'b T {
31+
x
32+
}
33+
}
34+
35+
impl<'a, 'b> Subtrait<'a, 'b, &'b &'a ()> for MyStruct {}
36+
37+
fn extend_lifetime<'a, 'b, T: ?Sized>(x: &'a T) -> &'b T {
38+
need_hrtb_subtrait::<MyStruct, T>(x)
39+
//~^ ERROR lifetime may not live long enough
40+
}
41+
42+
fn main() {
43+
let d;
44+
{
45+
let x = String::from("Hello World");
46+
d = extend_lifetime(&x);
47+
}
48+
println!("{}", d);
49+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
error: lifetime may not live long enough
2+
--> $DIR/hrtb-lifetime-extend-unsound.rs:38:5
3+
|
4+
LL | fn extend_lifetime<'a, 'b, T: ?Sized>(x: &'a T) -> &'b T {
5+
| -- -- lifetime `'b` defined here
6+
| |
7+
| lifetime `'a` defined here
8+
LL | need_hrtb_subtrait::<MyStruct, T>(x)
9+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function was supposed to return data with lifetime `'b` but it is returning data with lifetime `'a`
10+
|
11+
= help: consider adding the following bound: `'a: 'b`
12+
13+
error: aborting due to 1 previous error
14+
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Test for issue #25860: the cve-rs lifetime expansion exploit
2+
// This demonstrates casting an arbitrary lifetime to 'static using
3+
// HRTB and variance, which should NOT be allowed.
4+
//
5+
// Once fixed, this test should fail to compile with an appropriate error.
6+
7+
static STATIC_UNIT: &'static &'static () = &&();
8+
9+
/// This function has an implied bound: 'b: 'a
10+
/// (because &'a &'b () requires 'b to outlive 'a)
11+
fn lifetime_translator<'a, 'b, T: ?Sized>(
12+
_val_a: &'a &'b (),
13+
val_b: &'b T
14+
) -> &'a T {
15+
val_b
16+
}
17+
18+
/// The exploit: expand any lifetime 'a to any other lifetime 'b
19+
/// This is UNSOUND because 'a may not outlive 'b
20+
fn expand<'a, 'b, T: ?Sized>(x: &'a T) -> &'b T {
21+
// This coercion should fail because:
22+
// 1. lifetime_translator requires 'b: 'a (implied by &'a &'b ())
23+
// 2. When we call f(STATIC_UNIT, x), 'a gets unified with the lifetime of x
24+
// 3. But there's no guarantee that 'a: 'b!
25+
let f: for<'x> fn(_, &'x T) -> &'b T = lifetime_translator;
26+
//~^ ERROR mismatched types
27+
f(STATIC_UNIT, x)
28+
}
29+
30+
/// Concrete example: extend a local reference to 'static
31+
fn extend_to_static<'a, T: ?Sized>(x: &'a T) -> &'static T {
32+
expand(x)
33+
}
34+
35+
fn main() {
36+
// Use-after-free: this should not compile!
37+
let dangling: &'static str;
38+
{
39+
let local = String::from("This should not escape!");
40+
dangling = extend_to_static(&local);
41+
}
42+
println!("{}", dangling); // UB: use after free!
43+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
error[E0308]: mismatched types
2+
--> $DIR/cve-rs-lifetime-expansion.rs:25:44
3+
|
4+
LL | let f: for<'x> fn(_, &'x T) -> &'b T = lifetime_translator;
5+
| ----------------------------- ^^^^^^^^^^^^^^^^^^^ types differ
6+
| |
7+
| expected due to this
8+
|
9+
= note: expected fn pointer `for<'x> fn(_, &'x T) -> &'b T`
10+
found fn item `for<'a, 'b> fn(&'a &'b (), &'b _) -> &'a _ {lifetime_translator::<_>}`
11+
12+
error: aborting due to 1 previous error
13+
14+
For more information about this error, try `rustc --explain E0308`.

tests/ui/implied-bounds/implied-bounds-on-nested-references-plus-variance-2.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
//@ check-pass
2-
//@ known-bug: #25860
1+
// Issue #25860: This exploit is now FIXED and should fail to compile
2+
// Previously marked as known-bug, now correctly rejected
33

44
static UNIT: &'static &'static () = &&();
55

66
fn foo<'a, 'b, T>(_: &'a &'b (), v: &'b T, _: &()) -> &'a T { v }
77

88
fn bad<'a, T>(x: &'a T) -> &'static T {
99
let f: fn(_, &'a T, &()) -> &'static T = foo;
10+
//~^ ERROR mismatched types
1011
f(UNIT, x, &())
1112
}
1213

0 commit comments

Comments
 (0)