Using blanket impls with sealed traits can leak sealed traits into the public API?

Apologies if this falls under "help", but it seems like a surprising enough behavior to me I thought IRLO would be a more appropriate forum.

Here is an example lib.rs for a crate which I've published as sealed-trait-test:

mod sealed {
    pub trait Sealed {
        fn sealed_plus_one(n: usize) -> usize {
            n + 1
        }
    }
}

pub trait Public: sealed::Sealed {}

impl<T: Public> sealed::Sealed for T {}

pub struct Example;

impl Public for Example {}

...the goal being to use Sealed as an internal implementation detail without it being part of the public API, so as to allow making changes to it without those changes being breaking.

But lo and behold , this trait is callable from a 3rd party crate:

use sealed_trait_test::{Public, Example};

fn printer<T: Public>() {
    println!("plus one: {}", T::sealed_plus_one(41));
}

fn main() {
    printer::<Example>();
}

...prints 42.

Perhaps this is a weird eccentricity about blanket impls I didn't understand, but it's possible to call Sealed::sealed_plus_one without the Sealed trait in scope in this case, which means it's been leaked into the public API thus defeating the point of using a sealed trait in the first place.

If nothing else, perhaps this sharp edge should be documented along with sealed traits?

2 Likes

I would assume it's not the blanket impl that leaks it, it's the supertrait bound. It should be possible to do something like this using two private traits (one Sealed marker supertrait to block outside implementations, one blanket implemented but not a supertrait to provide the internal API).

Though, in this case because you're blanket implementing the private trait anyone can actually implement it externally by just doing impl sealed_trait_test::Public for Foo {}, so I'm not sure how sealed any of this is?

2 Likes

@Nemo157 oh whoops, I now realize my blanket impl example is backwards of what I was trying to illustrate. In the non-contrived code, it's:

impl<T: sealed::Sealed> Public for T {}

But you're also right the blanket impl is a red herring and it's the supertrait bound I was actually curious here, and that sealed supertraits are still callable downstream.

The idea of separating the trait I want sealed from some other sealed marker supertrait to prevent external impls is a good one, thanks!

If you really want sealed_plus_one to only be callable inside the defining crate then you could make it take a ZST token that's nameable only inside your crate with the same pub-in-private trick. Just make sure not to derive Default on it or stuff like that which makes it creable without naming it.

1 Like

Issue #83882.