Skip to content

Commit cc55381

Browse files
authored
Plug soundness hole for reach capabilities (#20051)
~~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.~~ New scheme: Enforce an analogous restriction to the one for creating reach capabilities for all values. The type of a value cannot both have a reach capability with variance >= 0 and at the same time a universal capability with variance <= 0.
2 parents c8c3bde + f2b7c12 commit cc55381

File tree

5 files changed

+113
-2
lines changed

5 files changed

+113
-2
lines changed

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

+42-2
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,44 @@ class CheckCaptures extends Recheck, SymTransformer:
249249
else i"references $cs1$cs1description are not all",
250250
pos, provenance)
251251

252+
def showRef(ref: CaptureRef)(using Context): String =
253+
ctx.printer.toTextCaptureRef(ref).show
254+
255+
// Uses 4-space indent as a trial
256+
def checkReachCapsIsolated(tpe: Type, pos: SrcPos)(using Context): Unit =
257+
258+
object checker extends TypeTraverser:
259+
var refVariances: Map[Boolean, Int] = Map.empty
260+
var seenReach: CaptureRef | Null = null
261+
def traverse(tp: Type) =
262+
tp match
263+
case CapturingType(parent, refs) =>
264+
traverse(parent)
265+
for ref <- refs.elems do
266+
if ref.isReach && !ref.stripReach.isInstanceOf[TermParamRef]
267+
|| ref.isRootCapability
268+
then
269+
val isReach = ref.isReach
270+
def register() =
271+
refVariances = refVariances.updated(isReach, variance)
272+
seenReach = ref
273+
refVariances.get(isReach) match
274+
case None => register()
275+
case Some(v) => if v != 0 && variance == 0 then register()
276+
case _ =>
277+
traverseChildren(tp)
278+
279+
checker.traverse(tpe)
280+
if checker.refVariances.size == 2
281+
&& checker.refVariances(true) >= 0
282+
&& checker.refVariances(false) <= 0
283+
then
284+
report.error(
285+
em"""Reach capability ${showRef(checker.seenReach.nn)} and universal capability cap cannot both
286+
|appear in the type $tpe of this expression""",
287+
pos)
288+
end checkReachCapsIsolated
289+
252290
/** The current environment */
253291
private val rootEnv: Env = inContext(ictx):
254292
Env(defn.RootClass, EnvKind.Regular, CaptureSet.empty, null)
@@ -779,8 +817,10 @@ class CheckCaptures extends Recheck, SymTransformer:
779817
report.error(ex.getMessage.nn)
780818
tree.tpe
781819
finally curEnv = saved
782-
if tree.isTerm && !pt.isBoxedCapturing then
783-
markFree(res.boxedCaptureSet, tree.srcPos)
820+
if tree.isTerm then
821+
checkReachCapsIsolated(res.widen, tree.srcPos)
822+
if !pt.isBoxedCapturing then
823+
markFree(res.boxedCaptureSet, tree.srcPos)
784824
res
785825

786826
override def recheckFinish(tpe: Type, tree: Tree, pt: Type)(using Context): Type =

tests/neg/unsound-reach-2.scala

+25
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): // error
22+
new Consumer[File^{backdoor*}]:
23+
def apply(f1: File^{backdoor*}) =
24+
escaped = f1
25+

tests/neg/unsound-reach-3.scala

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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^): X
9+
class Bar extends Foo[File^]:
10+
def use(x: File^): File^ = 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+
escaped = boom.use(f) // error
19+
// boom.use: (x: File^) -> File^{backdoor*}, it is a selection so reach capabilities are allowed
20+
// f: File^, so there is no reach capabilities
21+

tests/neg/unsound-reach.check

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- Error: tests/neg/unsound-reach.scala:18:9 ---------------------------------------------------------------------------
2+
18 | boom.use(f): (f1: File^{backdoor*}) => // error
3+
| ^^^^^^^^
4+
| Reach capability backdoor* and universal capability cap cannot both
5+
| appear in the type (x: File^)(op: box File^{backdoor*} => Unit): Unit of this expression

tests/neg/unsound-reach.scala

+20
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+

0 commit comments

Comments
 (0)