-
Notifications
You must be signed in to change notification settings - Fork 13.4k
#[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
Comments
I agree this can be a footgun: the safety precondition implied by the Unfortunately, it seems difficult to automatically distinguish mistakes from sound usage:
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 In analogy to unsafe fields, I could see an argument for making |
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. |
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. |
Maybe the attribute should be unsafe to apply to impl methods. rust-lang/rfcs#3715 |
RFC 3715 is interesting because:
Target feature attributes are the latter kind of unsafe so perhaps it should be spelled 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 |
I did say it is actually the former kind of unsafe. The |
I agree with this. And so long as it only discharges those requirements, I think My remaining concern is that 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 :) |
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 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
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. |
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. |
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 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
In such a case, I'd still assign the blame for the resulting UB to the fact that the author of In that sense, |
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:
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 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. |
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 I think it would be completely defensible to introduce an But requiring the |
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." |
I think there's a more general issue here, also: having any Generally, unsafe code cannot rely on the implementation of safe 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 For a very explicit example, this code invokes UB (calls // 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 If It could also be argued that Basically, In the OP example, the If I think this discussion is perhaps evidence that allowing Edit: There are some unsealed safe Footnotes |
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 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 |
Nominating for T-lang discussion (hope it's the correct procedure to get team's attention!) @rustbot label +I-lang-nominated |
But if an implementation of a safe trait uses an 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 |
Ah, good point... the requirement is inevitably on the caller for this.
I think we're saying the same thing, I just didn't word my statement very carefully -- this Another way to say this is that: an
What I cannot extract from your message is whether you think making |
I think forcing the implementer to write an extra With traits and functions and impls and so on, one can learn to recognize by syntactic context which meaning of Also, I still think that the "safe trait impl with I think we can side-step those complications by instead focusing on the "introducing safety requirements" aspect of |
I would say that example is a case of incorrect reasoning: you don't get to write your own safety comment on an |
I agree with this — it's the exact line of reasoning I followed to find
this issue.
…On Sat, May 24, 2025, 10:03 AM Ralf Jung ***@***.***> wrote:
*RalfJung* left a comment (rust-lang/rust#139368)
<#139368 (comment)>
Also, I #139368 (comment)
<#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.
—
Reply to this email directly, view it on GitHub
<#139368 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAR5MSRH2TFP32Z3VMDLAXL3AARWVAVCNFSM6AAAAAB2OXF6CSVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDSMBWGYYTAMBQGA>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
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 I just realized that refined trait implementatios may offer another solution here. That RFC requires trait impls to write |
Yes, it is similar. What's highlighting that you have to check this is that you wrote a safety comment on an 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 We could potentially require |
I nominated this for t-lang again, given that the previous suggestion will clearly not work -- there are basic ecosystem crates that have |
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. |
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.) (Edit: Updated the playground with some tweaks as per below.) |
@traviscross yes, that looks good, except that |
Thanks, yes; edited. |
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 Two, allow applying |
So, we should also allow target features on trait definitions then? Currently, we only allow that for defaulted methods. |
This comment has been minimized.
This comment has been minimized.
What would be the semantics of this? I see three options:
I am not sure I like any of the above options, but perhaps (2) is the best one. |
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 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 |
Yes. |
See our earlier team reply here: The trait defines the maximum that can be expected of callers. If it sets out a With my proposed extension, we'd say that if |
Implementation-wise, I would imagine the following PRs:
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:
Another difference between these is that |
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) |
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. |
@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 |
This sounds more and more like there should be an RFC, as there's some amount of design space here... |
In particular, any usage of |
I tried this code: (playground)
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
:From the playground.
The text was updated successfully, but these errors were encountered: