Skip to content

Commit b9aeef0

Browse files
authored
Restrict os.read within a Task to only allow reading from upstream task PathRefs (#5037)
Fixes #5023, the last bullet of #3746 we had punted on earlier With this PR: 1. A normal `Task` can only read from sub-paths of `PathRef`s returned by the upstream tasks it depends on. 2. Like the write restrictions implemented earlier, we do not restrict `Command`s (since those are expected to do arbitrary side-effecting things), 3. For reads we are also lenient for `InputImpl`s and their sub-types `SourceImpl`s and `SourcesImpl`s (since those are the roots of the build graph and so are expected to read from arbitrary files) To implement this, we need some way to traverse the return value of upstream tasks. This is done by instrumenting the `PathRef.jsonFormatter` that is called during the recursive serialization/deserialization of task return values during cache saves and loads. There is no other place for us to put this logic unless we add a second typeclass that task return values would have to implement, so this hackery piggy-backing on JSON serialization and de-serialization will have to do for now Added a single example test case to make sure this catches things, relying on the bulk of the existing test suite to ensure this doesn't break things. Also had to fix a bunch of existing violations of this rule, mostly by adjusting the code to use the proper pattern, but for some violations I just grandfathered them in for now. Definitely seems like a good guardrail to have given how often we screw it up ourselves in Mill's own repo For now this only limits reads during the _execution_ phase, and does not limit reads during the _resolution_ phase. That can come in a follow up
1 parent d2a3180 commit b9aeef0

File tree

33 files changed

+252
-149
lines changed

33 files changed

+252
-149
lines changed

contrib/scalapblib/test/src/mill/contrib/scalapblib/TutorialTests.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ object TutorialTests extends TestSuite {
5656
}
5757

5858
override def scalaPBSearchDeps = true
59-
override def scalaPBIncludePath = Seq(
60-
PathRef(moduleDir / "protobuf/tutorial")
59+
override def scalaPBIncludePath = Task.Sources(
60+
moduleDir / "protobuf/tutorial"
6161
)
6262
}
6363
lazy val millDiscover = Discover[this.type]

core/define/src/mill/define/PathRef.scala

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ object PathRef {
8282
private[mill] val validatedPaths: DynamicVariable[ValidatedPaths] =
8383
new DynamicVariable[ValidatedPaths](new ValidatedPaths())
8484

85+
private[mill] val serializedPaths: DynamicVariable[List[PathRef]] =
86+
new DynamicVariable(null)
87+
8588
class PathRefValidationException(val pathRef: PathRef)
8689
extends RuntimeException(s"Invalid path signature detected: ${pathRef}")
8790

@@ -169,11 +172,29 @@ object PathRef {
169172
new PathRef(path, quick, sig, revalidate)
170173
}
171174

175+
private[mill] def withSerializedPaths[T](block: => T): (T, Seq[PathRef]) = {
176+
serializedPaths.value = Nil
177+
try {
178+
val res = block
179+
(res, serializedPaths.value)
180+
} finally serializedPaths.value = null
181+
}
182+
private def storeSerializedPaths(pr: PathRef) = {
183+
if (serializedPaths.value != null) {
184+
serializedPaths.synchronized {
185+
serializedPaths.value = pr :: serializedPaths.value
186+
}
187+
}
188+
}
189+
172190
/**
173191
* Default JSON formatter for [[PathRef]].
174192
*/
175193
implicit def jsonFormatter: RW[PathRef] = upickle.default.readwriter[String].bimap[PathRef](
176-
p => p.toString(),
194+
p => {
195+
storeSerializedPaths(p)
196+
p.toString()
197+
},
177198
s => {
178199
val Array(prefix, valid0, hex, pathString) = s.split(":", 4)
179200

@@ -192,6 +213,7 @@ object PathRef {
192213
val sig = java.lang.Long.parseLong(hex, 16).toInt
193214
val pr = PathRef(path, quick, sig, revalidate = validOrig)
194215
validatedPaths.value.revalidateIfNeededOrThrow(pr)
216+
storeSerializedPaths(pr)
195217
pr
196218
}
197219
)

core/define/src/mill/define/internal/ResolveChecker.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package mill.define.internal
22

3-
object ResolveChecker extends os.Checker {
3+
class ResolveChecker(workspace: os.Path) extends os.Checker {
44
def onRead(path: os.ReadablePath): Unit = ()
55

66
def onWrite(path: os.Path): Unit = {
7-
sys.error(s"Writing to $path not allowed during resolution phase")
7+
sys.error(s"Writing to ${path.relativeTo(workspace)} not allowed during resolution phase")
88
}
99

1010
def withResolveChecker[T](f: () => T): T = {

core/eval/src/mill/eval/EvaluatorImpl.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ final class EvaluatorImpl private[mill] (
5252
allowPositionalCommandArgs: Boolean = false,
5353
resolveToModuleTasks: Boolean = false
5454
): mill.api.Result[List[Segments]] = {
55-
os.checker.withValue(ResolveChecker) {
55+
os.checker.withValue(ResolveChecker(workspace)) {
5656
Resolve.Segments.resolve(
5757
rootModule,
5858
scriptArgs,
@@ -73,7 +73,7 @@ final class EvaluatorImpl private[mill] (
7373
allowPositionalCommandArgs: Boolean = false,
7474
resolveToModuleTasks: Boolean = false
7575
): mill.api.Result[List[NamedTask[?]]] = {
76-
os.checker.withValue(ResolveChecker) {
76+
os.checker.withValue(ResolveChecker(workspace)) {
7777
Evaluator.currentEvaluator0.withValue(this) {
7878
Resolve.Tasks.resolve(
7979
rootModule,
@@ -91,7 +91,7 @@ final class EvaluatorImpl private[mill] (
9191
allowPositionalCommandArgs: Boolean = false,
9292
resolveToModuleTasks: Boolean = false
9393
): mill.api.Result[List[Either[Module, NamedTask[?]]]] = {
94-
os.checker.withValue(ResolveChecker) {
94+
os.checker.withValue(ResolveChecker(workspace)) {
9595
Evaluator.currentEvaluator0.withValue(this) {
9696
Resolve.Inspect.resolve(
9797
rootModule,
@@ -249,7 +249,7 @@ final class EvaluatorImpl private[mill] (
249249
selectMode: SelectMode,
250250
selectiveExecution: Boolean = false
251251
): mill.api.Result[Evaluator.Result[Any]] = {
252-
val resolved = os.checker.withValue(ResolveChecker) {
252+
val resolved = os.checker.withValue(ResolveChecker(workspace)) {
253253
Evaluator.currentEvaluator0.withValue(this) {
254254
Resolve.Tasks.resolve(
255255
rootModule,

core/exec/src/mill/exec/Execution.scala

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ private[mill] case class Execution(
166166
.toMap
167167

168168
futures(terminal) = Future.successful(
169-
Some(GroupExecution.Results(taskResults, group.toSeq, false, -1, -1, false))
169+
Some(GroupExecution.Results(taskResults, group.toSeq, false, -1, -1, false, Nil))
170170
)
171171
} else {
172172
futures(terminal) = Future.sequence(deps.map(futures)).map { upstreamValues =>
@@ -186,6 +186,11 @@ private[mill] case class Execution(
186186
.flatMap(_.iterator.flatMap(_.newResults))
187187
.toMap
188188

189+
val upstreamPathRefs = upstreamValues
190+
.iterator
191+
.flatMap(_.iterator.flatMap(_.serializedPaths))
192+
.toSeq
193+
189194
val startTime = System.nanoTime() / 1000
190195

191196
// should we log progress?
@@ -219,7 +224,8 @@ private[mill] case class Execution(
219224
allTransitiveClassMethods,
220225
forkExecutionContext,
221226
exclusive,
222-
offline
227+
offline,
228+
upstreamPathRefs
223229
)
224230

225231
// Count new failures - if there are upstream failures, tasks should be skipped, not failed

0 commit comments

Comments
 (0)