Skip to content

New lint: Limit unsafe blocks to a single unsafe operation #10064

Closed
@djkoloski

Description

@djkoloski

What it does

Checks for unsafe blocks that contain more than one unsafe operation. Unsafe operations include:

  • Dereferencing a raw pointer (*const T or *mut T)
  • Calling an unsafe function or method
  • Accessing or modifying a mutable static variable
  • Accessing fields of unions

Lint Name

one_unsafe_op_per_unsafe_block

Category

restriction

Advantage

Combined with undocumented_unsafe_blocks, this lint ensures that each unsafe operation must be independently justified. Combined with unused_unsafe, this lint also ensures that we know when we eliminate unsafe operations through refactoring.

See the "Example" section for comprehensive examples.

Drawbacks

It may be awkward or noisy to break up a series of related unsafe operations into individual blocks.

Example

With undocumented_unsafe_blocks

/// Reads a `char` from the given pointer.
///
/// # Safety
///
/// `ptr` must point to four consecutive, initialized bytes which
/// form a valid `char` when interpreted in the native byte order.
unsafe fn read_char(ptr: *const u8) -> char {
    // SAFETY: The caller has guaranteed that the value pointed
    // to by `bytes` is a valid `char`.
    unsafe { char::from_u32_unchecked(*ptr.cast::<u32>()) }
}

Although the unsafe block is properly justified for char::from_u32_unchecked, there are actually two unsafe operations in it. The pointer dereference needs to be justified as well, but it's easy to accidentally include it without updating the justification. By limiting unsafe blocks to a single unsafe operation, this would be rewritten as:

/// Reads a `char` from the given pointer.
///
/// # Safety
///
/// - `ptr` must be 4-byte aligned, point to four consecutive
///   initialized bytes, and be valid for reads of 4 bytes.
/// - The bytes pointed to by `ptr` must represent a valid
///   `char` when interpreted in the native byte order.
unsafe fn read_char(ptr: *const u8) -> char {
    // SAFETY: `ptr` is 4-byte aligned, points to four consecutive
    // initialized bytes, and is valid for reads of 4 bytes.
    let int_value = unsafe { *ptr.cast::<u32>() };
    // SAFETY: The caller has guaranteed that the four bytes
    // pointed to by `bytes` represent a valid `char`.
    unsafe { char::from_u32_unchecked(int_value) }
}

Breaking up our unsafe block led us to justify dereferencing ptr, which in turn led us to update the function's safety requirements to accurately reflect the invariants used by the implementation.

With unused_unsafe

Refactoring from the same starting function as the example in undocumented_unsafe_blocks:

/// Reads a `char` from the given reference.
///
/// # Safety
///
/// `int` must represent a valid `char` when interpreted in
/// the native byte order.
unsafe fn read_char(int: &u32) -> char {
    // SAFETY: The caller has guaranteed that the four bytes
    // pointed to by `bytes` represent a valid `char`.
    unsafe { char::from_u32_unchecked(*int) }
}

By changing our function to accept &u32, we were able to remove the only unsafe operation from our first unsafe block. In turn, we were able to eliminate the unsafe block, prompting us to update our safety requirements for our function. In this way, refactoring a function to remove unsafe helps ensure that the safety documentation also gets updated to reflect the new requirements.

Metadata

Metadata

Assignees

Labels

A-lintArea: New lints

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions