Conditions for unsafe code to rely on correctness

TLDR

Question: What's the ecosystem convention for unsafe code relying on correctness?

Answer: Unsafe code may assume the direct dependencies of its crate to be correct.

Rationale: Even though this allows safe programs to cause undefined behavior, this is the most practical option at this time.

The alternative option would be for unsafe code to only rely on what it can trust (either by verification or by assumption of a safety documentation). That's the strongest option and the one argued by the Rustonomicon. To verify a direct dependency, it must first be pinned. This is the major downside of this option. Also, crates rarely document safety guarantees (if at all).

If the type system provided a syntax for safety guarantees (the same way it provides one for safety requirements), then maybe crates would document safety guarantees. This was suggested in the unsafe mental model. The main issue being that it is not always clear what the safety requirements of a function should be, since it depends how unsafe code plans to use it.


Updated details


Original details

For example, let's assume primes is a crate published on crates.io with the following public API:

/// Returns the prime numbers fitting `u32` in order.
pub fn iter() -> impl Iterator<Item = u32>;

Let's assume another crate crypto also published on crates.io with the following unsafe code:

let first_prime = primes::iter().next().unwrap();
// SAFETY: The first prime number is 2 and it fits `u32`.
unsafe { std::hint::assert_unchecked(first_prime == 2) };

Finally, let's assume primes::iter() is implemented as follows:

pub fn iter() -> impl Iterator<Item = u32> {
    1..10
}

Which crate should get a "soundness issue" advisory from RUSTSEC?

I thought the convention (and uncontested opinion) was that it should be crypto (because it relies on a correctness guarantee without checking the implementation and pinning the version), but I've recently seen knowledgeable people argue it should be primes (because it is incorrect and crates on crates.io should be correct). So now I'm unsure and I can't find any official or authoritative documentation in that regard.

My rationalization of the convention I thought was in place (crypto is unsound), is that the Rust type system only provides safety guarantees, not correctness guarantees. On the one hand, while the ecosystem should aim for publishing correct crates only, there is little hope of enforcing that at scale. On the other hand, enforcing that only sound crates (more generally "correct for safety" crates) are published is more realistic. Safe crates are proved sound by the type system (for free). For the minority (about 25%) of crates with unsafe code, enforcement is either passive through soundness bug reports or active through unsafe code reviews.

Note that this is only about crates.io. Other cargo registries or build systems can have their own conventions.

What do you mean by verification? If you are referring to formal verification, I think most crates in the ecosystem have not undergone that process, as it is extremely costly. If you mean manual review, then I prefer

Unsafe code can always rely on correctness.

In this case, I think primes is the culprit and contains a bug. However, this bug cannot trigger undefined behavior on its own. It would require being combined with other unsafe code whose misuse can lead to UB, such as dereferencing a raw pointer. In that sense, the two form a source–sink relationship: Prime introduces the problematic state, and the unsafe operation acts as the sink that actually triggers UB.

My own approach to this is a "declared robustness" approach – a crate can explicitly declare a correctness property to be reliable for unsafe code, but if it doesn't, the writer of the unsafe code should check to make sure that any correctness properties that they rely on for soundness actually hold. (Anything stated in the standard library documentation is assumed to be specified robustly unless it says that isn't (e.g. if it says "not a stable guarantee"), primarily for practical reasons.)

I don't know whether or not my approach is standard. The other common approach seems to be "assume your upstream is correct, but don't assume your downstream is correct" but I'm not sure that the reasoning behind that is actually valid.

(That said, I try hard to avoid using upstream crates at all, so for me the problem doesn't really come up.)

3 Likes

I mean manual verification. Let me update the original message to clarify.

If the correctness claim comes with a formal proof, then the verification may simply consist of running the tool to check the proof. But this probably never happens in the ecosystem at this time. If it does, one could imagine crates.io running such tool and prevent publication if the check fails. Now the verification process is simply to check that the crate indeed comes from crates.io after the introduction of such mechanism.

What matters is not how the verification occurs, but who is responsible for it.

Do you have any example of such documentation?

But indeed, that's a 4th option I forgot to mention (even though I wrote about it in the unsafe mental model...). It's just that it comes with its own set of difficulties.

/// An assertion that the AVX and AVX-2 features exist on the
/// processor running the program.
///
/// ## Safety
///
/// If an instance of this structure exists, it implies
/// `is_x86_feature_detected!("avx2")` and unsafe code may rely on
/// this guarantee for soundness purposes.  As such, there is a
/// soundness requirement on creating that structure that the feature
/// is in fact detected.
#[non_exhaustive]
#[derive(Debug, Clone, Copy)]
pub struct Avx2Assertion;

Looking back on this, I should probably write a comment somewhere explaining that the #[non_exhaustive] is part of what fulfils the safety requirement, because it isn't obvious!

It's written in the form of a safety requirement, which it is within the module, but from the point of view of users of the module it's a robustness guarantee ("unsafe code may rely on this guarantee").

I think the user of unsafe code (the sink) is responsible for verification when the satisfaction of the unsafe code’s safety requirements depends on an upstream crate.

1 Like

I think the status quo of the ecosystem today is fairly clear: it is generally fine for a crate to rely on documented statements of other crates, including for soundness.

The alternative seems to be that the prime crate has to add some magic word to elevate its promise to something unsafe code can rely on; putting that keyword in all the right places seems like a tedious process with little payoff. Every single data structure crate (from SmallVec to fancy persistent data structures) would need to add such wording to be useful to unsafe code. I am not aware of any crate doing so, nor have I seen people conclude "unsafe code may not use any of those crates in a soundness-critical way". I am sure there are tons and tons of examples of unsafe code out there that rely on the correctness of data structures implemented outside std.

That sounds like a slightly different question to me and is more of a RUSTSEC policy question. The root cause of the bug unambiguously is with primes IMO, but as usual if you have subcontractors/dependencies then to some extent you take on responsibility for them.

primes itself is sound (one cannot cause UB with safe code using it) while crypto is not.

5 Likes

This is not exactly an example of safety guarantee. It's an example of safety invariant (which acts as both requirement and guarantee) encoded in the type system with a newtype. What I'm looking for is an example of a function with a safety guarantee in its documentation (i.e. not encoded in the type system).

Cool! Is this documented somewhere? If not, where should it go?

If it's different, then what does "fine" mean in your previous quoted sentence? I would naively expect the RUSTSEC policy to follow that convention. Or maybe there's simply no hard rule, since you used the word "generally"?

If this happens frequently enough, RUSTSEC could add fields to specify two packages. One for the buggy package (the crate containing the correctness bug), and one for the responsible package (the crate where unsafe code relies on correctness of the buggy code for its soundness).

Or it could create two separate advisories, an "informational" one for the buggy crate, and an "unsoundness" one for the unsafe crate, which references the "informational" advisory as "cause".

4 Likes

Interesting options, but in both cases the unsound crate still gets an advisory, which means that RUSTSEC (and thus the ecosystem because dependabot ingests those advisories and will transitively nag all reverse dependencies) provides an incentive for unsafe code to check and pin the crates which correctness it relies on, making it a de facto convention.

The more I think about it, the more it seems there's probably no convention at ecosystem-level and it's more of a crate-by-crate decision whether being unsound is an issue for the crate author (the same way it's a crate-by-crate decision whether being incorrect is an issue). If a crate author wants to avoid unsoundness (either to avoid being responsible for security vulnerabilities, the cause of dependabot security updates, or other reasons), then they will check and pin their correctness-relevant-for-safety dependencies. Otherwise they won't. Maybe that's also why there is no official documentation.

I have inferred this from what I know about the current state of the ecosystem. (I don't think I necessarily have a great overview over the ecosystem so I am curious to hear from other people about what they have seen in the wild.)

We don't have a constitution that officially lays down the social norms around Rust crates, so I don't think there is an obvious place to document this.

What I mean is that there are two factors that play together to cause this soundness bug (primes failing to implement documented behavior and crypto relying on documented behavior for soundness) and it's not clear where, pragmatically, the best place is for such an advisory. I don't know how people usually use these advisories, or how common it even is to issue one for soundness bugs.

From a purely formal perspective, I think the answer is clear: primes is sound, so it would be odd to have an advisory saying otherwise.

Pinning dependencies generally is an antipattern, isn't it? I don't think we should recommend that, and I haven't seen it happen much in the wild.

2 Likes

From my experience, the main way those advisories surface to users are through dependabot on Github. Here is an example PR (from last week). From Github you can't distinguish it from a regular version update PR, but in the email you receive from Github you have the following line at the top:

This automated pull request fixes a security vulnerability (moderate severity).

You might not have access to the link, but it's essentially a Github rendering of the information in the RUSTSEC advisory.

There seems to be about 30 soundness issues per year from this approximate query in the RUSTSEC advisory-db:

% git grep -l -e '\<UB\>' -e '[Uu]ndefined.[Bb]ehavior' -e '[Ss]ound' crates | xargs sed -En 's/^date = "(.*)-..-.."$/\1/p' | sort | uniq -c
      3 2018
     11 2019
     71 2020
     45 2021
     26 2022
     30 2023
     26 2024
     38 2025
      6 2026

For my project I only got 7 such security update PRs from dependabot in 2025 (I didn't check how many are due to UB).

The way you file one is through a PR to the advisory database.

Yes I think this is clear. With its current documentation, primes can only be incorrect. Only if it were to document a safety guarantee about the first element being 2, then it would be unsound (or more precisely, incorrect for safety). But there's no such thing as safety guarantees (I've never seen any except in the unsafe mental model and in the #[ensures] clause of safety contracts).

Yes, that's an antipattern. But having large unsafe scope is also an antipattern. The argument I've heard from those advocating to check and pin dependencies, is that it should be very rare for unsafe code to depend on correctness of "external" code (code external to the crate where the unsafe code lives, where the standard library is an exception and assumed correct).

1 Like

AFAIK "unsoundness" is an "informative" advisory, so I hope it wouldn't be raised as "moderate severity"...

I would be somewhat surprised if this is so rare, in particular when it comes to containers and similar data structures. The standard library provides the basics, but there is a long tail of data structures that can easily come up inside unsafe libraries.

As far as I can tell, the nearest "official" statement is in the nomicon:

The design of the safe/unsafe split means that there is an asymmetric trust relationship between Safe and Unsafe Rust. Safe Rust inherently has to trust that any Unsafe Rust it touches has been written correctly. On the other hand, Unsafe Rust cannot trust Safe Rust without care.

3 Likes

Thanks! This sounds familiar and probably one of the memory bits that gave me the impression it was a convention.

Yes, the maintainers guide says that UB is informational if it cannot be triggered by malicious input. The example I gave is a library, so it is not possible to know if the final program could trigger UB on malicious input to that final program, which I guess is why a conservative choice was made.

Note that on the Github advisory the severity is High. Only on the RUSTSEC one it is Medium.

Using unsafe is inherently risky, due to the possibility of human error. It gets more risky if you rely on an external crate. That doesn't mean it is wrong to do this, it all depends on the risk/reward situation and the context. But we routinely depend on external crates, which may have unsafe code in them. I don't think there is any need for a convention, use unsafe judiciously ( and use miri as well to try and weed out issues ).

Pinning (i.e. using anything other than a ^ semver constraint) a general dependency (i.e. not an internal crate that others should never rely on like an internal proc-macro crate) in a library crate is extremely disruptive to the ecosystem and should basically never be done.

I think in general for advisories the best resolution for a situation like this would be that primes releases a new patch version fixing the bug, then crypto releases a new patch version raising its minimum version constraint on primes up to the fixed version, then the advisory is published against crypto and users upgrading it will implicitly upgrade primes getting the issue solved.

4 Likes

I agree with @RalfJung that it is standard to rely on documented guarantees, but I also am hesitant to rely on guarantees made by non-"foundational" crates (a vague definition; my definition of it will probably be something along the lines of "was made for consumption by unsafe code").

In std we don't do this for user-implementable traits. E.g. we don't trust trait ExactSizeIterator impls for unsafe code and have an unsafe trait TrustedLen. This is partially because it's used in specialization, but there also are internal methods that outright require I: TrustedLen for their inputs.

So I think an important distinction is relying on the correctness of a concrete implementation - which is a finite amount of effort to review if necessary - vs. relying on an unbounded set.

This thread isn't about traits. It's about functions on concrete types. That's a huge difference.

Of course you can't rely on this for traits, in generic code. It's trivial for downstream code from your crate to write safe code that violates the trait's documentation. That's an entirely different situation than what we discuss here where we are invoking upstream code.

4 Likes