Thanks for the replies! Based on them, I believe the thread is actually about solving the following (apparent) contradiction.
To save space, I'll use the convention that x_y is the item y of crate x and that N is the type of natural numbers. All scenarios are from the point of view of the crypto crate.
Let's call Extern the following scenario:
/// Returns the `i`-th prime number (panics on zero).
pub fn primes_get(i: N) -> N; // it's really pub use primes::get;
pub fn crypto_test() {
unsafe { assert_unchecked(primes_get(1) == 2) }
}
Let's call Intern the following scenario:
fn crypto_get(i: N) -> N { /* correct implementation */ }
pub fn crypto_test() {
unsafe { assert_unchecked(crypto_get(1) == 2) }
}
Let's call Param the following scenario:
/// Safety: `prime(i)` must return the `i`-th prime number for `i` positive.
pub unsafe fn crypto_test(prime: fn(N) -> N) {
unsafe { assert_unchecked(prime(1) == 2) }
}
Let's call Trait the following scenario:
/// Safety: `get(i)` must return the `i`-th prime number for `i` positive.
pub unsafe trait Primes {
fn get(i: N) -> N;
}
pub fn crypto_test<P: Primes>() {
unsafe { assert_unchecked(P::get(1) == 2) }
}
In the Param and Trait scenarios, crypto_test is abstract over the function returning the i-th prime number (through prime and P respectively).
In the Intern scenario, crypto_test is not abstract over crypto_get (it's a concrete implementation).
However, in the Extern scenario, we need to look at how the crypto crate depends on the primes crate:
- If it depends with
= then it's not abstract. It's equivalent to the Intern scenario.
- If it depends with
^ then we need to check the presence of a top-level Cargo.lock file:
- If there is one (e.g. because we know the final binary), then it's not abstract. It's equivalent to using
= which is equivalent to the Intern scenario.
- If there is none (e.g. because we look at the
crypto library only), it seems to me that crypto_test should be considered abstract over primes_get since there's not yet any concrete implementation.
In that very last case, is crypto_test sound in the same way it is in the Trait, Intern, and even Param scenarios?
- If yes, what's the argumentation and how does it compare with the argumentation for
Trait, Intern, and Param?
- If no, what's the rationale to accept such code while rejecting the
Trait (resp. Param) scenario if it would use a safe trait (resp. a safe function)?
Note that it is safe to use a [patch.crates-io.primes] section in the top-level Cargo.toml (either with the official git repository or with a local path to safe code). This essentially means the final binary may use any possible safe implementation for primes_get, making it similar to the Param scenario but where the argument is provided at binary-level instead of each call-site.
I suspect the answer is of practical matter, because crypto_test is theoretically unsound in the Extern scenario (there is a safe binary that triggers UB because of crypto). The ecosystem simply accepts such unsoundness because the alternative would be worse (kind of like it accepts the File and Command APIs being unsound because there's no unsafe keyword to track environmental UBs: it's not practical for the theoretical benefit).
So this suggests that unsafe code can indeed rely on the correctness of external items (for internal items it's obvious), not because it is theoretically sound to do so, but because there's no reasonable alternative yet (maybe one day something based on cargo-vet could work, the same way a paranoid setting in cargo could ask to type cargo build --unsafe="executions of the compiled binary won't have environmental UB" or refuse to build).