Skip to content

Unsound application of boxed functions #15749

@Linyxus

Description

@Linyxus

Compiler version

3.1.3-RC4

Minimized code

class Unit
object unit extends Unit

type Top = {*} Any

type LazyVal[T] = {*} Unit -> T

case class Foo[T](x: T)

// Foo[□ {*} Unit -> T]
type BoxedLazyVal[T] = Foo[LazyVal[T]]

def force[A](v: BoxedLazyVal[A]): A =
  // Γ ⊢ v.x : □ {*} Unit -> A
  v.x(unit)  // (unbox v.x)(unit), where (unbox v.x) should be untypable

Output

The code compiles but it shouldn't.

[[syntax trees at end of                        cc]] // issues/unbox-minimised.scala
package <empty> {
  @CaptureChecked @SourceFile("issues/unbox-minimised.scala") class Unit() extends Object() {}
  final lazy module val unit: unit = new unit()
  @CaptureChecked @SourceFile("issues/unbox-minimised.scala") final module class unit() extends Unit() {
    private[this] type $this = unit.type
    private def writeReplace(): AnyRef = new scala.runtime.ModuleSerializationProxy(classOf[unit.type])
  }
  @CaptureChecked @SourceFile("issues/unbox-minimised.scala") case class Foo[T](x: T) extends Object(), Product, Serializable {
    override def hashCode(): Int = scala.runtime.ScalaRunTime._hashCode(this)
    override def equals(x$0: Any): Boolean = 
      this.eq(x$0.$asInstanceOf[Object]).||(
        matchResult1[Boolean]: 
          {
            case val x1: (x$0 : Any) = x$0
            if x1.$isInstanceOf[Foo[T] @unchecked] then 
              {
                case val x$0: Foo[T] = x1.$asInstanceOf[Foo[T] @unchecked]
                return[matchResult1] this.x.==(x$0.x).&&(x$0.canEqual(this))
              }
             else ()
            return[matchResult1] false
          }
      )
    override def toString(): String = scala.runtime.ScalaRunTime._toString(this)
    override def canEqual(that: Any): Boolean = that.isInstanceOf[Foo[T] @unchecked]
    override def productArity: Int = 1
    override def productPrefix: String = "Foo"
    override def productElement(n: Int): Any = 
      matchResult2[T]: 
        {
          case val x3: (n : Int) = n
          if 0.==(x3) then return[matchResult2] this._1 else ()
          throw new IndexOutOfBoundsException(n.toString())
        }
    override def productElementName(n: Int): String = 
      matchResult3[("x" : String)]: 
        {
          case val x4: (n : Int) = n
          if 0.==(x4) then return[matchResult3] "x" else ()
          throw new IndexOutOfBoundsException(n.toString())
        }
    T
    val x: T
    def copy[T](x: T): Foo[T] = new Foo[T](x)
    def copy$default$1[T]: T = Foo.this.x
    def _1: T = this.x
  }
  final lazy module val Foo: Foo = new Foo()
  @CaptureChecked @SourceFile("issues/unbox-minimised.scala") final module class Foo() extends AnyRef(), scala.deriving.Mirror.Product {
    private[this] type $this = Foo.type
    private def writeReplace(): AnyRef = new scala.runtime.ModuleSerializationProxy(classOf[Foo.type])
    def apply[T](x: T): Foo[T] = new Foo[T](x)
    def unapply[T](x$1: Foo[T]): Foo[T] = x$1
    override def toString: String = "Foo"
    type MirroredMonoType = Foo[? <: AnyKind]
    def fromProduct(x$0: Product): Foo.MirroredMonoType = new Foo[Any](x$0.productElement(0))
  }
  final lazy module val unbox-minimised$package: unbox-minimised$package = new unbox-minimised$package()
  @CaptureChecked @SourceFile("issues/unbox-minimised.scala") final module class unbox-minimised$package() extends Object() {
    private[this] type $this = unbox-minimised$package.type
    private def writeReplace(): AnyRef = new scala.runtime.ModuleSerializationProxy(classOf[unbox-minimised$package.type])
    type Top = {*} Any
    type LazyVal = [T] =>> {*} Unit -> T
    type BoxedLazyVal = [T] =>> Foo[{*} Unit -> T]
    def force[A](v: BoxedLazyVal[A]): A = v.x.apply(unit)
    def main(): Unit = 
      {
        abstract class Cap() extends Object() {
          def close(): Unit = unit
        }
        def withCap[T](op: ({*} Cap) => T): T = 
          {
            val cap: ? Cap = 
              {
                final class $anon() extends Cap() {}
                new Cap {...}():(? Cap)
              }
            val result: ? T = op.apply(cap)
            cap.close()
            result:({result} T)
          }
        def leaked: {} BoxedLazyVal[Cap] = 
          withCap[? Foo[{*} (x$0: ? Unit) -> ? Cap]](
            {
              {
                def $anonfun(cap: {*} Cap): ? Foo[{cap, *} (x$0: ? Unit) -> ? Cap] = 
                  {
                    val bad: {cap} (x$0: {} Unit) -> {cap} Cap = 
                      {
                        def $anonfun(_$1: Unit): {cap} Cap = cap
                        closure($anonfun)
                      }
                    Foo.apply[{bad, cap, *} (x$0: ? Unit) -> {cap} Cap](bad)
                  }
                closure($anonfun)
              }
            }
          )
        val leakedCap: {} Cap = force[? Cap](leaked)
        ()
      }
  }
}

-- Warning: issues/unbox-minimised.scala:1:6 -------------------------------------------------------------------------------------------------------------------------------------
1 |class Unit
  |      ^
  |      class Unit differs only in case from object unit. Such classes will overwrite one another on case-insensitive filesystems.
1 warning found
[success] Total time: 2 s, completed Jul 26, 2022, 4:25:25 PM

Expectation

This code is minimised from Ondrej's list encoding example (in #15731). In force, the function v.x is a boxed function of type □ {*} Unit -> A, so we should not allow the application v.x(unit) since we can not unbox it.

This leads to the leaking of scoped capabilities, for example:

def main() = {
  abstract class Cap { def close(): Unit = unit }
  def withCap[T](op: ({*} Cap) => T): T = {
    val cap = new Cap {}
    val result = op(cap)
    cap.close()
    result
  }

  def leaked: {} BoxedLazyVal[Cap] = withCap { cap =>
    val bad = (_: Unit) => cap
    Foo(bad)
  }

  val leakedCap: {} Cap = force(leaked)
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    cc-experimentIntended to be merged with cc-experiment branch on originitype:bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions