Repository navigation
Description
I tried this code: (playground)
// This trait is unsealed. Imagine it's in crate A.
pub trait Example {
unsafe fn demo(&self) {}
}
// Imagine this type is in crate B,
// which depends on A.
pub struct S;
impl Example for S {
// This isn't applied to the trait's fn!
#[target_feature(enable = "avx2,aes")]
unsafe fn demo(&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.
pub fn accept_dyn(value: &dyn Example) {
// 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.
Meta
rustc --version --verbose:
1.88.0-nightly (2025-04-02 d5b4c2e4f19b6d703737)
From the playground.
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…