Kotlin / kotlinx.collections.immutable

Immutable persistent collections for Kotlin
Apache License 2.0
1.12k stars 56 forks source link

Make all immutable interfaces `sealed` to prevent implementations that allow mutations #147

Open yogurtearl opened 1 year ago

yogurtearl commented 1 year ago

Right now you can roll your own mutable version of ImmutableList with something like this:

class SneakyList<E>(
    val mutableList: MutableList<E> = mutableListOf()
): ImmutableList<E> {
    override val size: Int
        get() = mutableList.size

    override fun get(index: Int): E = mutableList[index]
.
.
.
}

Even well-meaning implementations of the ImmutableList interface might accidentally enable mutability. To make ImmutableList (and ImmutableCollection, etc) even more immutable, make all of the the interfaces in this library sealed interfaces.

wzsaz commented 9 months ago

I don't think this is necessary. Wrongfully implementing an interface will always cause problems. Just because someone could implement the List interface incorrectly you would not make it sealed. Sealed interfaces should be conceptually reserved to represent closed implementation hierarchies.

yogurtearl commented 9 months ago

If the implementation of the interface can be mutable, then it is not "immutable" it is just a readonly interface similar to List

Laxystem commented 3 months ago

If the implementation of the interface can be mutable, then it is not "immutable" it is just a readonly interface similar to List

Correct. But immutability is a contract. Just like Java's mutability is a contract broken by Guava's immutable collections. Making ImmutableList sealed will prevent people from implementing immutable collections on their own.

Consider this:

https://github.com/Kotlin/kotlinx.collections.immutable/blob/1d18389e32e8afdbf5caa89808fb5af308fb5c8d/core/commonMain/src/adapters/ReadOnlyCollectionAdapters.kt#L11C1-L20C2

qurbonzoda commented 1 month ago

The Immutable and Persistent interfaces were introduced to allow users implement alternative versions that better suite their use cases. For example, in projects where collections are frequently accessed but very rarely updated, a regular copy-on-write implementation of these interfaces might be more appropriate. However, we recognize that only a small number of users might find this flexibility useful. Therefore, it is very helpful to hear from you whether you utilize this flexibility or it instead poses challenges for you.

In the future, we might decide to limit the implementation of these interfaces or expose the implementation classes. So that users can be confident that the implementation is indeed immutable.

yogurtearl commented 1 month ago

I find that kotlin.collections.List provides the needed flexibility for alternative list implementations. But when using ImmutableList I am looking for the strongest possible multiplatform immutability guarantees from a security and thread safety point of view. (I know reflection, serialization, agents, etc can be used to subvert some guarantees)

As an example, if I bring in 2 third party SDKs (sdk1 and sdk2) I want to be sure that immutableList won't change after I pass it into sdk2:

val immutableList: ImmutableList = sdk1.getData()

val processedData: List = sdk2.process(immutableList)

// immutableList is unchanged
yogurtearl commented 1 month ago

To add to the example above, if sdk1.getData() returned a custom mutable implementation of ImmutableList and Sdk2 was written in Java, then Java might happily modify the list when I call sdk2.process(immutableList) (should throw an exception).

public class Sdk2 {
    static void process(List<String> list) {
        list.add("foo");
    }
}

Also, I imagine there are potential performance gains that could be had by knowing all possible implementations of ImmutableList statically at compile time? i.e. faster forEach(), map() etc.

Laxystem commented 1 month ago

To further my point, if we make ImmutableList sealed, shouldn't we do the same for mutable ones, too? Afterall, the same arguments apply - the contract can be broken, it could have some performance benefits...

What you're suggesting applies to any interface: if we couple the implementation with the API (by making the interface sealed), we can get better performance and reliability.

But SOLID tells us not to. The interface is open for a reason - to allow alternative implementations - for example, to wrap native list implementations - why would I make them falsely implement mutable list and then wrap them with an adapter instead of just implementing immutable list?

Sporking commented 5 days ago

I don't think ImmutableList should be a sealed interface. I think it should be a final class, not an interface at all, which is what Google Guava does. Guava does it that way for pretty much exactly the reasons described in this bug report. (Specifically, that allowing additional implementations of ImmutableList and other immutable types can create serious security holes.)

This is not the first time this issue has been brought up. See for instance my comments here and here on a bug which was filed in 2017.

Regarding @Laxystem's argument that this isn't SOLID: That's true, but irrelevant in this case. Immutable types are often necessary for security, and as is often true in security, maximally flexible code is often unfixably insecure code. Sometimes making sure that nobody CAN violate a contract (even if they want to!) is very important. There's a reason that java.util.String doesn't allow subclasses of it to be created. If someone wants a "kind of, mostly, sort of immutable" class that is flexible to their own needs, they can subclass the List interface and do what they like. The entire point of an ImmutableList interface, on the other hand, is that its contents are NOT, EVER, UNDER ANY CIRCUMSTANCES supposed to change, unlike other subclasses of the List interface which does not make that guarantee. The fact that an ImmutableList is truly immutable enables special behavior such as being able to "copy" by just passing references around without needing to make defensive copies of the referenced data: that would not be possible if some "kind of, mostly, sort of immutable" subclass was being used. Therefore allowing subclasses of ImmutableList to be created violates this immutability guarantee and defeats the purpose and trustworthyness of ImmutableList itself.