Unsound application of boxed functions
See original GitHub issueCompiler 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:
- Created a year ago
- Reactions:1
- Comments:6 (3 by maintainers)
Top 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 >Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start FreeTop Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
Top GitHub Comments
@Linyxus Yes, I think that makes sense.
There’s something wrong with the
isBoxedCapturing
predicate. What happens is that the new selection rule forv.x
kicks in which says that sincev
has a pure type and the type ofx
as a member ofv
is unboxed, the result is also pure. The second part is wrong; the type ofx
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
.