Skip to content

Commit fa7680b

Browse files
committed
Plug soundness hole for reach capabilities
To prevent that a reach capability is undermined by passing in a local capability from the outside, we disallow function arguments where reach capabilities appear in contravariant or invariant positions.
1 parent 43ed9fd commit fa7680b

File tree

10 files changed

+95
-6
lines changed

10 files changed

+95
-6
lines changed

compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,31 @@ class CheckCaptures extends Recheck, SymTransformer:
453453
case appType => appType
454454
end recheckApply
455455

456+
// Uses 4-space indent as a trial
457+
override def recheckArg(arg: Tree, pt: Type)(using Context) =
458+
val argType = recheck(arg, pt)
459+
460+
def checkOnlyCovariantReachCaps = new TypeTraverser:
461+
def traverse(tp: Type) =
462+
tp match
463+
case CapturingType(parent, refs) =>
464+
traverse(parent)
465+
if variance <= 0 then
466+
for ref <- refs.elems do
467+
if ref.isReach then
468+
val prefix = if variance == 0 then "in" else "contra"
469+
report.error(
470+
em"""Reach capability ${ctx.printer.toTextCaptureRef(ref).show} is not allowed to appear in ${prefix}variant position
471+
|in argument type ${argType.widen}""",
472+
arg.srcPos
473+
)
474+
case _ =>
475+
traverseChildren(tp)
476+
477+
checkOnlyCovariantReachCaps.traverse(argType.widen)
478+
argType
479+
end recheckArg
480+
456481
private def isDistinct(xs: List[Type]): Boolean = xs match
457482
case x :: xs1 => xs1.isEmpty || !xs1.contains(x) && isDistinct(xs1)
458483
case Nil => true

compiler/src/dotty/tools/dotc/transform/Recheck.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ abstract class Recheck extends Phase, SymTransformer:
311311
else fntpe.paramInfos
312312
def recheckArgs(args: List[Tree], formals: List[Type], prefs: List[ParamRef]): List[Type] = args match
313313
case arg :: args1 =>
314-
val argType = recheck(arg, normalizeByName(formals.head))
314+
val argType = recheckArg(arg, normalizeByName(formals.head))
315315
val formals1 =
316316
if fntpe.isParamDependent
317317
then formals.tail.map(_.substParam(prefs.head, argType))
@@ -326,6 +326,9 @@ abstract class Recheck extends Phase, SymTransformer:
326326
case tp =>
327327
assert(false, i"unexpected type of ${tree.fun}: $tp")
328328

329+
def recheckArg(arg: Tree, pt: Type)(using Context): Type =
330+
recheck(arg, normalizeByName(pt))
331+
329332
def recheckTypeApply(tree: TypeApply, pt: Type)(using Context): Type =
330333
val funtpe = recheck(tree.fun)
331334
tree.fun.rememberType(funtpe) // remember type to support later bounds checks

tests/neg-custom-args/captures/i15749a.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ def test =
1919
def forceWrapper[A](mx: Wrapper[Unit ->{cap} A]): Wrapper[A] =
2020
// Γ ⊢ mx: Wrapper[□ {cap} Unit => A]
2121
// `force` should be typed as ∀(□ {cap} Unit -> A) A, but it can not
22-
strictMap[Unit ->{mx*} A, A](mx)(t => force[A](t)) // error // should work
22+
strictMap[Unit ->{mx*} A, A](mx)(t => force[A](t)) // error // error

tests/neg-custom-args/captures/reaches.check

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,8 @@
4141
| Required: File^{id*}
4242
|
4343
| longer explanation available when compiling with `-explain`
44+
-- Error: tests/neg-custom-args/captures/reaches.scala:67:10 -----------------------------------------------------------
45+
67 | ps.map((x, y) => compose(x, y)) // error
46+
| ^^^^^^^^^^^^^^^^^^^^^^
47+
|Reach capability ps* is not allowed to appear in contravariant position
48+
|in argument type (x$1: (box (x$0: A^?) ->{ps*} A^?, box (x$0: A^?) ->{ps*} A^?)^?) ->? box (x$0: A^?) ->{ps*} A^?

tests/neg-custom-args/captures/reaches.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,11 @@ def attack2 =
5959
val leaked = usingFile[File^{id*}]: f =>
6060
val f1: File^{id*} = id(f) // error
6161
f1
62+
63+
def compose[A, B, C](f: A => B, g: B => C): A ->{f, g} C =
64+
z => g(f(z))
65+
66+
def mapCompose[A](ps: List[(A => A, A => A)]): List[A ->{ps*} A] =
67+
ps.map((x, y) => compose(x, y)) // error
68+
// unfortunately this does not work. x and y have type A ->{ps*} A, and that
69+
// type is not allowed to appear contra-variantly in the argument

tests/neg-custom-args/captures/refine-withFile.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ val useFile: [R] -> (path: String) -> (op: File^ -> R) -> R = ???
55
def main(): Unit =
66
val f: [R] -> (path: String) -> (op: File^ -> R) -> R = useFile
77
val g: [R] -> (path: String) -> (op: File^{f*} -> R) -> R = f // error
8-
val leaked = g[File^{f*}]("test")(f => f) // boom
8+
val leaked = g[File^{f*}]("test")(f => f) // error

tests/neg/unsound-reach-2.scala

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import language.experimental.captureChecking
2+
trait Consumer[-T]:
3+
def apply(x: T): Unit
4+
5+
trait File:
6+
def close(): Unit
7+
8+
def withFile[R](path: String)(op: Consumer[File]): R = ???
9+
10+
trait Foo[+X]:
11+
def use(x: File^)(op: Consumer[X]): Unit
12+
class Bar extends Foo[File^]:
13+
def use(x: File^)(op: Consumer[File^]): Unit = op.apply(x)
14+
15+
def bad(): Unit =
16+
val backdoor: Foo[File^] = new Bar
17+
val boom: Foo[File^{backdoor*}] = backdoor
18+
19+
var escaped: File^{backdoor*} = null
20+
withFile("hello.txt"): f =>
21+
boom.use(f):
22+
new Consumer[File^{backdoor*}]: // error
23+
def apply(f1: File^{backdoor*}) =
24+
escaped = f1
25+

tests/neg/unsound-reach.check

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-- Error: tests/neg/unsound-reach.scala:18:17 --------------------------------------------------------------------------
2+
18 | boom.use(f): (f1: File^{backdoor*}) => // error
3+
| ^
4+
| Reach capability backdoor* is not allowed to appear in contravariant position
5+
| in argument type (f1: box File^{backdoor*}) ->? Unit
6+
19 | escaped = f1

tests/neg/unsound-reach.scala

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import language.experimental.captureChecking
2+
trait File:
3+
def close(): Unit
4+
5+
def withFile[R](path: String)(op: File^ => R): R = ???
6+
7+
trait Foo[+X]:
8+
def use(x: File^)(op: X => Unit): Unit
9+
class Bar extends Foo[File^]:
10+
def use(x: File^)(op: File^ => Unit): Unit = op(x)
11+
12+
def bad(): Unit =
13+
val backdoor: Foo[File^] = new Bar
14+
val boom: Foo[File^{backdoor*}] = backdoor
15+
16+
var escaped: File^{backdoor*} = null
17+
withFile("hello.txt"): f =>
18+
boom.use(f): (f1: File^{backdoor*}) => // error
19+
escaped = f1
20+

tests/pos-custom-args/captures/reaches.scala

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,6 @@ def compose1[A, B, C](f: A => B, g: B => C): A ->{f, g} C =
3535
def compose2[A, B, C](f: A => B, g: B => C): A => C =
3636
z => g(f(z))
3737

38-
def mapCompose[A](ps: List[(A => A, A => A)]): List[A ->{ps*} A] =
39-
ps.map((x, y) => compose1(x, y))
40-
4138
@annotation.capability class IO
4239

4340
def test(io: IO) =

0 commit comments

Comments
 (0)