Skip to content

#[target_feature] mismatch on unsafe trait fn vs its impl causes sneaky UB #139368

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
obi1kenobi opened this issue Apr 4, 2025 · 59 comments
Open
Labels
C-bug Category: This is a bug. I-lang-radar Items that are on lang's radar and will need eventual work or consideration. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-lang Relevant to the language team, which will review and decide on the PR/issue.

Comments

@obi1kenobi
Copy link
Member

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 A.
//
// 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.

@obi1kenobi obi1kenobi added the C-bug Category: This is a bug. label Apr 4, 2025
@rustbot rustbot added the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label Apr 4, 2025
@bjorn3 bjorn3 added I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness F-target_feature_11 target feature 1.1 RFC labels Apr 4, 2025
@rustbot rustbot added the I-prioritize Issue: Indicates that prioritization has been requested for this issue. label Apr 4, 2025
@bjorn3 bjorn3 added I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness I-prioritize Issue: Indicates that prioritization has been requested for this issue. and removed I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness I-prioritize Issue: Indicates that prioritization has been requested for this issue. labels Apr 4, 2025
@hanna-kruppe
Copy link
Contributor

hanna-kruppe commented 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 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.

@obi1kenobi
Copy link
Member Author

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.

@jieyouxu jieyouxu added T-lang Relevant to the language team, which will review and decide on the PR/issue. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Apr 5, 2025
@hanna-kruppe
Copy link
Contributor

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
Copy link
Contributor

oli-obk commented Apr 5, 2025

Maybe the attribute should be unsafe to apply to impl methods. rust-lang/rfcs#3715

@hanna-kruppe
Copy link
Contributor

hanna-kruppe commented Apr 5, 2025

RFC 3715 is interesting because:

  • #[derive(unsafe(Trait))] means unsafe in the sense of “proof obligations are discharged here” — consistent with existing unsafe attributes
  • #[proc_macro_derive(Trait, unsafe)] uses a slightly different syntax for the “introduces proof obligations for other parts of the code” kind of unsafe

Target feature attributes are the latter kind of unsafe so perhaps it should be spelled #[target_feature(enable=…, unsafe] rather than #[unsafe(target_feature(…))].

Note, however that it’s really only relevant for trait impls, not for inherent impls or free functions — without traits in the mix, there’s only one place where preconditions are defined and the caller always knows precisely which item will be called. Requiring the unsafe token in those contexts is pointless and would negate the recently stabilized “target feature 1.1” improvements. I’m not sure how I feel about making the attribute even more context dependent for the sake of what’s effectively a lint.

@bjorn3
Copy link
Member

bjorn3 commented Apr 5, 2025

Target feature attributes are the latter kind of unsafe so perhaps it should be spelled #[target_feature(enable=…, unsafe] rather than #[unsafe(target_feature(…))].

I did say it is actually the former kind of unsafe. The #[unsafe(target_feature)] discharges the safety requirements of the intrinsics called within this method based on the safety requirements of the method declaration in the trait definition.

@obi1kenobi
Copy link
Member Author

I agree with this. And so long as it only discharges those requirements, I think #[unsafe(target_feature)] sounds great.

My remaining concern is that #[target_feature] on impls of trait methods can also be the other kind of unsafe too: they can add features to the list specified in the trait definition, which users of dyn Trait or impl Trait are likely to be unaware of. This feels at minimum like a candidate for a clippy::suspicious lint to me, and possibly worth something akin to #[target_feature(.., unsafe)] for the "additions" list as well.

My use of syntax above is in reference to the feature itself, not a vote for a particular syntactic choice. I'll leave those to people way more qualified than me :)

@hanna-kruppe
Copy link
Contributor

hanna-kruppe commented Apr 5, 2025

I suppose this is a matter of perspective. While drafting my first comment in this issue, I was leaning in the same direction for a while. But ultimately I think it's more useful to view it only as introducing a precondition the caller has to fulfill, because that's the approach that works across the entire language and doesn't require any special pleading for traits.

It's language UB to call a #[target_feature] function if the target features aren't actually available at the time of the call, even if the function doesn't call any intrinsics. For example, it's UB to call #[target_feature(enable = "avx2")] fn nop() {} without having AVX2. But there's no UB if the function is never called! Thus, the attribute can't be only about discharging requirements for other operations on the body, it also imposes a safety requirement on the function being called at all. In particular, it's not possible for a #[target_feature] function to make itself safe-to-call by doing something like assert!(is_x86_feature_detected!("avx2")); before doing its actual work. (It is possible with #[cfg], of course, but that's the far less interesting case.)

We may then use this precondition to call other functions requiring the same target feature, which is why such calls are sound: we're only propagating what we got from the caller. Before target feature 1.1, this was annoyingly explicit: every function with #[target_feature] was unsafe to call, and inside them you'd again wrap the calls to other target feature functions (including but not limited to intrinsics) in an unsafe {} block to call them. With target feature 1.1, the simple and mechanical cases are effectively inferred. But the preconditions are still there:

  • Calling a safe target feature function from another function that doesn't have the right target feature attributes still requires unsafe {} (caller needs to check that the feature is available).
  • Safe target feature functions can't be coerced to safe function pointers, because then the compiler loses track of who calls it and what target features they have. They also can't occur in trait impls, for the same reason.
  • Global handlers like the panic hook can't be marked with #[target_feature] either, not even if they're declared unsafe, because they need to be safe to call from anywhere in the program. The person implementing those handlers can't unilaterally assert that those callers will ensure the target feature is available, because the callers have no way of knowing about it!

Returning to trait impls: yes, the body of the impl method can assume that the target feature is available and thus freely call other functions that require those features (which the compiler will happily let you do). But the immediate reason for that is the same as for free/inherent functions: calling the method would be UB if the features weren't available (again: even if the method body is actually empty), so in general it's unsafe to call the method and this precondition must be propagated up the call stack. The only thing that's different about traits is where those preconditions should be documented so the caller can see them.

@obi1kenobi
Copy link
Member Author

obi1kenobi commented Apr 5, 2025

The only thing that's different about traits is where those preconditions should be documented so the caller can see them.

This is kind of a key thing though, no?

If a trait specifies a safe function, an impl can't (and shouldn't be able to) make that function unsafe by adding new safety obligations to the (previously empty) set.

In this case, it sounds like you're implying that adding new safety obligations is fine because the set wasn't originally empty this time. That doesn't seem right to me.

So I wouldn't agree it's just a documentation issue, especially if no concrete way to resolve the issue purely at the level of documentation is presented. I don't see a feasible way for this to be solved at the documentation level alone, and I'm skeptical one exists at all.

@hanna-kruppe
Copy link
Contributor

hanna-kruppe commented Apr 5, 2025

I'm not implying unilaterally adding a new safety precondition at the impl level is "fine". A Rust program written that way isn't sound. A trait impl defining an unsafe-to-call method can only rely on the preconditions stated by the trait's documentation for that method, not add new ones that it would like to have. But at the language level, it seems most useful to me to classify #[target_feature] as only introducing a precondition. That's consistent with how it works for free functions and inherent impls (which don't have another source of preconditions they need to match) and sufficient to explain why there can be language UB if the trait impl goes beyond what the trait definition documented. The challenge of ensuring that trait definition and trait impl agree on what the preconditions are isn't really unique to target features. It may appear trivial in most cases, but impls are allowed to refine the safety preconditions stated by the trait (including dropping them entirely, once #100706 is stabilized). As a silly example:

trait Foo {
    /// # Safety
    /// `n` must be a prime number.
    unsafe fn foo(n: u32);
}

impl Foo for () {
    /// # Safety
    /// `n` must be odd. Note that `Foo` requires prime numbers, but
    /// composite odd numbers are also fine for this impl.
    unsafe fn foo(n: u32) {
        // ...
    }
}

This impl is unsound because the author of the impl forgot that there is an even prime number, so their supposed refinement of Foo::foo's safety precondition is actually introducing a new precondition not documented in the trait. This might lead to language UB in the method body due to some unsafe {} block that relies on the illegitimate precondition. But the method might also consist entirely of safe code, e.g., storing the value of n somewhere. This might still ultimately result in UB if the stored value is later used elsewhere in an unsafe {} block that discharges its obligation by arguing:

SAFETY: n must be odd here. It came from a call to <() as Foo>::foo and we haven't modified it since then. Thus, the caller of that function must have ensured that n is odd.

In such a case, I'd still assign the blame for the resulting UB to the fact that the author of impl Foo for () incorrectly refined the precondition. And yet, the impl doesn't contain any unsafe keyword which we might blame for incorrect discharging an obligation (only one for affirming that foo has some safety preconditions). The only occurrence of unsafe in that impl is in the "imposing precondition on the caller" sense, and the mistake is purely in doc comments.

In that sense, #[target_feature] is better because it's at least compiler-visible that a precondition is being introduced. @bjorn3 is right that the person writing the impl should check that the precondition stated on the trait actually implies all the preconditions on the impl's version of the method. A lint or a modification of the attribute syntax to include the unsafe token may help with this. But I don't think it's useful to extend the "discharging obligations" meaning of unsafe to also cover introducing and refining preconditions. Those two aspects are separate but complementary: soundness can't be established solely by focusing on the parts that discharge obligations, the surrounding code that defines safety-relevant pre- and postconditions also matters, even if it is safe code.

@obi1kenobi
Copy link
Member Author

Broadly, I'm onboard with that 👍 In particular, I agree that incorrect refinements should be considered unsound, and that only the trait's safety requirements should be relevant to the caller.

Just one thing I'd push back on:

the person writing the impl should check that the precondition stated on the trait actually implies all the preconditions on the impl's version of the method.

I think "the Rust way" is that whenever possible, built-in tooling (rustc, clippy, etc.) should check this instead of relying on humans. It obviously isn't possible every time (e.g. the odd numbers and primes example), but with #[target_feature] it definitely is possible and I think we should do it.

Between a rustc lint and a clippy lint, I personally would advocate for a rustc lint because I believe we've all agreed this is an unsoundness issue. But if you all think it should go to clippy instead, we can move the issue there.

@hanna-kruppe
Copy link
Contributor

hanna-kruppe commented Apr 5, 2025

To be clear, I do not agree this is a language soundness issue. As stated before, I'd say it's a "language makes it hard to write sound unsafe code" issue, like the motivation for unsafe fields. You may call that a soundness issue, but it's different from when rustc emits incorrect code or the type checker lets you write transmute without any unsafe tokens.

I think it would be completely defensible to introduce an unsafe token in the attribute syntax (as long as it's only required in trait impls). I just believe that it's best understood as the "introducing preconditions" kind of unsafe, not a new flavor of discharging. Either way the unsafe token would give impl authors a chance to realize that they're adding another precondition by writing this attribute (even/especially if they haven't internalized the precondition/discharging distinction). Note that even then, the compiler can only prompt the author to check their work and pinky-promise that they're refining the trait method's precondition. It's not possible to remove the need for a human to check this even with #[target_feature] -- the compiler knows that the attribute introduces a precondition, but can't automatically relate it to the other preconditions stated in comments.

But requiring the unsafe in the attribute that is a backwards incompatible change so it would need to wait for the next edition. In the meantime, a lint is useful. Since the bar for rustc lints is relatively higher and clippy already has many lints aimed at preventing mistakes in unsafe code, I'd suggest implementing it in clippy is a natural first step.

@obi1kenobi
Copy link
Member Author

Sorry for the confusion — my opinion is "the impl is unsound and should be flagged" and not "this Rust feature is unsound and should be rolled back."

@zachs18
Copy link
Contributor

zachs18 commented Apr 6, 2025

I think there's a more general issue here, also: having any unsafe fn in a safe trait makes it unclear who needs to uphold what preconditions, and if impls are allowed to restrict the documented preconditions of unsafe fns in the trait (especially when calling that unsafe fn on dyn Trait)

Generally, unsafe code cannot rely on the implementation of safe traits to be correct for soundness, but if that is true even when a safe trait Trait contains an unsafe fn bar, then it can never be sound to call Trait::bar on an arbitrary1 dyn Trait (or arbitrary ConcreteType: Trait), regardless of any target_features, because there's no way to know the actual safety preconditions of <ConcreteType as Trait>::bar since they could be "incorrect" with respect to the trait's documentation.

In @hanna-kruppe's prime example:

trait Foo {
    /// # Safety
    /// `n` must be a prime number.
    unsafe fn foo(n: u32);
}
impl Foo for () {
    /// # Safety
    /// `n` must be odd. Note that `Foo` requires prime numbers, but
    /// composite odd numbers are also fine for this impl.
    unsafe fn foo(n: u32) {
        // ...
    }
}

If it is true that implementing a safe trait incorrectly is not unsound, then impl Foo for () is not unsound, but any caller of <() as Foo>::foo needs to uphold <() as Foo>::foo's documented safety preconditions, not Foo::foo's (and thus cannot pass 2). Though of course restricting the precondition of an unsafe fn in a safe trait is still a footgun, it might not be unsound.


For a very explicit example, this code invokes UB (calls std::hint::unreachable_unchecked), so it must be unsound, but where is the unsoundness?

// crate a
pub trait Foo {
  // SAFETY: this is always safe to call // *1
  unsafe fn bar();
}

// crate b depends on crate a
pub struct LocalType;
impl crate_a::Foo for LocalType {
  // SAFETY: this is never safe to call
  unsafe fn bar() {
    // SAFETY: <() as Foo>::bar is never safe to call, so this is unreachable
    unsafe { std::hint::unreachable_unchecked() } // *2
  }
}

// crate c depends on crate a
pub fn quux<T: crate_a::Foo>() {
  // SAFETY: as per Foo::bar's docs, Foo::bar is always safe to call
  unsafe { <T as Foo>::bar() } // *3
}

// bin crate depends on all others
fn main() {
  crate_c::quux::<crate_b::LocalType>(); // *4
}

It cannot be *4 because that is safe code.

If Foo were an unsafe trait, then it would definitely be *2 (since restricting the preconditions on bar would be an incorrect implementation of Foo, which is unsound for unsafe traits), but since Foo is not an unsafe Trait, it could be argued that implementing it incorrectly shouldn't be unsound, so the error is at *3.

It could also be argued that unsafe fn in safe trait impls can have arbitrary preconditions unrelated to the trait's documentation (since implementing a safe trait "wrong" cannot be unsound), so the unsoundness is at *1 since it lies about the precondition of <Arbitrary as Foo>::bar when it can't actually know the precondition.


Basically, *2 in my example corresponds to the #[target_feature(enable = "avx,aes")] in the OP example, *3 corresponds to unsafe { value.demo() }, and *4 corresponds to whatever calls accepts_dyn.

In the OP example, the #[target_feature(enable = "avx,aes")] adds a precondition to <S as Example>::demo, that (presumably2) is not allowed by the documentation of Example::demo.

If Example in the OP was an unsafe trait (and it documented fn demo's preconditions), then the #[target_feature] would be the source of the unsoundness, but Example isn't an unsafe trait, so it's a bit unclear.


I think this discussion is perhaps evidence that allowing unsafe fn in a safe trait is unclear and perhaps could be phased out over an edition (only defining safe traits with unsafe fn, implementing them would need to stay for backwards-compatbility to implement traits from older crates in newer editions).

Edit: There are some unsealed safe traits in the stdlib with unsafe fn: std::os::fd::FromRawFd::from_raw_fd and FromRawHandle/FromRawSocket on Windows. I'm not sure if the intent is that implementors can or cannot impose additional restrictions (e.g. can <MyOsSpecialFileType as FromRawFd>::from_raw_fd require that the passed fd is actually that special file type for soundness?). The existing impls in the stdlib definitely don't impose such restrictions (since they are mirrored with safe-to-call From<OwnedFd> impls).

Footnotes

  1. Ignoring things like sealed traits for the moment; assume Trait is pub and possible to implement outside its crate.

  2. Technically, since Example::demo has no safety documentation, it's impossible to soundly call it at all, but I'm assuming it was just omitted for brevity.

@hanna-kruppe
Copy link
Contributor

hanna-kruppe commented Apr 6, 2025

I agree that the vast majority of traits with unsafe methods probably should be unsafe, and the exceptions are very subtle. That said, "the blame can't lie in safe code" is a useful heuristic but not 100% right in general.

For example, BTreeMap's CursorMut has several unsafe methods that require the caller to uphold order and uniqueness of the keys in the map/set. This is not required for BTreeMap's own soundness (it already has to deal with broken Ord impls because that's a safe trait) but only so that BTreeMap can provide a conditional invariant to third-party unsafe code: if you put keys with a correct Ord into a map, you can rely on the keys being unique and sorted. This is a safety-relevant guarantee that BTreeMap chooses to provide, regardless of its own internal use of unsafe. If BTreeMap was implemented in 100% safe code and documented this guarantee, but forgot to mark the key-modifying methods as unsafe, then any resulting UB from unsafe code relying on that guarantee is arguably the fault of BTreeMap.

Of course, unsafe code authors also need to be careful and selective about when and how they trust safe code to be correct. It's definitely wrong to rely on an "open world" set of safe code, such as all impls of a safe trait or any value of a function pointer type, to be correct. That would make compositional soundness proofs impossible, and those are the big advantage of Rust over "just write C and prove that it's memory safe 🙃". But it also doesn't scale to say that unsafe code authors can only trust code they wrote themselves in the same crate. For starters, you often have to trust standard library APIs -- and the standard library shouldn't be unique in this respect. Rust doesn't currently have any way to say "I'm typing unsafe here to promise that I'm actually implementing my documented invariants correctly in safe code" so we're left with situations where the correctness of a 100% safe crate can matter for soundness of unsafe code in other crates.

@apiraino
Copy link
Contributor

apiraino commented Apr 7, 2025

Nominating for T-lang discussion (hope it's the correct procedure to get team's attention!)

@rustbot label +I-lang-nominated

@rustbot rustbot added the I-lang-nominated Nominated for discussion during a lang team meeting. label Apr 7, 2025
@scottmcm
Copy link
Member

scottmcm commented Apr 9, 2025

Generally, unsafe code cannot rely on the implementation of safe traits to be correct for soundness, but if that is true even when a safe trait Trait contains an unsafe fn bar

But if an implementation of a safe trait uses an unsafe block (aka a hold_my_beer block) then it's that implementation that's responsible for the correctness of that unsafe block under the trait's preconditions.

impl Foo for Bar {
    unsafe fn foo() { unreachable!() }
}

has to be sound, since it's not discharging any obligations.

So in your example, I'd say it's necessarily *2 that's at fault.

@traviscross traviscross removed the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label Apr 9, 2025
@RalfJung
Copy link
Member

RalfJung commented May 23, 2025

By the way, note that your example doesn't actually need target_feature_11 to trigger "surprise" UB, I believe:

Ah, good point... the requirement is inevitably on the caller for this.

I #139368 (comment) why I don't think it's accurate to treat this as (only) discharging some safety requirements. It definitely introduces safety conditions for the caller of the #[target_feature] fn to prove, even if there's no further calls to other #[target_feature] fns. If it didn't, #139368 (comment) wouldn't have UB after all!

I think we're saying the same thing, I just didn't word my statement very carefully -- this unsafe fn does more than just acknowledge that we have some safety preconditions (defined by the trait), it adds new ones someone has to discharge. But an unsafe fn in a trait impl cannot just add new safety preconditions!

Another way to say this is that: an unsafe fn in a trait impl does not get to choose its own safety contract. Adding #[target_feature] to it thus comes with the obligation of proving that the safety contract defined for this function by the trait ensures that these target features are in fact available.

(And to be clear, I never opposed new features in rustc and the language to make this subtlety easier to wrangle. I just advocate for those new features to be framed around what I believe is the most accurate understanding of what is and isn't the problem.)

What I cannot extract from your message is whether you think making #[target_feature] in trait impls an unsafe attribute is a good way to wrangle this issue or not. What are your thoughts on that proposal?

@hanna-kruppe
Copy link
Contributor

hanna-kruppe commented May 23, 2025

What I cannot extract from your message is whether you think making #[target_feature] in trait impls an unsafe attribute is a good way to wrangle this issue or not. What are your thoughts on that proposal?

I think forcing the implementer to write an extra unsafe tokens vs. what they need without #[target_feature] on the method could be an effective way of flagging that an additional safety condition is being added. However, I fear that doing this with the exact same attribute syntax used for no_mangle and friends will end up with a somewhat novel and somewhat worse variant of the "unsafe is two separate keywords in a trench coat" problem that keeps confusing non-expert Rust users.

With traits and functions and impls and so on, one can learn to recognize by syntactic context which meaning of unsafe any particular token refers to. With a single syntax for unsafe attributes, this would become more messy. I understand your point that the obligation you have to prove when applying the attribute in this position is that you're not extending the safety requirements beyond what the trait laid down. This is technically a fine way to make it consistent with other unsafe attributes (and also to justify why it's only unsafe in trait impls), but it's also incredibly subtle. And it's still a bit inconsistent with other unsafe attributes because those can, in principle, introduce UB in a program even if the annotated item is never used.

Also, I still think that the "safe trait impl with unsafe fn wrongly chooses safety contract not implied by what the trait said" problem is not exclusive to #[target_feature]. While target features give an easy way to get language UB from such mistakes, you can cause (self-proclaimed) library UB as well. And if an unsafe block somewhere else in the program then escalates that library UB into language UB, I'd still consider the safe trait impl at fault (at least partially). If the root of the problem were safe trait impls choosing their own safety contract, then we should instead consider all such impls unsafe in the sense of asking the implementer to acknowledge they could be screwing up their program by writing something incorrect in their /// # Safety comments in the unsafe methods (cf. unsafe fields). That would mean essentially deprecating safe traits with unsafe methods -- probably not feasible now due to churn and losing a bit of expressiveness, but might have been a good idea pre-1.0.

I think we can side-step those complications by instead focusing on the "introducing safety requirements" aspect of #[target_feature]. We could choose a different syntax (still involving unsafe, but not in the exact same position) for this attribute in trait impls. The same syntax could also be used for unsafe target feature functions in other positions where the reminder about the extra implied precondition can be useful (e.g., trait method declarations without default body, if we start allowing #[target_feature] on them). Target feature functions that can be apparently-safe today should remain so, since those shift the unsafe to the parts of the code that can and must care about it (e.g. when you get an unsafe fn from an apparently-safe function item, you can look up the function item to see the exact target features it requires).

@RalfJung
Copy link
Member

Also, I #139368 (comment) that the "safe trait impl with unsafe fn wrongly chooses safety contract not implied by what the trait said" problem is not exclusive to #[target_feature].

I would say that example is a case of incorrect reasoning: you don't get to write your own safety comment on an unsafe fn in a trait impl -- or at best, you can only require a precondition that's weaker than what the trait says.

@obi1kenobi
Copy link
Member Author

obi1kenobi commented May 24, 2025 via email

@hanna-kruppe
Copy link
Contributor

hanna-kruppe commented May 24, 2025

Weakening the precondition is perfectly fine and useful (up to and including dropping it entirely). The problem in that example is that the implementer intended to weaken it but got the implication wrong. This feels similar to accidentally strengthening the precondition of an unsafe method by adding #[target_feature] in the impl in a way that's not a refinement of the traits contract -- which is currently very easy because nothing highlights that you even have to check this. So it feels somewhat inconsistent to only have compiler-enforced "discharging the obligation of getting the implication right" for one of those cases and not the other. (I acknowledge there are also several reasons to treat the two differently.)

I just realized that refined trait implementatios may offer another solution here. That RFC requires trait impls to write #[refine] to acknowledge when they don't match the trait's definition of an item exactly but refine it in some way, e.g., by dropping the unsafe from a method. We could similarly say, if you implement a trait method and add #[target_feature] to it, you're not inheriting the safety contract from the trait unchanged and the compiler should flag it so you can check if this was intentional. This is not necessarily an extra unsafe thing, though it might be. If it doesn't involve the unsafe token somehow, it should probably at least involve target_feature to distinguish it from other refinements that are usually less critical for soundness but may occur at the same time (just like how a method may be unsafe for reasons unrelated to target features).

@RalfJung
Copy link
Member

Weakening the precondition is perfectly fine and useful (up to and including dropping it entirely). The problem in that example is that the implementer intended to weaken it but got the implication wrong. This feels similar to accidentally strengthening the precondition of an unsafe method by adding #[target_feature] in the impl in a way that's not a refinement of the traits contract -- which is currently very easy because nothing highlights that you even have to check this.

Yes, it is similar. What's highlighting that you have to check this is that you wrote a safety comment on an unsafe fn in a trait impl in the first place -- that's just not a legal thing to do!

I agree that that's subtle, but it is the only way I can see to make this consistent given how the language works today.

This feels somewhat different from #[target_feature] because there it's the compiler adding the precondition, not you. But yeah it's a rather fuzzy line.

We could potentially require unsafe impl even when implementing safe traits that have unsafe fn, to acknowledge "I did not strengthen any of the unsafe preconditions", but then that effectively makes the trait itself unsafe so it's rather odd.

@RalfJung RalfJung added the I-lang-nominated Nominated for discussion during a lang team meeting. label May 24, 2025
@RalfJung
Copy link
Member

I nominated this for t-lang again, given that the previous suggestion will clearly not work -- there are basic ecosystem crates that have #[target_feature] in trait impls, and they way they are doing it looks entirely coherent. See the discussion starting here.

@hanna-kruppe
Copy link
Contributor

hanna-kruppe commented May 24, 2025

Yes, it is similar. What's highlighting that you have to check this is that you wrote a safety comment on an unsafe fn in a trait impl in the first place -- that's just not a legal thing to do!

Aside: the way rustdoc and other tooling works, if you want to add or customize any documentation to your impl of a trait's unsafe fn, you're between a rock and a hard place. You could copy-paste the safety section from the trait's doc comment into your impl's doc comment, but then the comments can drift over time and it's hard to distinguish from "you wrote your own safety comment". Alternatively, you can not copy it, but then anyone looking at that method's documentation (e.g., on the implementing type's rustdoc page or by hovering over a method call in an IDE) won't see the safety requirements at all. You can write "Safety: see the trait" but that's still indistinguishable from "wrote your own safety comment" for an automated tool, and still not great for people reading the docs.

@traviscross
Copy link
Contributor

traviscross commented May 24, 2025

To be sure we're all on the same page, I'm curious whether everyone agrees with the following as being representative of the use case here, and agrees with the safety analysis.

Playground link

(Note that the doc comments are the safety contract and the regular comments are the crate-internal safety analysis.)

(Edit: Updated the playground with some tweaks as per below.)

@RalfJung
Copy link
Member

@traviscross yes, that looks good, except that Avx2 needs a private field or else it cannot carry an invariant. I'd also add SAFETY documentation to the Avx2 type explicitly stating its invariant -- the moment a type carries an invariant, that invariant should be written down somewhere, and the type itself is the most natural place.

@traviscross
Copy link
Contributor

Thanks, yes; edited.

@traviscross
Copy link
Contributor

Based on that, here's what I currently think we should do, extending what we had said earlier.

One, if the impl item enables more target features than the trait def item, then the target_feature attribute must be marked unsafe(..) in the impl. We should 1) make the change to allow unsafe(target_feature(enable = "..")), 2) add a FCW in all editions with a machine-applicable fix, 3) make this case a hard error over an edition (and maybe later in all editions).

Two, allow applying unsafe(target_feature(enable = "..")) in impls to safe functions. The obligation is on the implementor, not the caller.

@RalfJung
Copy link
Member

One, if the impl item enables more target features than the trait def item,

So, we should also allow target features on trait definitions then? Currently, we only allow that for defaulted methods.

@veluca93

This comment has been minimized.

@veluca93
Copy link
Contributor

One, if the impl item enables more target features than the trait def item,

So, we should also allow target features on trait definitions then? Currently, we only allow that for defaulted methods.

What would be the semantics of this? I see three options:

  1. features get enabled automatically on implementors. This could be surprising for implementors
  2. features don't get enabled automatically on implementors. This could be surprising for callers and in general IMO introduces a new meaning for #[target_features()] ("features this trait method could have")
  3. implementors need to specify the same features. This could be a new interesting way for things to be semver-breaking.

I am not sure I like any of the above options, but perhaps (2) is the best one.

@hanna-kruppe
Copy link
Contributor

To be sure we're all on the same page, I'm curious whether everyone agrees with the following as being representative of the use case here, and agrees with the safety analysis.

(Note that the doc comments are the safety contract and the regular comments are the crate-internal safety analysis.)

It seems representative of the use case and safety reasoning in aho-corasick (@BurntSushi as the author would know best). I agree with the actual safety comments (internal and doc comments). I do not agree with the //~^ note that the #[target_feature] in the Avx2 impl necessarily needs to be an unsafe attribute.

Another use case is when the trait defines the safety contract of some of its methods as "caller must ensure target feature X is enabled" and implementations (may) apply #[target_feature(enable=X)] based on that. There's already been discussion of what language features would be needed to accept that automatically, but unless and until those are added, the way you can do it in Rust today leads to rather different safety reasoning than in the linked playground.

@traviscross
Copy link
Contributor

So, we should also allow target features on trait definitions then?

Yes.

@traviscross
Copy link
Contributor

traviscross commented May 24, 2025

What would be the semantics of this?

See our earlier team reply here:

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.

@traviscross traviscross added the P-lang-drag-1 Lang team prioritization drag level 1. https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang label May 24, 2025
@veluca93
Copy link
Contributor

What would be the semantics of this?

See our earlier team reply here:

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:

  1. Add support for unsafe(target_feature(enable = X)), which is identical to target_feature(enable = X) except it disables checks.
  2. Allow setting the target_feature attribute on trait method definition
  3. 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 :-)

@traviscross
Copy link
Contributor

Sounds reasonable to me. Maybe we'd also want a step for allowing this on safe trait functions. Note also:

  1. 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.

@veluca93
Copy link
Contributor

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:

  1. 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)

@traviscross
Copy link
Contributor

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.

@traviscross
Copy link
Contributor

@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

@rustbot rustbot removed I-lang-nominated Nominated for discussion during a lang team meeting. P-lang-drag-1 Lang team prioritization drag level 1. https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang labels May 25, 2025
@RalfJung
Copy link
Member

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...

@hanna-kruppe
Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-bug Category: This is a bug. I-lang-radar Items that are on lang's radar and will need eventual work or consideration. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-lang Relevant to the language team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests