Pre-RFC: Rust Safety Standard

Rust Safety Standard

This standard is a draft and welcomes feedback.

1. Background

Rust’s safety guarantees rely heavily on the correct use of unsafe code. However, maintaining safety becomes increasingly challenging as real-world crates grow in size, particularly when attempting to enforce a clear separation between safe and unsafe components. Rust currently provides only the following high-level soundness requirement:

We say that a library (or an individual function) is sound if it is impossible for safe code to cause Undefined Behavior using its public API.

This definition does not provide actionable guidance for determining which functions or struct methods should be declared safe or unsafe. The design space becomes even more complex when visibility boundaries are taken into account.

Furthermore, when a function is declared unsafe, developers must clearly document the conditions required for its correct use, and users must fully understand them. This raises another important question: what information is necessary to specify the safety requirements of an unsafe function?

This document addresses these challenges by presenting actionable guidance derived from extensive reviews of unsafe code in several Rust projects, particularly the Rust standard library and Rust-for-Linux. Our recommendations are grounded in the theoretical foundation presented in A Trace-based Approach for Code Safety Analysis.

2 Rationale

2.1 Definition of Unsafe

In general, unsafe indicates that there are additional requirements imposed on the user. These requirements should be clearly documented to avoid misuse.

  • Unsafe function: a function is unsafe if it requires the caller to satisfy certain requirements.
  • Unsafe trait: a trait is unsafe if it requires the implementer to satisfy certain requirements.

These two concepts are orthogonal.

2.2 Safety Documentation

  • Define unsafe code: Safety requirements should be provided alongside each unsafe definition as comments or documents. They should state sufficient conditions to prevent undefined behavior when using the unsafe code, and may also describe discouraged usage patterns that are known to lead to undefined behavior.
  • Use unsafe code: When using unsafe code, developers must provide comments justifying why the relevant safety requirements are upheld.

2.3 Design Choices

In general, there are two scenarios in which a developer declares a function unsafe.

  • Dependent unsafe (mandatory):

    • A free function invokes unsafe code from its dependencies, and the safety requirements of that unsafe code cannot be fully discharged by the function itself. As a result, the function must be declared unsafe to propagate the remaining safety requirements to its caller.
    • An associated function may violate a struct’s type invariant, which can in turn affect other methods of the struct that rely on unsafe code. Regardless of whether it contains unsafe code, such a function should be declared unsafe, with safety requirements specifying how the invariant must be upheld.
  • New unsafe (by design):

    • Besides dependent unsafe, developers may deliberately introduces additional safety contracts that must be upheld by the caller in order to use the function safely.
    • When there is confusion about whether a function should be declared safe or unsafe, remember that safety invariants always go before unsafe. Using unsafe means that a function may violate some safety invariants if it is misused.

In the following example, whether new_unchecked should be safe or unsafe depends on whether the type explicitly declares a safety invariant that x must be even. If such an invariant is part of the type’s contract, then new_unchecked must be declared unsafe, since incorrect usage can violate the invariant. If such invariant does not exist, then new_unchecked is safe, as misuse does not violate any safety invariant.

Therefore, both versions below are sound, but they correspond to different design choices regarding the type’s invariants.

/// # Safety:
/// ## Type invariants:
/// - `x` must be even.
pub struct EvenNumber {
  val: u32,
}

impl EvenNumber {
    /// # Safety
    /// - `x` must be even.
    pub unsafe fn new_unchecked(x: u32) -> Self {
        EvenNumber{x}
    }
}
pub struct EvenNumber {
  val: u32,
}

impl EvenNumber {
    pub fn new_unchecked(x: u32) -> Self {
        EvenNumber{x}
    }
}

For example, in the Rust standard library, RawWaker and RawWakerVTable are types without type invariants and currently contain no internal unsafe code. Therefore, their constructors new are both declared safe. These structs are then used by LocalWaker, which enforces the type invariants. As a result, its constructors new and from_raw are declared unsafe.

Note that the introduction of new unsafe code is discouraged unless necessary. Sometimes, introducing new unsafe functions can bring benefits and help avoid exposing additional unsafe functions. This is essential to prevent the proliferation of unsafe code and the degradation of overall safety in Rust projects.

2.4 Decoupling

Sometimes, it can be unclear how safety duties should be allocated among related program components. Decoupling these responsibilities is therefore essential. A common source of confusion involves free functions that return a struct instance. In such cases, we recommend treating the free function as indirectly constructing the instance via the struct’s own constructors, including the literal constructor. For example, in the following code, we can treat new_unchecked as a free function that returns a an EvenNumber instance, instead of a constructor; the real constructor is the literal constructor of the tuple struct EvenNumber().

pub struct EvenNumber(u32);

/// # Safety
/// - `x` must be even.
pub unsafe fn new_unchecked(x: u32) -> EvenNumber {
    EvenNumber(x)
}

A better design is to place the unsafe function inside the impl block of the struct:

/// # Safety:
/// ## Type invariants:
/// - `x` must be even.
pub struct EvenNumber(u32);

impl EvenNumber {
    /// # Safety
    /// - `x` must be even.
    pub unsafe fn new_unchecked(x: u32) -> Self {
        EvenNumber(x)
    }
}

Similarly, one may create objects of other types that enclose the struct, such as Box<EvenNumber>. These should also rely on the constructor of EvenNumber.

pub unsafe fn new_unchecked(x: u32) -> Box<EvenNumber> {
    Box::new(EvenNumber::new_unchecked(x))
}

2.5 Visibility and Soundness

In Rust, soundness is tied to Visibility, because it determines which and how APIs are accessible. By default, a module is the smallest visibility boundary: all functions, structs, and struct fields are private to the module unless explicitly made public. Developer can rely on the visibility restrictions to maintain sound abstractions, i.e., all uses of a module’s public safe items (or unsafe items, provided their safety requirements are met) from outside the module must not lead to undefined behavior. This is the default soundness criterion adopted by the Rust standard library.

2.5.1 Struct Literals and Field Projections

In the following example, Vec is a struct with defined invariants, and new is a safe constructor that ensures all safety invariants hold. Although it is possible to break these invariants within the module using struct literals or field projections (e.g., v.len = usize::MAX), this is still considered sound. Preventing these fields from being publicly accessible is key to ensuring soundness.

/// # Safety:
/// ## Type invariants:
/// - If `cap == 0`, `ptr` must be dangling and never dereferenced.
/// - If `cap > 0`, `ptr` points to a heap allocation for `cap` elements of `T`.
/// - The range `[0, len)` is initialized.
/// - `len <= cap`.
/// - This `Vec` uniquely owns the allocation.
mod vec {
    pub struct Vec<T> {
        ptr: NonNull<T>, // pointer to heap allocation
        len: usize,      // number of initialized elements
        cap: usize,      // total allocated capacity
        _marker: PhantomData<T>,
    }

    pub fn new() -> Self {
        Self {
            ptr: NonNull::dangling(),
            len: 0,
            cap: 0,
            _marker: PhantomData,
        }
    }
    ...
}

2.5.2 Static Variables

A function may rely on the states of private static variables to ensure soundness. For example, the function do_critical_task in the following code can be declared safe. It requires the static variable PTR to be either null or to point to CONFIG. This requirement is always satisfied because, within this module, PTR can only have these two states. Although future modifications to the module could introduce additional states or allow PTR to be modified in new ways, it is unnecessary to declare do_critical_task as unsafe. In that case, declaring the new function as unsafe should be preferred in order to avoid introducing a semantically breaking change to the function do_critical_task.

mod FooSys {
    use std::{ptr, sync::atomic::{AtomicPtr, Ordering}};

    /// Static atomic pointer pointing to system configuration
    static PTR: AtomicPtr<u32> = AtomicPtr::new(ptr::null_mut());

    /// Initialize the system and set PTR to point to a valid static resource
    pub fn initialize_system() {
        static CONFIG: u32 = 42;
        PTR.store(&CONFIG as *const u32 as *mut u32, Ordering::SeqCst);
    }

    pub fn do_critical_task() {
        let ptr = PTR.load(Ordering::SeqCst);
        if !ptr.is_null() {
            unsafe { read_config(ptr); }
        }
    }

    // Safety: `ptr` must point to a valid configuration.
    unsafe fn read_config(ptr: *mut u32) {
       ...
    }
}

Sometimes, in a large crate, a static variable may be pub across several modules but not exposed outside the crate. As long as it cannot enter invalid states and downstream crate users cannot introduce invalid states to it, using the static variable in this way is considered sound.

2.5.3 Sealed Traits

Making a trait sealed is necessary if implementing it outside the module could introduce undefined behavior. In the example below, the module FooSys defines a private trait Sealed and a public trait Foo that requires implementers to also implement Sealed. This design ensures that only types defined within the module, such as Bar, can implement Foo. This prevents external types that have a len field from misimplementing Foo.

mod FooSys {
    // Private module to seal the trait
    mod private {
        pub trait Sealed {}
    }

    /// Public trait `Foo`, only implementable within this module
    /// because it requires `Sealed`.
    pub trait Foo: private::Sealed {
        fn set_len(&mut self, len: usize);
        fn get_len(&self) -> usize;
    }

    pub struct Bar {
        len: usize, 
    }

    impl private::Sealed for Bar {}

    impl Foo for Bar {
        ...
    }
}

3 Rules for Free Functions

A free function is a function defined at the module level that can be called directly by its path rather than through an instance or type.

3.1 Safety Rules

The soundness of free functions is relatively straightforward to justify.

  • Function Safety Rule 1: If a free function contains unsafe code, it can be declared safe only if all conditions required for the safe use of that unsafe code are met.
  • Function Safety Rule 2: If a free function contains no unsafe code, there is no mandatory contract to be upheld, and it can be declared safe.

Note that Function Safety Rule 2 continues to hold even when the function calls other functions that contain unsafe code, provided that those callees themselves satisfy Function Safety Rule 1 and do not impose additional safety contracts.

3.2 Safety Comments

There are two mandatory rules and one recommended practice for documenting the safety requirements of free functions:

  • Function Comments Rule 1: All unsafe functions must document their safety requirements.
  • Function Comments Rule 2: Safety requirements must be externally verifiable and must not depend on the function’s internal implementation.
  • Function Comments Rule 3 (Recommended): At the callsite of unsafe code, users are encouraged to justify why the safety requirements are satisfied.

3.3 Example Cases

The following function foo performs a raw pointer dereference, which is an unsafe operation. The raw pointer r is derived from the function parameter p, and the function does not check whether it is valid before dereferencing it. According to Function Safety Rule 1, foo must be declared unsafe.

/// # Safety:
/// - `p` must be valid for reads.
/// - `p` must be properly aligned for `u32`.
pub unsafe fn foo(p: *const u32) -> u32 {
    let r = p;
    unsafe { *r }
}

Additionally, according to Function Comments Rule 1, developers must provide the safety requirements of the function. The safety requirements reference only the function parameter p, which is visible to the caller, rather than the internal pointer r. Therefore, they satisfy Function Comments Rule 2.

When calling the unsafe function foo, the caller must check if all safety requirements are satisfied. In the following example, the caller bar justifies why each of the safety requirements of foo is satisfied at the callsite in accordance with Function Comments Rule 3. Since all the requirements are satisfied, bar can be declared safe according to Function Safety Rule 1.

pub fn bar() {
    let mut x: u32 = 42;
    let p: *const u32 = &x;
    // Safety: 
    // - `p` is valid for reads because it points to `x`.
    // - `p` is properly aligned for `u32`.
    unsafe { foo(p); }
}

If one of the safety requirements cannot be satisfied by the caller, the caller cannot be declared safe. Moreover, the caller must propagate any unsatisfied safety requirements. Consider the example below: bar satisfies one safety requirement (valid for reads), but cannot ensure the second requirement (alignment). Therefore, bar must be declared unsafe due to the unsatisfied alignment requirement.

/// # Safety:
/// - `x` must be properly aligned for `u32`.
/// - `T` must have size at least 4.
/// - `T` must not have padding in its first 4 bytes.
pub unsafe fn bar<T>(x: T) {
    let p: *const u32 = &x as *const T as *const u32;

    // Safety: 
    // - `p` is valid for reads because it points to `x`.
    unsafe { foo(p as *mut u32); }
}

In some cases, the safety requirements of unsafe callees are not directly propagated to the caller; instead, they may manifest as different safety obligations.

/// # Safety: 
/// - `x` <= 0 or 
/// - the following three properties should be upheld:
///   * `x` must be properly aligned for `u32`.
///   * `T` must have size at least 4.
///   * `T` must not have padding in its first 4 bytes.
pub unsafe fn bar<T>(x: T) {
    let p: *const u32 = &x as *const T as *const u32;

    if x > 0 {
        // Safety: 
        // - `p` is valid for reads because it points to `x`.
        unsafe { foo(p as *mut u32); }
    }
}

Besides function calls, operations such as raw pointer dereferencing, accessing static mut values, and reading or writing union fields can be treated in the same way as unsafe callees with specific safety requirements, and the same rules apply.

4 Rules for Stucts

A struct is a user-defined data type composed of fields. Its behavior is defined through associated functions within impl blocks.

  • A method is a special associated function that takes self as the first parameter.
  • Associated functions without a receiver are commonly used for constructors or type-level operations.
  • Struct instances can also be created using struct literals, e.g., Foo { field1: value, ... }.

A struct can have a type invariant that specifies the conditions that all instances of the type must satisfy, regardless of which constructor is used to create them. Type invariants are crucial to the safety of associated functions and should be clearly documented. Similar to unsafe, there are two types of invariants:

  • Dependent invariants (mandatory): Associated functions contain unsafe code and rely on these invariants to avoid undefined behavior.
  • New invariants (by design): The invariants are unrelated to unsafe code of the struct and are introduced intentionally by design.

Type invariants are widely used in Rust-for-Linux, e.g., IovIterSource, List, ListLinks, Cursor, CursorPeek. They play a key role in preventing the safety of methods from depending on the behavior of constructors, and vice versa.

4.1 Safety Rules

  • Struct Safety Rule 1: A constructor can be declared safe if it guarantees that the type invariant of the struct is satisfied; otherwise, it must be declared unsafe.
  • Struct Safety Rule 2: For associated functions without a receiver, their safety rules are generally the same with the safety rules defined for free functions.
  • Struct Safety Rule 3: A method that contains no unsafe code can be declared safe if it does not violate the type invariant of the struct.
  • Struct Safety Rule 4: If a method contains unsafe code, it can be declared safe only if both of the following conditions are met:
      1. It does not violate the type invariant of the struct.
      1. All safety requirements of its internal unsafe code are satisfied.

4.2 Safety Comments

  • Struct Comments Rule 1 (Recommended): Each struct with unsafe constructors should document the type invariant.
  • Struct Comments Rule 2: Each unsafe associated function should document its safety requirements:
      1. The requirements should not depend on other functions of the struct.
      1. They must be externally verifiable and must not depend on the function’s internal implementation.
  • Struct Comments Rule 3 (Recommended): At the callsite of unsafe code, users are encouraged to justify why the safety requirements are satisfied.

4.3 Example Cases

Consider the following struct, there are different ways to declare the safety of its associated functions.

struct Foo<'a> {
    ptr: *mut u8,
    len: usize 
}

One possible design is to declare the constructor safe while marking other methods unsafe. In this case, this type does not maintain a validity invariant. Instead, each unsafe method defines the conditions required for safe use.

impl Foo {
    pub fn from(p: *mut u8, l: usize) -> Foo {
        Foo { ptr: p, len: l }
    }

    /// # Safety:
    /// - `self.ptr` must be valid for reads of `self.len` bytes.
    /// - The memory must be properly aligned.
    /// - The memory must not be mutated for the duration of the returned slice.
    pub unsafe fn get(&self) -> &[u8] {
        // Safety:
        // - The caller guarantees that `ptr` and `len` satisfy the requirements of `slice::from_raw_parts`.
        slice::from_raw_parts(self.ptr, self.len)
    }

    /// # Safety:
    /// - This method may break the type invariant of the struct
    /// - `l` must not exceed the size of the allocation referenced by `self.ptr`.
    pub unsafe fn set_len(&mut self, l: usize) {
        self.len = l;
    }
}

Alternatively, the constructor can be declared unsafe while the accessor method remains safe. In this design, the constructor establishes a type invariant that other methods can rely on.

/// # Safety:
/// ## Type invariants:
/// - `ptr` is valid for reads of `len` bytes.
/// - `ptr` is properly aligned.
/// - The referenced memory remains valid for the lifetime of `Foo`.
struct Foo<'a> {
    ptr: *mut u8,
    len: usize 
}

impl Foo {
    /// # Safety
    /// - `p` must be valid for reads of `l` bytes.
    /// - `p` must be properly aligned.
    /// - The memory must remain valid for the lifetime of the constructed `Foo`.
    pub unsafe fn from(p: *mut u8, l: usize) -> Foo {
        Foo { ptr: p, len: l }
    }

    pub fn get(&self) -> &[u8] {
        // Safety:
        // - The invariant is guaranteed by `from`.
        unsafe { slice::from_raw_parts(self.ptr, self.len) }
    }

    /// # Safety:
    /// - This method may break the type invariant of the struct
    /// - `l` must not exceed the size of the allocation referenced by `self.ptr`.
    pub unsafe fn set_len(&mut self, l: usize) {
        self.len = l;
    }
}

Both implementations satisfy Rust’s soundness requirement. For the first type, examples can be found in the Rust standard library, such as DwarfReader and Unique; the second type is commonly used in Rust-for-Linux, with examples including IovIterSource, Resource, and Bitmap.

5 Rules for Traits

A trait defines a collection of associated items (typically functions) that can be shared across multiple types. Traits may provide default implementations for some items; these implementations are automatically available to implementing types unless explicitly overridden.

  • When a type implements a trait, the trait’s functions behave like associated functions of the type.
  • Similar to those introduced in structs, these associated functions may or may not have a receiver.

5.1 Safety Rules

  • Trait Safety Rule 1: Declare a trait unsafe when the correctness of its implementations is required to prevent undefined behavior in safe code.
  • Trait Safety Rule 2: A trait method should be unsafe if its correct use depends on safety guarantees that must be enforced by the caller.

5.2 Safety Comments

  • Trait Comments Rule 1: An unsafe trait must document the safety invariants that all implementations are required to uphold.

  • Trait Comments Rule 2: Each unsafe method of a trait must clearly document the safety requirements that callers must satisfy.

    • The safety requirements of an unsafe method are distinct from the trait invariants.
    • The safety requirements of an unsafe method must be specified in the trait definition, where they form part of the trait’s contract.
    • Implementations should not introduce stricter safety requirements than those declared by the trait.
  • Trait Comments Rule 3 (Recommended): Implementations of unsafe traits are encouraged to justify why the required invariants are satisfied.

5.3 Example Cases

The following example introduces a trait Buffer that should be declared unsafe, because incorrectly implementing it may introduce undefined behavior. The trait defines an unsafe method get_unchecked. It cannot be declared safe because its safety requirements cannot be guaranteed solely by the trait invariant.

/// # Safety:
/// ## Trait invariants: 
/// - Implementors must guarantee that `as_bytes()` always returns a slice pointing to valid memory for reads
/// - and that the slice remains valid for the lifetime of the returned reference. 
unsafe trait Buffer {
    fn as_bytes(&self) -> &[u8];

    /// # Safety:
    /// - The caller must ensure that `index < self.as_bytes().len()`.
    unsafe fn get_unchecked(&self, index: usize) -> u8;
}
1 Like

This isn't true in general. You have to look at the whole module to determine that.

pub struct MyVec<T> {
    ptr: *mut T,
    capacity: usize,
    len: usize,
}

pub fn set_len<T>(vec: &mut MyVec<T>, len: usize) {
    vec.len = len;
}

// more functions that create or push to a MyVec

Here the set_len function is a free function that contains no unsafe code, but it can still cause UB, because the private fields of MyVec are visible in the whole module. You also have to look at the whole module for free functions.

8 Likes

Something that should probably be listed explicitly: can a safe trait ever have an unsafe method?

I've long thought that the answer to this is "no" because a safe trait is not supposed to be able to cause unsoundness if implemented incorrectly, and one example of implementing the trait incorrectly would be by writing an unsafe method with the wrong preconditions, something that seems like it would necessarily lead to undefined behaviour because users of the trait would be unaware of the safety condition.

I did recently notice that there was an exception, though: it is possible for a safe trait to have an unsafe method if the trait doesn't constrain what its unsafe preconditions are. Anything that called the method on an unknown type (i.e. a type obtained via a type variable) would then need to itself be unsafe, and place its own preconditions on what the unsafe preconditions of the type were.

I don't have a strong opinion about how this is resolved, but think that the issue should at least be discussed.

Is this a real-world case or merely a toy example?

This is a toy example in the sense that i wouldn't make this a free function, but a method (like it is done in the standard library: Vec::set_len). Then your rules for structs would apply and it wouldn't be allowed to be a safe function anymore.

I just wanted to show that there isn't really a distintion between free functions and methods and therefore i don't think you can really put them into two categories like that.

2 Likes

I'm a bit confused by this. If the implementation does not use unsafe operations, how could it be unsound? The most minimal example would be:

trait Foo {
    /// Safety: This function must not be called.
    unsafe fn dont_call_me();
}

How can a safe (and even unsafe) implementation cause UB?

In other words, an unsafe function does not need to have correctness guarantees (which would make its trait unsafe), it only has soundness requirements.

1 Like

Thanks for the suggestion. From my perspective, the safety of a trait and that of its methods are orthogonal. There are several safe traits in the Rust standard library that define unsafe methods, e.g., Step, FromRawFd, CommandExt

1 Like

I should have said "can a safe trait ever usefully have an unsafe method?".

Yes, it's clearly possible to give a safe trait an unsafe method that can't be called, but then there's no purpose for the method existing in the trait. The problem is about whether you can have one that can be called.

1 Like

Unrelated to the topic, but just a minor detail. This is unsound without requiring that T does not have padding bytes in its first 4 bytes (and possibly not even pointer bytes depending on What about: Pointer-to-integer transmutes? · Issue #286 · rust-lang/unsafe-code-guidelines · GitHub).

3 Likes

I agree that this is somewhat confusing. Your example, in essence, modifies the struct instance via a literal assignment. The underlying issue stems from Rust’s default module-level visibility rules. These issues are discussed in Section 5, particularly under Weak Struct-level Soundness Criterion.

It's not about usefulness either. An unsafe function does not need to have correctness guarantees to be useful. You can literally put any generic unsafe free function into a trait where the generic becomes the implementing type:

trait Foo {
    unsafe fn get_unchecked(xs: &[Self], i: usize) -> &Self;
}

But you can't (correctly) call this function unless you know what concrete type you're calling it on, so it isn't useful as a trait method – the trait doesn't (and due to being a safe trait can't) specify what the unsafe preconditions on get_unchecked are, so the caller has no way to know that it's complying with them.

(As I mentioned earlier, it is possible for the caller to delegate the responsibility of checking the unsafe preconditions to something that does know what concrete type and thus which concrete implementation is in use, which makes it not entirely useless. I'm not sure whether there are many use cases for this pattern, though – I was considering using it in one project but eventually ended up doing something else.)

Miri does not report undefined behavior in the following case: Rust Playground. Is there an example that would actually trigger UB here?

That's not how it works.

Whether your function is safe documents a contract between the module or crate and its users. If the function is prepared to accept all possible inputs, and the caller can call it anytime they want and do anything with the outputs, the function is safe. If not, the function is unsafe. Whether the implementation calls other unsafe APIs is mostly an orthogonal issue: that's a separate contract at a different boundary.

For instance:

pub struct EvenNumber(u32);

/// Safety: `x` must be even.
pub unsafe fn new_even_unchecked(x: u32) -> EvenNumber {
    EvenNumber(x)
}
6 Likes

Rust Playground (I've marked the NEW and UPDATED lines with comments)

1 Like

I assumed the safety precondition would be obvious since it's the same as the free function. Calling <[u8]>::get_unchecked() and <u8 as Foo>::get_unchecked() requires to discharge the exact same conditions. There's nothing unknown to the caller.

Nothing is forcing an implementation of that trait to have the same safety conditions, though.

Your declaration is

trait Foo {
    unsafe fn get_unchecked(xs: &[Self], i: usize) -> &Self;
}

and it could be implemented like tihs:

trait Foo {
    /// Safety: `xs` must be at least five elements long.
    unsafe fn get_unchecked(xs: &[Self], _i: usize) -> &Self {
        xs.get_unchecked(4)
    }
}

Due to being a safe trait, an incorrect implementation is not allowed to create undefined behaviour. This trait has been implemented incorrectly in a sense (the unsafe precondition is not the expected one), but the implementation complies with its safety conditions. As such, it would be desirable for a caller that complied with the safety conditions stated in the trait to be sound, but in this case calls to the method can be UB because the caller doesn't know about the extra safety condition that got added by the implementation.

The issue is fixed now. Thanks.

1 Like

Thanks. Does such a case exist in the Rust standard library? First of all, the free function new_even_unchecked uses the literal constructor. Therefore, we can reduce the issue to the soundness of the struct EvenNumber. From my perspective, it is semantically incorrect but should not cause a memory safety issue. Think about the real-world example String::from_utf8_unchecked. It is marked unsafe because violating the type invariant would trigger a memory safety issue when accessing the string’s memory via other methods containing unsafe operations like ptr::copy(). In other words, the error itself can be viewed as a source, and memory access can be viewed as a sink. If a struct contains no sink, there is no need to declare EvenNumber::new_even_unchecked() as unsafe.

I don't know what you mean by "soundness of a struct". It's functions that can be unsound, not structs.

I might add this function later:

/// Result is guaranteed to be even.
pub fn even_to_u32(a: EvenNumber) -> u32 {
    a.0
}

If new_even_unchecked didn't declare the precondition, I wouldn't be able to promise the guarantee that even_to_u32 always returns an even number. So the unsafe is necessary.