Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
yogurtearl opened this issue Jun 9, 2023 · 10 comments

Comments

@yogurtearl
Copy link

yogurtearl commented Jun 9, 2023

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
Copy link

wzsaz commented Sep 20, 2023

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
Copy link
Author

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

@Laxystem
Copy link

Laxystem commented Apr 3, 2024

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
Copy link
Contributor

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
Copy link
Author

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
Copy link
Author

yogurtearl commented May 24, 2024

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
Copy link

Laxystem commented May 28, 2024

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?

This was referenced Jun 6, 2024
@Sporking
Copy link

Sporking commented Jun 27, 2024

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.

@Laxystem
Copy link

@Sporking about security: in all Kotlin targets, the moment malicious code runs, you're fucked. Unless JetBrains is planning on improving security across the board in ways completely unprecedented for such a high-level language (besides Rust), I just don't see a reason for this.

Kotlin relies on sandboxing for security. Yes, for languages like Rust and C that are used to write the sandbox and the OS themselves you have a point - but not for a language like Kotlin that is made to be sandboxed - Android is completely sandboxed, so are Wasm and Js; The JVM as a whole is a walking security nightmare; and Kotlin/Native exists in name only.

Immutable lists serve a different point in Kotlin: thread-safety and iteration-safety.

@Laxystem
Copy link

Laxystem commented Jul 22, 2024

For example, when writing a game engine, I'm keeping vectors immutable and open; Yes, one can abuse that to render completely different things on my game's window - but that same malware can just create its own window, or encrypt your entire filesystem, because the JVM is insecure af anyway.

And this is without mentioning mixins that can just inject into your immutable list implementation at runtime anyway.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants