-
-
Notifications
You must be signed in to change notification settings - Fork 14.4k
lint(unsafe_code): exclude unsafe declarations from lint coverage #148651
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
Conversation
|
rustbot has assigned @petrochenkov. Use |
This comment has been minimized.
This comment has been minimized.
At least for extern blocks, that is not correct. The mere presence of an incorrect extern block can cause UB. See #46188. |
This comment has been minimized.
This comment has been minimized.
Indeed, my bad, I've rolled back this part. |
This comment has been minimized.
This comment has been minimized.
Unsafe declarations of traits, functions, methods, or extern blocks cannot cause undefined behavior by themselves — only unsafe blocks or implementations of unsafe traits can. The `unsafe_code` lint previously applied to these declarations, preventing, for example, declarations of `extern "C"` blocks. See rust-lang#108926. This change removes all such unsafe declarations from `unsafe_code` coverage. However, to maintain soundness, unsafe functions *with bodies* are only allowed when `unsafe_op_in_unsafe_fn` is set to `forbid`. This ensures unsafe operations inside such functions require an `unsafe` block. Declarations of unsafe functions *without bodies* (e.g., in traits) are always allowed. Closes rust-lang#108926
e15274b to
c513db8
Compare
|
This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed. Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers. |
|
|
I'll send this to lang team for a vibe check. |
|
We discussed this in the @rust-lang/lang meeting today. We did not reach a consensus but there were several points made in various directions. Everyone agreed that there is didactic value to being precise about when/where you are making an unsafe assertion (i.e., writing trust code that may cause UB if wrong) versus creating an unsafe API (and leaving the assertions to consuming crates). Many felt that, nonetheless, in practice when they used this lint they meant that they would prefer to have neither unsafe assertions nor unsafe APIs. The counterexample was something like No option had meeting consensus. The options we considered are:
EDIT: added the 'do nothing' option |
|
There's also the "do-nothing" option, where we'd document that the lint means "no |
|
The language doesn't currently permit this, afaik, but I think another use case would be: trait Foo {
// Allowed to assume P is true
unsafe fn bar(&self);
}
impl Foo for () {
fn bar(&self) {
// but I don't need or want to actually make use of this unsafe predicate P in *my impls*
}
}...it'd be nice to have a real example though. Most of the cases I've thought of still wind up with some unsafe code somewhere. |
|
Elaborating on the use case for That said, 👍 for making it a lint group with separate lints for the two cases, so that you can forbid one but not the other. |
Actually, implementing an unsafe trait method is neither an unsafe assertion, nor creating an unsafe API. It's a third category on its own. This is by the way the use case mentioned in the linked issue #108926. pub trait WriteSlot {
fn slot_size(&self) -> usize;
/// # Safety
/// `slot` slice's length must be equal to the result of [`Self::slot_size`]
unsafe fn write_slot(self, slot: &mut [u8]);
}I would like to enable the user to use unsafe optimizations (skip bounds check, etc.), but he might prefer using safe code instead, and that's perfectly fine. But EDIT: Thinking a bit more about it, implementing an unsafe trait method is quite similar to implementing a trait method whose one argument have a |
|
@wyfo is right, that is the use case. Let's say we have a crate that is // # Upstream
pub trait Tr {
/// SAFETY: Caller must ensure that `x` is even and non-zero.
unsafe fn op(x: u8);
}Then in our crate: // # Our crate
#![forbid(unsafe_code)]
pub struct OurThing;
impl Tr for OurThing {
/// SAFETY: This is safe to call with any value.
unsafe fn op(x: u8) {
println!("`x` is {x} and we're OK with zero and odd numbers");
}
}If we had #100706 ("refined trait impls", RFC 3245), then we'd probably drop the The argument is simply that 1) there's no possibility of unsafety, 2) this makes sense for all parties to do. It makes sense for the upstream to do this so as to leave room for downstreams to be able to unsafely use this invariant in impls. It makes sense for us, as one downstream, to implement the trait (for whatever reason) but in an entirely safe way (not making use of the invariant), consistent with our (This is similar, of course, to @nikomatsakis' example in #148651 (comment); I highlight only that the inter-crate relationship may be important to the motivation and that the argument might still work without RFC 3245, though it works better with it.) |
|
Oh, I was sure that RFC 3245 was already stable behavior!
Assuming that RFC was stable, then I'd categorize "implementing an unsafe method" as "creating an unsafe API" because you chose to implement it as In the meeting I was in favor of changing the lint but I'd find it unfortunate if our reason for changing it was that the lack of RFC 3245 forces our hand... |
|
We could construct the example such that the // # Upstream
pub trait Tr {
final fn f() {
// SAFETY: We ensure that `x` is even and non-zero.
unsafe { Self::op(2) };
}
/// SAFETY: Caller must ensure that `x` is even and non-zero.
unsafe fn op(x: u8);
}
// # Our crate
#![forbid(unsafe_code)]
struct OurThing; // NOTE: Not `pub`.
impl Tr for OurThing {
/// SAFETY: This is safe to call with any value.
unsafe fn op(x: u8) {
println!("`x` is {x} and we're OK with zero and odd numbers");
}
}
fn main() {
// This is safe.
OurThing::f();
} |
|
Note that The problem occurs when the /// Abstracts over a copy implementation.
///
/// # Safety
///
/// The operation `op` must call its argument as if it were `ptr::copy()`. In particular, the
/// pointers must be valid (but may overlap).
#[forbid(unsafe_code)] // this function's incorrectness has soundness consequences
pub fn with_copy(op: impl FnOnce(unsafe fn(*const u8, *mut u8, usize))) {
op(std::ptr::copy_nonoverlapping); // incorrect, using std::ptr::copy would be correct
}
fn main() {
let mut data = *b"hello world";
// SAFETY: The pointers are valid (and overlap, but that's authorized).
with_copy(|copy| unsafe { copy(data.as_ptr(), data[4..].as_mut_ptr(), 7) });
println!("{}", std::str::from_utf8(&data).unwrap());
}A similar example can be written using the return type rather than an argument of an argument: #[forbid(unsafe_code)]
fn get_copy() -> unsafe fn(*const u8, *mut u8, usize) {
std::ptr::copy_nonoverlapping
}Or more simply when defining a type alias which contains the Actually thinking about this, the same should hold for any type that needs an unsafe block to use, like raw pointers. So the treatment of the unsafe function pointer types should be similar to the one of raw pointer types, which is currently coherent. |
You bring an interesting point here, and I think you're right. Once RFC 3245/#100706 lands, user will be able to keep the About splitting the lints, I don't see any use case where it would be useful. If a user allows unsafe API declaration, that's necessarily to use it with unsafe assertions in the same crate. In the same way, if a user allows unsafe assertions, which is the real unsafe thing, then I don't see why he would prevent himself to declare unsafe API. Appart of the didactic value, I think it should require a concrete use case to start working on it. So, now I'm convinced that implementing an unsafe method belongs indeed to "creating an unsafe API", as long as RFC 3245 is concerned, I think I went in the wrong direction and will close the PR if nobody disagree with it. |
|
From #148651 (comment):
With that logic, shouldn't I think the "do nothing" option is insufficient. We should at least fix the lint documentation (if not its behavior, as this PR is doing). Right now, the lint documentation says:
with the following explanation:
The way I (and apparently other users) understand those sentences, is that the lint is about code that may cause undefined behavior (so not where the undefined behavior would happen, but where the blame would be assigned). This notion of "unsafe code" (which is the lint name) is actually mentioned by the reference under
Sadly it doesn't really specify what such unsafe code really is. The fact that the word "unsafe" is rendered as Something else that should probably be mentioned in the documentation (which seems to be a general assumption) is that this lint is meant to be used at crate-level with the I can understand that changing the semantics of the lint is not worth the benefit of having less false positives or being more didactic/consistent/unsurprising, but in that case I would expect the documentation of the lint to more accurately describe its intent, behavior, and expected usage. This is not only useful for users, but also for maintainers as the language evolves and decision needs to be made on whether a new language construct should lint or not. |
Unsafe declarations of traits, functions, methods,
or extern blockscannot cause undefined behavior by themselves — only unsafe blocks or implementations of unsafe traits can.The
unsafe_codelint previously applied to these declarations, preventing, for example, declarations of unsafe function with safe body. See #108926.This change removes all such unsafe declarations from
unsafe_codecoverage.However, to maintain soundness, unsafe functions with bodies are only allowed when
unsafe_op_in_unsafe_fnis set toforbid. This ensures unsafe operations inside such functions require anunsafeblock. Declarations of unsafe functions without bodies (e.g., in traits) are always allowed.Closes #108926