question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Unsound application of boxed functions

See original GitHub issue

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)
}

Issue Analytics

  • State:closed
  • Created a year ago
  • Reactions:1
  • Comments:6 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
oderskycommented, Jul 27, 2022

@Linyxus Yes, I think that makes sense.

1reaction
oderskycommented, Jul 26, 2022

There’s something wrong with the isBoxedCapturing predicate. What happens is that the new selection rule for v.x kicks in which says that since v has a pure type and the type of x as a member of v is unboxed, the result is also pure. The second part is wrong; the type of x should be boxed.

Before it was not a problem since we did not apply this reasoning to selections, so the type of v.x was simply {*} Unit -> Int.

Read more comments on GitHub >

github_iconTop Results From Across the Web

'static closures with non-'static return type are unsound #84366
OK, I'm thinking about this issue. So, in a top-level function, it is considered "ok" for a lifetime to appear in the return...
Read more >
Why do these behave so differently? Is this unsound? - help - The ...
My take is that when you're asking the compiler to cast from type ?Unspecified to type Box<Predicate> ; it first infers it's own...
Read more >
Java and Scala's type systems are unsound (2016)
Using casts between types with different generics can cause "heap corruption", but the compiler will give warnings on them (except if explicitly ...
Read more >
[Agda] Postulated computing quotients are unsound - Chalmers
Carrier A) → Quotient A To avoid direct pattern matching we didn't export the constructor, but only a function [_] = box.
Read more >
What's the most unsound program you've had to maintain?
I once had to maintain a legacy C application which had previously been written and maintained by some programmers who had lost the...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found