You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Unsound: a generic trait impl method's body is never checked against the trait method's spec, so callers assume an unverified ensures and panicking programs verify as safe #190
When a genericimpl block implements a trait method, the impl method's body is never analyzed against the trait method's spec, yet callers are still handed the trait method's #[ensures(..)] as an assumption. A generic impl whose body violates the trait's postcondition is therefore accepted, and every caller trusts the (false) postcondition, so always-panicking programs verify as safe.
The trigger is narrow and precise: the impl block is generic (impl<T> Tr for W<T>). The identical program with a concreteimpl (impl Tr for W) is correctly rejected, because a concrete impl method is analyzed eagerly and a generic one is not.
Minimal reproduction
repro.rs:
#[thrust_macros::context]traitTr{#[thrust_macros::requires(true)]#[thrust_macros::ensures(result > 0)]fnm(&self) -> i32;}#[derive(PartialEq)]structW<T>(T);impl<T> thrust_models::ModelforW<T>whereT: thrust_models::Model{typeTy = Self;}impl<T>TrforW<T>{fnm(&self) -> i32{ -1}// body returns -1, violating `ensures(result > 0)`}fnmain(){let w = W(0i32);let r = w.m();assert!(r > 0);// proved from the (unchecked) trait `ensures`; panics at runtime}
Row 3 (assert!(false) in the generic trait-impl body is not caught) proves the generic trait-impl body is not analyzed at all — this is not merely a "checked against a too-weak spec" case.
Rows 5 and 6 show that generic inherent methods and generic free functionsare analyzed against their bodies when called (their assert!(false) / violated ensures is caught). The skip is specific to generic methods dispatched through a trait.
Root cause
Concrete and generic function defs are registered differently in refine_fn_def (src/analyze/crate_.rs:143-157):
if sig.has_param(){// generic def -> registered as a *deferred* def, analyzed lazily on instantiationself.ctx.register_deferred_def(target_def_id, local_def_id);// (or _without_analysis)}else{// concrete def -> registered eagerlylet expected = analyzer.expected_ty();self.ctx.register_def(target_def_id, expected);}
The eager whole-crate analysis pass, analyze_local_defs (src/analyze/crate_.rs:160-190), only runs bodies for concrete defs — it skips anything deferred, because concrete_def_ty returns None for DefTy::Deferred (src/analyze.rs:420-424):
So a genericimpl method's body is only ever analyzed lazily, through def_ty_with_args (src/analyze.rs:473-533), whose should_analyze() branch is the sole place a deferred body is run(&expected). That branch fires only when the callee is resolved to the impl method's own def-id. For a call dispatched through the trait, the callee type is obtained from the trait method def-id instead (that is where the annotated spec lives), so def_ty_with_args is never invoked on the generic impl method — and its body is never checked. Generic inherent methods don't have this problem: a call resolves directly to the method's def-id, so their deferred body is analyzed (contrast row 5).
Meanwhile the caller still receives the trait's postcondition. expected_ty (src/analyze/local_def.rs:238-308) folds the trait method's requires/ensures into the method's expected type (:255-268), and — since the impl method carries no direct annotations, is_fully_annotated() is false (:166-177) — returns trait_item_ty (:304-305), i.e. the trait method's analyzed type -> { result | result > 0 }. That type is what a call site assumes. The postcondition is thus assumed by callers but never discharged against the generic impl body.
This is exactly the generic-impl gap in the fix from #188 ("check trait-impl methods against the trait method's spec"): #188 makes the check work for concrete impls (which flow through the eager register_def / analyze_local_defs path), but generic impls take the deferred path and slip past it.
Not a numeric-range issue: the constant is -1; no overflow, truncation, or unsigned wraparound is involved.
Scope / when it bites
Any generic impl<..> Trait for Type<..> of a trait method with a spec, whose body violates the trait's ensures (or that internally can panic), called on a concrete instance. The body is silently unverified and callers trust the spec.
Does not affect concrete impl blocks, generic inherent methods, or generic free functions — all three analyze their bodies.
Suggested direction
Ensure a generic impl method's body is analyzed against its (trait-derived) expected type at least once. Options: (a) in analyze_local_defs, additionally analyze deferred trait-impl-method defs by substituting opaque type params for their generics (mirroring the existing polymorphic-body handling already used there for generic free functions / inherent methods), rather than relying on a call site resolving to the impl def-id; or (b) when a trait-dispatched call resolves the callee spec from the trait method, also force def_ty_with_args on the corresponding impl method so its deferred body is run against the trait spec.
Summary
When a generic
implblock implements a trait method, theimplmethod's body is never analyzed against the trait method's spec, yet callers are still handed the trait method's#[ensures(..)]as an assumption. A genericimplwhose body violates the trait's postcondition is therefore accepted, and every caller trusts the (false) postcondition, so always-panicking programs verify assafe.The trigger is narrow and precise: the
implblock is generic (impl<T> Tr for W<T>). The identical program with a concreteimpl(impl Tr for W) is correctly rejected, because a concreteimplmethod is analyzed eagerly and a generic one is not.Minimal reproduction
repro.rs:Thrust reports
safe. At runtimem()returns-1, soassert!(r > 0)always panics — the equivalent plain-Rust program aborts with exit code 101:So a program that unconditionally panics is verified
safe: a soundness hole.Diagnostic contrast
Only two things change across these rows; each is decisive.
impl<T> Tr for W<T>, body-1,assert!(m()>0)safeimpl Tr for W, body-1,assert!(m()>0){ assert!(false); 1 }, result unusedsafe{ assert!(false); 1 }, result unusedimpl<T> W<T> { fn m(&self)->i32 { assert!(false); 1 } }, calledfn f<T>(_:T)->i32 { -1 }withensures(result>0), called,assert!(f(0)>0)Reading:
assert!(false)in the generic trait-impl body is not caught) proves the generic trait-impl body is not analyzed at all — this is not merely a "checked against a too-weak spec" case.assert!(false)/ violatedensuresis caught). The skip is specific to generic methods dispatched through a trait.Root cause
Concrete and generic function defs are registered differently in
refine_fn_def(src/analyze/crate_.rs:143-157):The eager whole-crate analysis pass,
analyze_local_defs(src/analyze/crate_.rs:160-190), only runs bodies for concrete defs — it skips anything deferred, becauseconcrete_def_tyreturnsNoneforDefTy::Deferred(src/analyze.rs:420-424):So a generic
implmethod's body is only ever analyzed lazily, throughdef_ty_with_args(src/analyze.rs:473-533), whoseshould_analyze()branch is the sole place a deferred body isrun(&expected). That branch fires only when the callee is resolved to theimplmethod's own def-id. For a call dispatched through the trait, the callee type is obtained from the trait method def-id instead (that is where the annotated spec lives), sodef_ty_with_argsis never invoked on the genericimplmethod — and its body is never checked. Generic inherent methods don't have this problem: a call resolves directly to the method's def-id, so their deferred body is analyzed (contrast row 5).Meanwhile the caller still receives the trait's postcondition.
expected_ty(src/analyze/local_def.rs:238-308) folds the trait method'srequires/ensuresinto the method's expected type (:255-268), and — since theimplmethod carries no direct annotations,is_fully_annotated()isfalse(:166-177) — returnstrait_item_ty(:304-305), i.e. the trait method's analyzed type-> { result | result > 0 }. That type is what a call site assumes. The postcondition is thus assumed by callers but never discharged against the genericimplbody.This is exactly the generic-
implgap in the fix from #188 ("check trait-impl methods against the trait method's spec"): #188 makes the check work for concrete impls (which flow through the eagerregister_def/analyze_local_defspath), but generic impls take the deferred path and slip past it.Distinct from existing issues
assert!(false)inside the body, whereas here that body is not analyzed at all.sig/ret/paramwhose body violates the declared refinement verifies assafewhen it is not called (unspecified param precondition becomes an inference template that is discharged vacuously) #179 (sig/ret/param-only function whose body violates it when it is never called). Here the method is called and the panic is real at runtime; the spec isrequires/ensureson a trait method, and the failure is that the impl body is never checked, not that an uninstantiated template is discharged vacuously.-1; no overflow, truncation, or unsigned wraparound is involved.Scope / when it bites
impl<..> Trait for Type<..>of a trait method with a spec, whose body violates the trait'sensures(or that internally can panic), called on a concrete instance. The body is silently unverified and callers trust the spec.implblocks, generic inherent methods, or generic free functions — all three analyze their bodies.Suggested direction
Ensure a generic
implmethod's body is analyzed against its (trait-derived) expected type at least once. Options: (a) inanalyze_local_defs, additionally analyze deferred trait-impl-method defs by substituting opaque type params for their generics (mirroring the existing polymorphic-body handling already used there for generic free functions / inherent methods), rather than relying on a call site resolving to the impl def-id; or (b) when a trait-dispatched call resolves the callee spec from the trait method, also forcedef_ty_with_argson the correspondingimplmethod so its deferred body is run against the trait spec.Environment
6953863nightly-2025-09-08(perrust-toolchain.toml)THRUST_SOLVER_ARGS