Sorry for taking a while to respond; I've compiled a list of examples of this pattern in common crates [0], including the standard library. For each crate, safe functions and closures above the horizontal rule depend on the behavior of other functions, and those below the horizontal rule depend only on the behavior of their containing function. Closures* marked with an asterisk capture data from their environment, so it would take some effort to refactor those to use unsafe fn() pointers.
One example of the safe trait implementation scenario is in tracing-log [1]. The <LogVisitor<'a> as Visit>::record_str() method would leak dangling &str references if the value argument didn't live as long as 'a. LogVisitor<'a> is a private type in the tracing-log crate, and Visit is a public trait in the tracing-core crate. The Visit trait had existed before tracing-log was first released, so it could not be modified without breaking backward compatibility.
As written, there is no actual way for someone to cause UB with this library. tracing-log is written so that LogVisitor<'a> values only ever go into Event::<'a>::record(), and tracing-core is written so that it passes the visitor &str references directly from the Event<'a>, that live at least as long as it. If someone added a function to tracing-log that otherwise called <LogVisitor<'a> as Visit>::record_str(), or if someone modified tracing-core to pass a copy of the string to Visit::record_str(), then someone could cause UB with this library; but no one has done so, so no one can cause UB.
> The acid test here is whether it is possible that if the implementation of Option::map() were to ever change (while itself still being sound) whether it is possible that it could expose unsoundness without violating the FnOnce preconditions, i.e. without causing a compiler error. If a language (or library) version upgrades can expose unsoundness in your code, then your function is unsound.
Are you saying that unsafe invariants must never be broken, even if external functions have behavior completely different from how they are documented? In that case, it would seem like nearly all nontrivial unsafe code would disappoint you, since it depends at least on the safe standard library functions like Option::map() doing what they say on the tin. Public documentation of behavior is a binding promise under the SemVer regime that Cargo follows.
More generally, I think we both agree that a sound library is one that "cannot be used to cause UB from safe code". But I say that it's fine as long as the public interface as it stands (or well-defined internal interface) cannot be used to cause UB, whereas you say that it must not cause UB even if someone were to hypothetically add another function that calls any safe private function.
To me, that standard seems somewhat arbitrary. Suppose I were to add a new function inside the alloc::vec module:
pub fn safe_set_len<T, A: Allocator>(vec: &mut Vec<T, A>, new_len: usize) {
vec.len = new_len;
}
This function is perfectly safe to write, and it would be perfectly safe for external users to call. However, it would allow half of the safe Vec methods to cause UB. So since I could hypothetically add this safe function, does that mean that the Vec API is already unsound? I'd say not, since there is an invariant attached to Vec that the length is no greater than the capacity, and adding this function would allow this invariant to be violated.
Would you disagree, and say that the Vec API as a whole is unsound, since the soundness of some of its unsafe code depends on the nonexistence of certain functions that would be safe to write? Or would you instead say that such an invariant can be attached to a type, but only begrudgingly to an outer function, and never to a module or a crate? I can't see the latter distinction as being anything but arbitrary.
You claim that your position "is the default assumption of any Rust developer", but I think my list of examples provides evidence against that claim: plenty of large crates' maintainers have little issue defining "safe" private functions or closures that reach outward for their invariants. What's important is that the maintainers don't actually merge any PRs that break the invariants. (Ideally, this would be by way of thorough documentation, but that is far from strictly necessary.)
[0] https://gist.github.com/LegionMammal978/7654f484b495d67c63d0...
[1] https://github.com/tokio-rs/tracing/blob/tracing-log-0.1.3/t...