-
-
Notifications
You must be signed in to change notification settings - Fork 14.3k
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
base: main
Are you sure you want to change the base?
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)] // even though this function is unsound
pub fn with_copy(op: impl FnOnce(unsafe fn(*const u8, *mut u8, usize))) {
op(std::ptr::copy_nonoverlapping); // using std::ptr::copy is sound
}
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
} |
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