Unsafe-Related Lint Changes
The Rust 2024 edition significantly sharpens how unsafe contracts are declared and audited across four areas: requiring explicit unsafe { ... } blocks inside unsafe fn bodies, requiring unsafe extern on foreign blocks, requiring #[unsafe(...)] on symbol-level attributes, and marking std::env::set_var / remove_var as unsafe fn.
Rust 2021 (Implicit / Unmarked Unsafe Obligations):
- `unsafe fn foo(p: *const i32) -> i32 { *p }` (Entire body was implicitly an unsafe block!)
- `extern "C" { fn c_func(); }` (Unchecked ABI promise without `unsafe` keyword)
- `#[no_mangle] pub extern "C" fn bar() {}` (Global linker symbol without `unsafe` attribute)
Rust 2024 (Explicit Unsafe Obligations Everywhere):
- `unsafe fn foo(p: *const i32) -> i32 { unsafe { *p } }` (`unsafe_op_in_unsafe_fn` warn!)
- `unsafe extern "C" { safe fn abs(x: i32) -> i32; fn c_func(); }`
- `#[unsafe(no_mangle)] pub extern "C" fn bar() {}`
1. unsafe_op_in_unsafe_fn Enabled by Default
In earlier editions, marking a function unsafe fn implicitly treated the entire function body as one giant unsafe block. If a 60-line unsafe fn performed 58 lines of safe validation and 2 lines of raw pointer dereferencing, a reader could not tell which lines actually performed unsafe operations—and an accidental call to another unsafe function on line 30 went completely unnoticed by the compiler!
In Rust 2024, the unsafe_op_in_unsafe_fn lint is enabled (warn) by default:
- Declaring
unsafe fnstates that calling the function has safety preconditions. - Inside the body of an
unsafe fn, every individual unsafe operation must still be wrapped in its own explicitunsafe { ... }block!
/// Copies `count` elements from `src` to `dst` if `count > 0`.
///
/// # Safety
///
/// `src` and `dst` must be valid, aligned, non-overlapping pointers for `count` elements.
pub unsafe fn copy_elements<T>(src: *const T, dst: *mut T, count: usize) {
if count == 0 {
return;
}
// In Rust 2024, even inside an `unsafe fn`, the exact line performing an unsafe
// operation is wrapped in an explicit `unsafe { ... }` block:
// SAFETY: Caller guaranteed `src` and `dst` are valid and non-overlapping for `count` items.
unsafe {
std::ptr::copy_nonoverlapping(src, dst, count);
}
}
2. Unsafe Attributes: #[unsafe(no_mangle)], #[unsafe(export_name)], #[unsafe(link_section)]
Attributes that alter linker symbol names (no_mangle, export_name) or place data into custom binary ELF/Mach-O/PE sections (link_section) can cause Undefined Behavior if a symbol collides with a C library function (such as naming a Rust function malloc or read). Rust 2024 requires wrapping these attributes in #[unsafe(...)]:
| Rust 2021 Syntax | Rust 2024 Syntax | Why It Is unsafe |
|---|---|---|
#[no_mangle] |
#[unsafe(no_mangle)] |
Exports an unmangled global linker symbol that must not collide with incompatible foreign signatures. |
#[export_name = "foo"] |
#[unsafe(export_name = "foo")] |
Overrides the linker symbol name to "foo". |
#[link_section = ".text"] |
#[unsafe(link_section = ".text")] |
Places code or data into a specific linker section that must satisfy platform ABI expectations. |
#[unsafe(export_name = "custom_ffi_version")]
pub extern "C" fn get_version() -> u32 {
1
}
fn main() {
let src = [10, 20, 30];
let mut dst = [0, 0, 0];
// SAFETY: `src` and `dst` are distinct 3-element arrays on the stack.
unsafe {
copy_elements(src.as_ptr(), dst.as_mut_ptr(), 3);
}
assert_eq!(dst, [10, 20, 30]);
println!("Copied elements: {dst:?}, version={}", get_version());
}
Copied elements: [10, 20, 30], version=1