Skip to content

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

Open

Description

@obi1kenobi

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

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Apr 4, 2025
  2. added
    I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness
    on Apr 4, 2025
  3. added
    I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}
    on Apr 4, 2025
  4. added
    I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness
    I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}
    and removed
    I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness
    I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}
    on Apr 4, 2025
  5. hanna-kruppe commented on Apr 4, 2025

    @hanna-kruppe
    Contributor

    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.

  6. obi1kenobi commented on Apr 4, 2025

    @obi1kenobi
    MemberAuthor

    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.

  7. added
    T-langRelevant to the language team
    T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
    on Apr 5, 2025
  8. hanna-kruppe commented on Apr 5, 2025

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

  9. oli-obk commented on Apr 5, 2025

    @oli-obk
    Contributor

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

  10. 60 remaining items

  11. veluca93 commented on May 24, 2025

    @veluca93
    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 :-)

  12. traviscross commented on May 24, 2025

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

  13. veluca93 commented on May 24, 2025

    @veluca93
    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)

  14. traviscross commented on May 24, 2025

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

  15. traviscross commented on May 25, 2025

    @traviscross
    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

  16. removed
    I-lang-nominatedNominated for discussion during a lang team meeting.
    P-lang-drag-1Lang team prioritization drag level 1. https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang
    on May 25, 2025
  17. RalfJung commented on May 25, 2025

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

  18. hanna-kruppe commented on May 25, 2025

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

  19. traviscross commented on May 25, 2025

    @traviscross
    Contributor

    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.

  20. veluca93 commented on May 25, 2025

    @veluca93
    Contributor

    I drafted up a RFC following this discussion: rust-lang/rfcs#3819

  21. added a commit that references this issue on Jun 6, 2025
  22. added a commit that references this issue on Jun 7, 2025
  23. added a commit that references this issue on Jun 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-bugCategory: This is a bug.I-lang-radarItems that are on lang's radar and will need eventual work or consideration.T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.T-langRelevant to the language team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions