Skip to content

Unsoundness from not-yet-initialized paths — no null-involved #5854

Closed
@Blaisorblade

Description

@Blaisorblade

Here's an unsoundness hole I wasn't aware about, and I can't find in the issue tracker — it involves initialization, but not null. Instead of using null, we can just select an ill-bounded type from a not-yet-initialized path that's going to crash/loop when we get there — so we get a ClassCastException instead of an abort (exception) or loop.

scala> new {
     |   val a: String = (((1: Any): b.A): Nothing): String
     |   val b: { type A >: Any <: Nothing } = ???
     | }
java.lang.ClassCastException: java.lang.Integer cannot be cast to scala.runtime.Nothing$
	at rs$line$1$$anon$1.<init>(rs$line$1:2)
scala> new {
     |   val a: String = (((1: Any): b.A): Nothing): String
     |   def loop(): Nothing = loop()
     |   val b: { type A >: Any <: Nothing } = loop()
     | }
     |
java.lang.ClassCastException: java.lang.Integer cannot be cast to scala.runtime.Nothing$
	at rs$line$4$$anon$1.<init>(rs$line$4:2)
	at rs$line$4$.<init>(rs$line$4:5)

One can verify that commenting out val a gives code that involves no nulls.

Discussion

We've known for a while that we can inhabit types with bad bounds with expressions that don't return a value — val v { type A >: Any <: Nothing } = failing/looping. But we always reasoned that v.A would only be available after the crash. The above examples show a loophole we missed.

This loophole is outside the taxonomy of "scaling-up" loopholes in https://www.scala-lang.org/blog/2016/02/17/scaling-dot-soundness.html.

And it doesn't appear in any known DOT variant that I'm familiar with, because it requires both paths and potentially diverging initialization expressions. If this issue is indeed novel to us, I'll ask other DOT researchers.

Above, the type of b has obvious bad bounds, but I assume we can give it non-obvious bad bounds (as usual) to get the same error.

I can see two likely sound potential fixes, but they are not yet realistic.

  1. checking strong normalization for value members. Likely sound, pretty restrictive. This could "work" if we required programmers to use some escape hatch (@total) when we can't check totality ourselves — the main benefit would be that @total documents the danger.
  2. forbidding paths starting in this.f before f is initialized, following @liufengyun et al.'s safe initialization work. I'd expect this to be pretty restrictive, but I don't know.
  3. We could also declare these errors out-of-scope.

Regarding 2: In fact, I realized this problem by comparing DOT's and @liufengyun's typing rules for object creation: one declares all fields to be instantly initialized and accessible in the typing context when typing other fields (which is valid if initializers are syntactic values), the other only has in context the actually initialized fields.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions