Pre-RFC: 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 criteria 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 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 come 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
/// ## Struct invariant
/// - `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}
    }
}

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.3 Decoupling

To provide clearer and more actionable guidance, decoupling the relationships between safety responsibilities is very important. One key decoupling strategy is to separate the safety of free functions and structs. In particular, we conceptually treat struct construction and field access as follows: a free function creates a struct instance only via the struct’s constructors (including struct literals), and any direct field access is modeled as an invocation of the struct’s implicit methods.

For example, in the following code, we can treat new_even_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
/// ## Struct invariant
/// - `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))
}

In the same way, direct field assignments (e.g., even_number.0 = ...) can be treated as invoking the literal methods of the struct.

3 Establish A Soundness Criterion

Existing soundness criteria require only that safe code cannot cause undefined behavior, but they do not clearly specify the scope in which such code usage is considered. Visibility plays a critical role in soundness, because it determines which and how APIs are accessible.

Therefore, the first step for a Rust project to follow this standard is to establish a visibility-aware soundness criterion that enables systematic soundness auditing.

3.1 Visibility

The basic unit of Rust software is a crate. Each crate can contain one or more modules, which in turn can contain submodules, forming a tree-like structure.

  • Module-based visibility:
    • By default, functions, structs, and struct fields are only visible within the module in which they are defined (i.e., private).
    • Exception: Items of a public trait are automatically public, regardless of whether they are explicitly marked pub; variants of enumeration types are public if the enum itself is public.
  • Visibility granularity: Rust supports different scopes:
    • pub: the item is visible outside the crate.
    • pub(crate): the item is visible anywhere within the current crate.
    • pub(in path): the item is visible only within the specified module path.
    • pub(super): the item is visible to the parent module.

A program component can rely on visibility restrictions to maintain sound abstractions.

3.2 Visibility-based Soundness Criteria

Before determining whether each function should be declared safe or unsafe, the project owner should be explicit about which criterion is being adopted, as it guides the subsequent rules for soundness checking. The project owner may select the soundness guarantees that best suit their project.

  • Module-level Soundness Criterion (default): 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, as a module represents the smallest unit of accessibility control.

For example, the following code is considered sound even if it is possible to store an arbitrary raw pointer into HOOK using only safe code. This criterion has been confirmed by the library team in issues/152078.

static HOOK: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut());

pub fn rust_oom(layout: Layout) -> ! { 
     crate::sys::backtrace::__rust_end_short_backtrace(|| { 
         let hook = HOOK.load(Ordering::Acquire); 
         let hook: fn(Layout) = 
             if hook.is_null() { default_alloc_error_hook } else { unsafe { mem::transmute(hook) } }; 
         hook(layout); 
         crate::process::abort() 
     }) 
 } 
  • Crate-level Soundness Criterion (relaxed): All uses of the crate’s public safe items (or unsafe items when their safety requirements are satisfied) from outside the crate must not cause undefined behavior.

This is a more relaxed criterion while still providing soundness guarantees. Some systems may nevertheless prefer such safety criteria because certain APIs are designed for specific usage contexts and can rely on global or system state to ensure safety.

  • Struct-level Soundness Criterion (stricter): All uses of a struct’s safe items (or unsafe items when their safety requirements are satisfied) must not cause undefined behavior, even within the same module.

This is the most strict criterion. Since all private methods and fields are accessible within a module, a struct cannot rely on visibility to prevent misuse from code in the same module. It is equivelent to the module-level soundness criterion if the module has only one struct.

Ensuring struct-level soundness can be challenging, particularly when considering struct literals, which allow creation of instances without going through constructors that uphold the type invariant. This might be achievable in the future through the use of unsafe fields. For example, Vec is defined as follows. Developers within the same module can easily create a Vec instance or modify len via struct literals in ways that violate the type invariant.

pub struct Vec<T, #[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global> {
    buf: RawVec<T, A>,
    len: usize,
}

3.3 Principle of Least Unsound Scope

The soundness criterion serves as the last line of defense against undefined behavior. Whenever possible, safety should be ensured earlier by restricting the scope of unsafe code or enforcing invariants locally.

For example, in Rust-for-Linux’s List, the private method insert_inner is declared unsafe with clearly documented safety requirements to prevent misuse. This demonstrates that even if a function is not part of the public API, it is recommended to mark it unsafe when its correct use depends on invariants that cannot be enforced by the type system alone.

4 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.

4.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.

4.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.

4.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.

5 Rules for Structs

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.

5.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.

5.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.

5.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 invariant
/// - `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.

6 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.

6.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.

6.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.

6.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 invariant: 
/// - 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.

6 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.