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
{{ message }}
Repository navigation
#[target_feature] mismatch on unsafe trait fn vs its impl causes sneaky UB #139368
// This trait is unsealed. Imagine it's in crate A.pubtraitExample{unsafefndemo(&self){}}// Imagine this type is in crate B,// which depends on A.pubstructS;implExampleforS{// This isn't applied to the trait's fn!#[target_feature(enable = "avx2,aes")]unsafefndemo(&self){}}// Imagine this function is in crate C,// which also depends on A but might or might not be used// together with B.//// It seems to be impossible to write the safety comment below.pubfnaccept_dyn(value:&dynExample){// SAFETY: umm ???// Who knows what #[target_feature]// attributes the trait impl has imposed?!unsafe{ value.demo()}}
The "imagine this type is in crate X" is not strictly necessary — this is still a problem within a single crate too. It's just much easier to spot the problem when the code is all together instead of in 3 different and separately evolving crates.
I expected to see this happen: to soundly use unsafe items from a trait, reading the safety comments on the trait and its items should generally be sufficient. A safe (non-unsafe) attribute shouldn't be able to be used in a manner that causes hard-to-find unsoundness and UB. This code should either be rejected outright for tightening the trait's safety requirements in the impl, or at least require #[unsafe(target_feature)] plus raise a lint for the requirements-tightening.
Instead, this happened: this code compiles fine with no warning. The attribute is not unsafe. Even an unintentional "editing error" is likely to silently cause impls' requirements to diverge from the trait's requirements and lead to unsoundness.
I agree this can be a footgun: the safety precondition implied by the #[target_feature(enable=...)] may be missed because it didn't occur to the author when they added the unsafe. I don't think this is a language soundness issue in the technical sense: ultimately there's no UB unless a caller writes unsafe {} to call the method, and they can do so soundly if they know the safety conditions. It's just easier than it should be for them to not know those conditions, and that's worth addressing to make it easier to write sound unsafe code.
Unfortunately, it seems difficult to automatically distinguish mistakes from sound usage:
The safety contract of the method may, in fact, already require the caller to ensure the target features required by the respective impl are available. memchr's internal Vector trait kinda does this for abstracting over different SIMD instruction sets. In that case the impls don't actually add #[target_feature] themselves, but the trait still requires callers to ensure the target features of the relevant impl are enabled. (This trait can afford to be vague about which target features those are because it's for internal use only and only used via static dispatch, so the rest of the library can actually ensure this.)
The impl in question may, in fact, discharge the safety precondition itself, e.g., by ensuring values of the Self type are only constructed if the target features are available. It's a bit unfortunate that this requires marking the trait method as unsafe (possibly with a safety precondition of "this is always safe to call, ignore rustc"). However, the trait method is the optimal place to introduce the target feature in the case of dynamic dispatch. You could write the method body as unsafe { the_actual_impl(self, arg1, arg2, ...) } where only the_actual_impl has the target feature, but then that call can't be inlined due to mismatched target features, so anyone calling the method through the vtable would unnecessarily do two calls (first an indirect one to the method, then another one to the_actual_impl).
Making the attribute unsafe is not quite right: ultimately UB only comes from a call to the method, which requires the caller to write an unsafe block. This is different from existing unsafe attributes, which can introduce UB just by applying them to an item, even if the item is never used at runtime. In contrast, #[target_feature] is only another way to add a precondition for an unsafe function, not by itself unsafe to invoke. This is similar to how an unsafe fn can lay out safety preconditions for callers without any unsafe blocks or attributes in the immediate vicinity. Vec::set_len is the canonical example: only unsafe-to-call because it's relevant for Vec's safety invariant, not because it does anything unsafe on its own. If and when unsafe fields are adopted, this could change. But unsafe fields are a tool to support writing unsafe code more confidently, they weren't needed to plug any soundness holes. I see this issue similarly.
In analogy to unsafe fields, I could see an argument for making #[target_feature] some kind of unsafe attribute. It doesn't need to be unsafe for soundness, but there's a trend in Rust's evolution towards more fine-grained annotations for both introducing and discharging safety-related proof obligations (unsafe fields, unsafe_op_in_unsafe_fn lint, unsafe extern blocks with a mix of safe and unsafe items, etc.). From this POV, it might make sense to call out #[target_feature] separately in the same way that unsafe fields are useful for keeping track of where safety invariants need to be upheld.
I'm curious about your opinion on a possible rustc warn-by-default lint for when a trait impl introduces more target feature requirements than the pub trait being implemented.
I think that would definitely be good idea as a clippy lint. The bar for rustc lints is much higher and it’s always possible to uplift a clippy lint into rustc later. This pattern doesn’t always indicate a mistake, and when it’s actually what you want I don’t think you can silence the lint by writing the code slightly differently, so this lint would probably end up allowed/expected in some places.
The trait defines the maximum that can be expected of callers. If it sets out a target_feature(enable = X), then an impl can safely say target_feature(enable = Y) if Y ⊆ X. If Y ⊂ X, then we fire the refinement lint.
With my proposed extension, we'd say that if Y ⊈ X, then the attribute in the impl must be unsafe and the impl is responsible for ensuring that no call can be made that requires more of callers than X.
Implementation-wise, I would imagine the following PRs:
Add support for unsafe(target_feature(enable = X)), which is identical to target_feature(enable = X) except it disables checks.
Allow setting the target_feature attribute on trait method definition
Add a check for "impl has more feature than trait method definition" (FCW or lint)
Does that seem reasonable? If so, I will slowly get started on them :-)
Sounds reasonable to me. Maybe we'd also want a step for allowing this on safe trait functions. Note also:
Add support for unsafe(target_feature(enable = X)), which is identical to target_feature(enable = X) except it disables checks.
Another difference between these is that unsafe(target_feature(enable = Y)) only makes sense, I think, in impls, so in a trait def it should do what we currently do when an attribute is unnecessarily wrapped in unsafe(..), which is to give an error.
Sounds reasonable to me. Maybe we'd also want a step for allowing this on safe trait functions.
I had included this in (1), but can split it if you think is best.
Note also:
Add support for unsafe(target_feature(enable = X)), which is identical to target_feature(enable = X) except it disables checks.
Another difference between these is that unsafe(target_feature(enable = Y)) only makes sense, I think, in impls, so in a trait def it should do what we currently do when an attribute is unnecessarily wrapped in unsafe(..), which is to give an error.
Does the unsafe version of the attribute possibly make sense on functions? I imagine it could have a similar meaning, of the form "the caller needs to check nothing", but that would require some bits of extra implementation (to track which target_features have been unsafely-enabled and not require unsafe for calls into functions that only have unsafely-enabled additional features)
I had included this in (1), but can split it if you think is best.
Seems OK together. Either way really.
Does the unsafe version of the attribute possibly make sense on functions? I imagine it could have a similar meaning, of the form "the caller needs to check nothing"...
Interesting, yes, I see what you mean. If we wanted that, I think almost certainly we'd want to spell that differently -- not sure how (ideas?). Then this same unsafe thing would make sense on defaulted trait functions as well.
Given the situation and the above plan, and that it's in line with what we had earlier said and foreshadowed, I think we can elide the nomination for now, as we have other fish to fry.
Does the unsafe version of the attribute possibly make sense on functions? I imagine it could have a similar meaning, of the form "the caller needs to check nothing", but that would require some bits of extra implementation (to track which target_features have been unsafely-enabled and not require unsafe for calls into functions that only have unsafely-enabled additional features)
This sounds more and more like there should be an RFC, as there's some amount of design space here...
Reacted by Hanna Kruppe, Jubilee and Predrag Gruevski
In particular, any usage of unsafe(target_feature) where the safety comment boils down to “this function is safe to call because the safety invariant of a parameter’s type imply the target features are enabled” seems to overlap significantly with rust-lang/rfcs#3525 and the currently running project goal it evolved into.
This sounds more and more like there should be an RFC, as there's some amount of design space here...
Yes, agreed. Open to nightly experimentation ahead of that RFC if it's helpful.
Does the unsafe version of the attribute possibly make sense on functions? I imagine it could have a similar meaning, of the form "the caller needs to check nothing"...
Interesting, yes, I see what you mean. If we wanted that, I think almost certainly we'd want to spell that differently -- not sure how (ideas?). Then this same unsafe thing would make sense on defaulted trait functions as well.
Thinking about it, what's started to make sense to me is to have a separate unsafe(target_feature(force = "..")). The semantics would be that target_feature(enable = "..") enables the target features and adds preconditions, while unsafe(target_feature(force = "..")) enables the target features without adding preconditions. Then, with the FCW, we'd migrate uses of target_feature(enable = "..") within impls to unsafe(target_feature(force = "..")) with a hint that suggests instead adding target_feature(enable = "..") to the trait def item.
Separating things in this way would probably make things more clear and enable better diagnostics.
Activity
hanna-kruppe commentedon Apr 4, 2025
I agree this can be a footgun: the safety precondition implied by the
#[target_feature(enable=...)]may be missed because it didn't occur to the author when they added theunsafe. I don't think this is a language soundness issue in the technical sense: ultimately there's no UB unless a caller writesunsafe {}to call the method, and they can do so soundly if they know the safety conditions. It's just easier than it should be for them to not know those conditions, and that's worth addressing to make it easier to write sound unsafe code.Unfortunately, it seems difficult to automatically distinguish mistakes from sound usage:
implare available.memchr's internalVectortrait kinda does this for abstracting over different SIMD instruction sets. In that case the impls don't actually add#[target_feature]themselves, but the trait still requires callers to ensure the target features of the relevantimplare enabled. (This trait can afford to be vague about which target features those are because it's for internal use only and only used via static dispatch, so the rest of the library can actually ensure this.)Selftype are only constructed if the target features are available. It's a bit unfortunate that this requires marking the trait method asunsafe(possibly with a safety precondition of "this is always safe to call, ignore rustc"). However, the trait method is the optimal place to introduce the target feature in the case of dynamic dispatch. You could write the method body asunsafe { the_actual_impl(self, arg1, arg2, ...) }where onlythe_actual_implhas the target feature, but then that call can't be inlined due to mismatched target features, so anyone calling the method through the vtable would unnecessarily do two calls (first an indirect one to the method, then another one tothe_actual_impl).Making the attribute unsafe is not quite right: ultimately UB only comes from a call to the method, which requires the caller to write an
unsafeblock. This is different from existing unsafe attributes, which can introduce UB just by applying them to an item, even if the item is never used at runtime. In contrast,#[target_feature]is only another way to add a precondition for an unsafe function, not by itself unsafe to invoke. This is similar to how anunsafe fncan lay out safety preconditions for callers without anyunsafeblocks or attributes in the immediate vicinity.Vec::set_lenis the canonical example: only unsafe-to-call because it's relevant for Vec's safety invariant, not because it does anything unsafe on its own. If and when unsafe fields are adopted, this could change. But unsafe fields are a tool to support writing unsafe code more confidently, they weren't needed to plug any soundness holes. I see this issue similarly.In analogy to unsafe fields, I could see an argument for making
#[target_feature]some kind of unsafe attribute. It doesn't need to be unsafe for soundness, but there's a trend in Rust's evolution towards more fine-grained annotations for both introducing and discharging safety-related proof obligations (unsafe fields,unsafe_op_in_unsafe_fnlint,unsafe externblocks with a mix of safe and unsafe items, etc.). From this POV, it might make sense to call out#[target_feature]separately in the same way that unsafe fields are useful for keeping track of where safety invariants need to be upheld.obi1kenobi commentedon Apr 4, 2025
I agree with substantially everything you said.
I'm curious about your opinion on a possible rustc warn-by-default lint for when a trait impl introduces more target feature requirements than the pub trait being implemented.
hanna-kruppe commentedon Apr 5, 2025
I think that would definitely be good idea as a clippy lint. The bar for rustc lints is much higher and it’s always possible to uplift a clippy lint into rustc later. This pattern doesn’t always indicate a mistake, and when it’s actually what you want I don’t think you can silence the lint by writing the code slightly differently, so this lint would probably end up allowed/expected in some places.
oli-obk commentedon Apr 5, 2025
Maybe the attribute should be unsafe to apply to impl methods. rust-lang/rfcs#3715
60 remaining items
veluca93 commentedon May 24, 2025
Implementation-wise, I would imagine the following PRs:
unsafe(target_feature(enable = X)), which is identical totarget_feature(enable = X)except it disables checks.target_featureattribute on trait method definitionDoes that seem reasonable? If so, I will slowly get started on them :-)
traviscross commentedon May 24, 2025
Sounds reasonable to me. Maybe we'd also want a step for allowing this on safe trait functions. Note also:
Another difference between these is that
unsafe(target_feature(enable = Y))only makes sense, I think, in impls, so in a trait def it should do what we currently do when an attribute is unnecessarily wrapped inunsafe(..), which is to give an error.veluca93 commentedon May 24, 2025
I had included this in (1), but can split it if you think is best.
Does the unsafe version of the attribute possibly make sense on functions? I imagine it could have a similar meaning, of the form "the caller needs to check nothing", but that would require some bits of extra implementation (to track which target_features have been unsafely-enabled and not require unsafe for calls into functions that only have unsafely-enabled additional features)
traviscross commentedon May 24, 2025
Seems OK together. Either way really.
Interesting, yes, I see what you mean. If we wanted that, I think almost certainly we'd want to spell that differently -- not sure how (ideas?). Then this same unsafe thing would make sense on defaulted trait functions as well.
traviscross commentedon May 25, 2025
@rustbot labels -I-lang-nominated -P-lang-drag-1
Given the situation and the above plan, and that it's in line with what we had earlier said and foreshadowed, I think we can elide the nomination for now, as we have other fish to fry.
cc @rust-lang/lang
RalfJung commentedon May 25, 2025
This sounds more and more like there should be an RFC, as there's some amount of design space here...
hanna-kruppe commentedon May 25, 2025
In particular, any usage of
unsafe(target_feature)where the safety comment boils down to “this function is safe to call because the safety invariant of a parameter’s type imply the target features are enabled” seems to overlap significantly with rust-lang/rfcs#3525 and the currently running project goal it evolved into.traviscross commentedon May 25, 2025
Yes, agreed. Open to nightly experimentation ahead of that RFC if it's helpful.
Thinking about it, what's started to make sense to me is to have a separate
unsafe(target_feature(force = "..")). The semantics would be thattarget_feature(enable = "..")enables the target features and adds preconditions, whileunsafe(target_feature(force = ".."))enables the target features without adding preconditions. Then, with the FCW, we'd migrate uses oftarget_feature(enable = "..")within impls tounsafe(target_feature(force = ".."))with a hint that suggests instead addingtarget_feature(enable = "..")to the trait def item.Separating things in this way would probably make things more clear and enable better diagnostics.
veluca93 commentedon May 25, 2025
I drafted up a RFC following this discussion: rust-lang/rfcs#3819
Rollup merge of rust-lang#141471 - RalfJung:unsafe-fn-in-trait, r=tra…
Unrolled build for #141471
Rollup merge of #141471 - RalfJung:unsafe-fn-in-trait, r=traviscross
Rollup merge of rust-lang#141471 - RalfJung:unsafe-fn-in-trait, r=tra…