Skip to content

Commit 252e78a

Browse files
authored
Fix potential soundness hole when adding references to a mapped capture set (#18758)
Fix potential soundness hole when adding references to a set that is the image of an idempotent `tm` map `tm`. If the element `ref` did not come from the source of the set, we still assumed that `tm(ref) = ref`, so that we simply added the reference to the set and also back-propagated it to source. But that is not necessarily the case. (Although it is the case in our complete test suite, so I am not sure this case can actually arise in practice. Nevertheless, it's better to not leave a potential soundness hole here.) In the new implementation, we test whether `tm(ref) = ref`, and only proceed as before if that's the case. If not there are two sub-cases: - `{ref} <:< tm(ref)` and the variance of the set is positive. In that case we can soundly add `tm(ref)` to the set while back-propagating `ref` as before. - Otherwise there's nothing obvious left to do except fail (which is always sound).
2 parents 6792b64 + 7204fd7 commit 252e78a

File tree

6 files changed

+49
-78
lines changed

6 files changed

+49
-78
lines changed

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

Lines changed: 5 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ private val Captures: Key[CaptureSet] = Key()
1919

2020
object ccConfig:
2121

22-
/** Switch whether unpickled function types and byname types should be mapped to
23-
* impure types. With the new gradual typing using Fluid capture sets, this should
24-
* be no longer needed. Also, it has bad interactions with pickling tests.
22+
/** If true, allow mappping capture set variables under captureChecking with maps that are neither
23+
* bijective nor idempotent. We currently do now know how to do this correctly in all
24+
* cases, though.
2525
*/
26-
private[cc] val adaptUnpickledFunctionTypes = false
26+
inline val allowUnsoundMaps = false
2727

2828
/** If true, use `sealed` as encapsulation mechanism instead of the
2929
* previous global retriction that `cap` can't be boxed or unboxed.
@@ -48,7 +48,7 @@ def isCaptureCheckingOrSetup(using Context): Boolean =
4848
*/
4949
def depFun(args: List[Type], resultType: Type, isContextual: Boolean, paramNames: List[TermName] = Nil)(using Context): Type =
5050
val make = MethodType.companion(isContextual = isContextual)
51-
val mt =
51+
val mt =
5252
if paramNames.length == args.length then make(paramNames, args, resultType)
5353
else make(args, resultType)
5454
mt.toFunctionType(alwaysDependent = true)
@@ -106,22 +106,6 @@ extension (tree: Tree)
106106
case Apply(_, Typed(SeqLiteral(elems, _), _) :: Nil) => elems
107107
case _ => Nil
108108

109-
/** Under pureFunctions, add a @retainsByName(*)` annotation to the argument of
110-
* a by name parameter type, turning the latter into an impure by name parameter type.
111-
*/
112-
def adaptByNameArgUnderPureFuns(using Context): Tree =
113-
if ccConfig.adaptUnpickledFunctionTypes && Feature.pureFunsEnabledSomewhere then
114-
val rbn = defn.RetainsByNameAnnot
115-
Annotated(tree,
116-
New(rbn.typeRef).select(rbn.primaryConstructor).appliedTo(
117-
Typed(
118-
SeqLiteral(ref(defn.captureRoot) :: Nil, TypeTree(defn.AnyType)),
119-
TypeTree(defn.RepeatedParamType.appliedTo(defn.AnyType))
120-
)
121-
)
122-
)
123-
else tree
124-
125109
extension (tp: Type)
126110

127111
/** @pre `tp` is a CapturingType */
@@ -199,29 +183,6 @@ extension (tp: Type)
199183
case _ =>
200184
tp
201185

202-
/** Under pureFunctions, map regular function type to impure function type
203-
*/
204-
def adaptFunctionTypeUnderPureFuns(using Context): Type = tp match
205-
case AppliedType(fn, args)
206-
if ccConfig.adaptUnpickledFunctionTypes && Feature.pureFunsEnabledSomewhere && defn.isFunctionClass(fn.typeSymbol) =>
207-
val fname = fn.typeSymbol.name
208-
defn.FunctionType(
209-
fname.functionArity,
210-
isContextual = fname.isContextFunction,
211-
isImpure = true).appliedTo(args)
212-
case _ =>
213-
tp
214-
215-
/** Under pureFunctions, add a @retainsByName(*)` annotation to the argument of
216-
* a by name parameter type, turning the latter into an impure by name parameter type.
217-
*/
218-
def adaptByNameArgUnderPureFuns(using Context): Type =
219-
if ccConfig.adaptUnpickledFunctionTypes && Feature.pureFunsEnabledSomewhere then
220-
AnnotatedType(tp,
221-
CaptureAnnotation(CaptureSet.universal, boxed = false)(defn.RetainsByNameAnnot))
222-
else
223-
tp
224-
225186
/** Is type known to be always pure by its class structure,
226187
* so that adding a capture set to it would not make sense?
227188
*/

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

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import util.{SimpleIdentitySet, Property}
1616
import typer.ErrorReporting.Addenda
1717
import util.common.alwaysTrue
1818
import scala.collection.mutable
19-
import config.Config.ccAllowUnsoundMaps
2019

2120
/** A class for capture sets. Capture sets can be constants or variables.
2221
* Capture sets support inclusion constraints <:< where <:< is subcapturing.
@@ -667,7 +666,7 @@ object CaptureSet:
667666

668667
private def mapIsIdempotent = tm.isInstanceOf[IdempotentCaptRefMap]
669668

670-
assert(ccAllowUnsoundMaps || mapIsIdempotent, tm.getClass)
669+
assert(ccConfig.allowUnsoundMaps || mapIsIdempotent, tm.getClass)
671670

672671
private def whereCreated(using Context): String =
673672
if stack == null then ""
@@ -683,7 +682,9 @@ object CaptureSet:
683682
// `r` is _one_ possible solution in `source` that would make an `r` appear in this set.
684683
// It's not necessarily the only possible solution, so the scheme is incomplete.
685684
source.tryInclude(elem, this)
686-
else if !mapIsIdempotent && variance <= 0 && !origin.isConst && (origin ne initial) && (origin ne source) then
685+
else if ccConfig.allowUnsoundMaps && !mapIsIdempotent
686+
&& variance <= 0 && !origin.isConst && (origin ne initial) && (origin ne source)
687+
then
687688
// The map is neither a BiTypeMap nor an idempotent type map.
688689
// In that case there's no much we can do.
689690
// The scheme then does not propagate added elements back to source and rejects adding
@@ -697,8 +698,11 @@ object CaptureSet:
697698
def propagateIf(cond: Boolean): CompareResult =
698699
if cond then propagate else CompareResult.OK
699700

700-
if origin eq source then // elements have to be mapped
701-
val mapped = extrapolateCaptureRef(elem, tm, variance)
701+
val mapped = extrapolateCaptureRef(elem, tm, variance)
702+
def isFixpoint =
703+
mapped.isConst && mapped.elems.size == 1 && mapped.elems.contains(elem)
704+
705+
def addMapped =
702706
val added = mapped.elems.filter(!accountsFor(_))
703707
addNewElems(added)
704708
.andAlso:
@@ -707,6 +711,35 @@ object CaptureSet:
707711
else CompareResult.Fail(this :: Nil)
708712
.andAlso:
709713
propagateIf(!added.isEmpty)
714+
715+
def failNoFixpoint =
716+
val reason =
717+
if variance <= 0 then i"the set's variance is $variance"
718+
else i"$elem gets mapped to $mapped, which is not a supercapture."
719+
report.warning(em"""trying to add $elem from unrecognized source $origin of mapped set $this$whereCreated
720+
|The reference cannot be added since $reason""")
721+
CompareResult.Fail(this :: Nil)
722+
723+
if origin eq source then // elements have to be mapped
724+
addMapped
725+
.andAlso:
726+
if mapped.isConst then CompareResult.OK
727+
else if mapped.asVar.recordDepsState() then { addAsDependentTo(mapped); CompareResult.OK }
728+
else CompareResult.Fail(this :: Nil)
729+
else if !isFixpoint then
730+
// We did not yet observe the !isFixpoint condition in our tests, but it's a
731+
// theoretical possibility. In that case, it would be inconsistent to both
732+
// add `elem` to the set and back-propagate it. But if `{elem} <:< tm(elem)`
733+
// and the variance of the set is positive, we can soundly add `tm(ref)` to
734+
// the set while back-propagating `ref` as before. Otherwise there's nothing
735+
// obvious left to do except fail (which is always sound).
736+
if variance > 0
737+
&& elem.singletonCaptureSet.subCaptures(mapped, frozen = true).isOK then
738+
// widen to fixpoint. mapped is known to be a fixpoint since tm is idempotent.
739+
// The widening is sound, but loses completeness.
740+
addMapped
741+
else
742+
failNoFixpoint
710743
else if accountsFor(elem) then
711744
CompareResult.OK
712745
else

compiler/src/dotty/tools/dotc/config/Config.scala

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -235,15 +235,4 @@ object Config {
235235
*/
236236
inline val checkLevelsOnConstraints = false
237237
inline val checkLevelsOnInstantiation = true
238-
239-
/** If true, print capturing types in the form `{c} T`.
240-
* If false, print them in the form `T @retains(c)`.
241-
*/
242-
inline val printCaptureSetsAsPrefix = true
243-
244-
/** If true, allow mappping capture set variables under captureChecking with maps that are neither
245-
* bijective nor idempotent. We currently do now know how to do this correctly in all
246-
* cases, though.
247-
*/
248-
inline val ccAllowUnsoundMaps = false
249238
}

compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import ast.{Trees, tpd, untpd}
3232
import Trees._
3333
import Decorators._
3434
import transform.SymUtils._
35-
import cc.{adaptFunctionTypeUnderPureFuns, adaptByNameArgUnderPureFuns}
3635
import dotty.tools.dotc.quoted.QuotePatterns
3736

3837
import dotty.tools.tasty.{TastyBuffer, TastyReader}
@@ -383,7 +382,7 @@ class TreeUnpickler(reader: TastyReader,
383382
// Note that the lambda "rt => ..." is not equivalent to a wildcard closure!
384383
// Eta expansion of the latter puts readType() out of the expression.
385384
case APPLIEDtype =>
386-
postProcessFunction(readType().appliedTo(until(end)(readType())))
385+
readType().appliedTo(until(end)(readType()))
387386
case TYPEBOUNDS =>
388387
val lo = readType()
389388
if nothingButMods(end) then
@@ -460,8 +459,7 @@ class TreeUnpickler(reader: TastyReader,
460459
val ref = readAddr()
461460
typeAtAddr.getOrElseUpdate(ref, forkAt(ref).readType())
462461
case BYNAMEtype =>
463-
val arg = readType()
464-
ExprType(if withPureFuns then arg else arg.adaptByNameArgUnderPureFuns)
462+
ExprType(readType())
465463
case _ =>
466464
ConstantType(readConstant(tag))
467465
}
@@ -495,12 +493,6 @@ class TreeUnpickler(reader: TastyReader,
495493
def readTreeRef()(using Context): TermRef =
496494
readType().asInstanceOf[TermRef]
497495

498-
/** Under pureFunctions, map all function types to impure function types,
499-
* unless the unpickled class was also compiled with pureFunctions.
500-
*/
501-
private def postProcessFunction(tp: Type)(using Context): Type =
502-
if withPureFuns then tp else tp.adaptFunctionTypeUnderPureFuns
503-
504496
// ------ Reading definitions -----------------------------------------------------
505497

506498
private def nothingButMods(end: Addr): Boolean =
@@ -1240,8 +1232,7 @@ class TreeUnpickler(reader: TastyReader,
12401232
case SINGLETONtpt =>
12411233
SingletonTypeTree(readTree())
12421234
case BYNAMEtpt =>
1243-
val arg = readTpt()
1244-
ByNameTypeTree(if withPureFuns then arg else arg.adaptByNameArgUnderPureFuns)
1235+
ByNameTypeTree(readTpt())
12451236
case NAMEDARG =>
12461237
NamedArg(readName(), readTree())
12471238
case EXPLICITtpt =>
@@ -1453,7 +1444,7 @@ class TreeUnpickler(reader: TastyReader,
14531444
val args = until(end)(readTpt())
14541445
val tree = untpd.AppliedTypeTree(tycon, args)
14551446
val ownType = ctx.typeAssigner.processAppliedType(tree, tycon.tpe.safeAppliedTo(args.tpes))
1456-
tree.withType(postProcessFunction(ownType))
1447+
tree.withType(ownType)
14571448
case ANNOTATEDtpt =>
14581449
Annotated(readTpt(), readTree())
14591450
case LAMBDAtpt =>

compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import scala.collection.mutable
3333
import scala.collection.mutable.ListBuffer
3434
import scala.annotation.switch
3535
import reporting._
36-
import cc.{adaptFunctionTypeUnderPureFuns, adaptByNameArgUnderPureFuns}
3736

3837
object Scala2Unpickler {
3938

@@ -824,7 +823,7 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas
824823
}
825824
val tycon = select(pre, sym)
826825
val args = until(end, () => readTypeRef())
827-
if (sym == defn.ByNameParamClass2x) ExprType(args.head.adaptByNameArgUnderPureFuns)
826+
if (sym == defn.ByNameParamClass2x) ExprType(args.head)
828827
else if (ctx.settings.scalajs.value && args.length == 2 &&
829828
sym.owner == JSDefinitions.jsdefn.ScalaJSJSPackageClass && sym == JSDefinitions.jsdefn.PseudoUnionClass) {
830829
// Treat Scala.js pseudo-unions as real unions, this requires a
@@ -833,7 +832,6 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas
833832
}
834833
else if args.nonEmpty then
835834
tycon.safeAppliedTo(EtaExpandIfHK(sym.typeParams, args.map(translateTempPoly)))
836-
.adaptFunctionTypeUnderPureFuns
837835
else if (sym.typeParams.nonEmpty) tycon.EtaExpand(sym.typeParams)
838836
else tycon
839837
case TYPEBOUNDStpe =>

compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -640,8 +640,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
640640
try changePrec(GlobalPrec)(toText(arg) ~ "^" ~ toTextCaptureSet(captureSet))
641641
catch case ex: IllegalCaptureRef => toTextAnnot
642642
if annot.symbol.maybeOwner == defn.RetainsAnnot
643-
&& Feature.ccEnabled
644-
&& Config.printCaptureSetsAsPrefix && !printDebug
643+
&& Feature.ccEnabled && !printDebug
645644
&& Phases.checkCapturesPhase.exists // might be missing on -Ytest-pickler
646645
then toTextRetainsAnnot
647646
else toTextAnnot

0 commit comments

Comments
 (0)