Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reimplement PipelineMain's sequencing to avoid races #8815

Merged
merged 1 commit into from Apr 8, 2020

Conversation

dwijnand
Copy link
Member

@dwijnand dwijnand commented Mar 19, 2020

@scala-jenkins scala-jenkins added this to the 2.13.3 milestone Mar 19, 2020
@SethTisue SethTisue requested a review from retronym March 19, 2020 18:42
@dwijnand

This comment has been minimized.

@dwijnand dwijnand force-pushed the handle-toctou-in-PipelineMain branch from 89d8558 to bd74039 Compare March 19, 2020 21:20
@retronym
Copy link
Member

I think the underlying problem is elsewhere. Maybe PipelineMain are ignoring an compile error in an trying to compiler a downstream project anyway? Or, similarly, ignoring an error in outline compilation and trying to do the full compilation anyway.

Reviewing the code, I think the root cause might be that def outlineCompile checks this.reporter.hasErrors rather than compiler.reporter.hasErrors. I saw a few other suspect looking things in PipelineMain and ended up with this diff:

diff --git a/src/compiler/scala/tools/nsc/PipelineMain.scala b/src/compiler/scala/tools/nsc/PipelineMain.scala
index ee97797447..e83a756032 100644
--- a/src/compiler/scala/tools/nsc/PipelineMain.scala
+++ b/src/compiler/scala/tools/nsc/PipelineMain.scala
@@ -154,9 +154,11 @@ class PipelineMainClass(argFiles: Seq[Path], pipelineSettings: PipelineMain.Pipe
       val counter = new AtomicInteger(count)
       val failed = new AtomicBoolean(false)
       val handler = (a: Try[_]) => a match {
-        case f @ Failure(_) =>
+        case f @ Failure(t) =>
           if (failed.compareAndSet(false, true)) {
-            done.complete(f)
+            val allFailures: immutable.Seq[Throwable] = allFutures.flatMap(_.value).flatMap(_.failed.toOption)
+            allFailures.foreach(_.printStackTrace())
+            done.complete(Failure(t))
           }
         case Success(_) =>
           val remaining = counter.decrementAndGet()
@@ -431,6 +433,7 @@ class PipelineMainClass(argFiles: Seq[Path], pipelineSettings: PipelineMain.Pipe
         reporter.flush()
       else if (command.shouldStopWithInfo)
         reporter.echo(command.getInfoMessage(result))
+      result.reporter = createReporter(result.settings)
       result
     } catch {
       case t: Throwable =>
@@ -450,8 +453,8 @@ class PipelineMainClass(argFiles: Seq[Path], pipelineSettings: PipelineMain.Pipe
         run1 compile files
         outlineTimer.stop()
         log(f"scalac outline: done ${outlineTimer.durationMs}%.0f ms")
-        reporter.finish()
-        if (reporter.hasErrors) {
+        compiler.reporter.finish()
+        if (compiler.reporter.hasErrors) {
           log("scalac outline: failed")
           outlineDone.complete(Failure(new RuntimeException(label + ": compile failed: ")))
         } else {
@@ -460,8 +463,7 @@ class PipelineMainClass(argFiles: Seq[Path], pipelineSettings: PipelineMain.Pipe
         }
       } catch {
         case t: Throwable =>
-          t.printStackTrace()
-          outlineDone.complete(Failure(new RuntimeException(label + ": compile failed: ")))
+          outlineDone.complete(Failure(new RuntimeException(label + ": compile failed: ", t)))
       }
     }
 
@@ -488,9 +490,7 @@ class PipelineMainClass(argFiles: Seq[Path], pipelineSettings: PipelineMain.Pipe
                 log(f"scalac (${ix + 1}/$groupCount): done ${group.timer.durationMs}%.0f ms")
               }
               if (compiler2.reporter.hasErrors) {
-                group.done.complete(Failure(new RuntimeException(label + ": compile failed: ")))
-              } else {
-                group.done.complete(Success(()))
+                throw new RuntimeException(label + ": compile failed: ")
               }
             } finally {
               compiler2.close()
@@ -575,7 +575,7 @@ class PipelineMainClass(argFiles: Seq[Path], pipelineSettings: PipelineMain.Pipe
             log(f"javac: done ${javaTimer.durationMs}%.0f ms ")
           } else {
             javaTimer.stop()
-            log(f"javac: error ${javaTimer.durationMs}%.0f ms ")
+            throw new RuntimeException(f"javac: error ${javaTimer.durationMs}%.0f ms ")
           }
           ()
         })

@dwijnand
Copy link
Member Author

I think the underlying problem is elsewhere. Maybe PipelineMain are ignoring an compile error in an trying to compiler a downstream project anyway? Or, similarly, ignoring an error in outline compilation and trying to do the full compilation anyway.

Why do you think it's elsewhere? It's a thrown NoSuchFileException after Files.exist check, which shouts "race condition" to me.

But I'll throw your diff in too, then.

@eed3si9n
Copy link
Member

eed3si9n commented Mar 20, 2020

def awaitAll(fs: Seq[Future[_]]): Future[_] = {
val done = Promise[Any]()
val allFutures = projects.flatMap(_.futures)
val count = allFutures.size
val counter = new AtomicInteger(count)
val failed = new AtomicBoolean(false)
val handler = (a: Try[_]) => a match {
case f @ Failure(_) =>
if (failed.compareAndSet(false, true)) {
done.complete(f)
}
case Success(_) =>
val remaining = counter.decrementAndGet()
if (remaining == 0) done.success(())
}
allFutures.foreach(_.onComplete(handler))
done.future
}

In the above, awaitAll completes done in case any of the allFutures fail, which means if you have two subprojects one that fails and one that succeeds, the composed future would complete before the successful one is done. Scripted test in my Zinc PR uses Future.sequence, but I ran into similar issue, and in my case it I needed to add a Future that waits for all regular JARs - sbt/zinc@e54c4dd

@dwijnand
Copy link
Member Author

which means if you have two subprojects one that fails and one that succeeds, the composed future would complete the successful one is done.

You're missing a word there somewhere, so I don't understand what you're saying. Isn't the counter setting things up right?

@eed3si9n
Copy link
Member

@dwijnand it's missing "before" -- the composed future would complete before the successful one is done. In case of a failure in one subproject, done.complete(f) gets called while some compilations might be still going on.

@dwijnand
Copy link
Member Author

Right. What's wrong with that?

@eed3si9n
Copy link
Member

When the test exits I am guessing that /tmp/scala.picklecache7643988611803649817/tmp/pipelineBase3274412707466812224/ would get deleted and you're seeing java.nio.file.NoSuchFileException: /tmp/scala.picklecache7643988611803649817/tmp/pipelineBase3274412707466812224/projects/m1/p1/target.jar when the successful subproject completes its compilation.

@retronym
Copy link
Member

Maybe the correct building block we're after is:

    def sequenceFailSlow[A](fs: Seq[Future[A]]): Future[Seq[A]] = {
      val futures: Seq[Future[Either[Throwable, A]]] = fs.map(_.transform(tr => Success(tr.toEither)))
      Future.sequence(futures).map {
        results =>
          val successes = results.collect { case Right(value) => value }
          val failures = results.collect { case Left(throwable) => throwable }.toList
          failures match {
            case head :: rest => rest.foreach(head.addSuppressed(_)); throw head
            case Nil =>
              successes
          }
      }
    }

    def awaitDone(): Unit = {
      val allFutures: immutable.Seq[Future[_]] = projects.flatMap(_.futures)
      val numAllFutures = allFutures.size
      val awaitAllFutures: Future[_] = sequenceFailSlow[Any](allFutures)

@retronym
Copy link
Member

retronym commented Mar 21, 2020

It's a thrown NoSuchFileException after Files.exist check, which shouts "race condition" to me.

Echoing @eed3si9n's analysis: yes, this indicates a race. But if we remove the symptom of the NoSuchFileException, the test will fail anyway when the downstream project is compiled with the file absent from its classpath.

And some other misc fixes & cleanups.
@dwijnand dwijnand changed the title Handle a TOCTOU race in PipelineMain Reimplement PipelineMain's sequencing to avoid races Mar 28, 2020
@dwijnand dwijnand force-pushed the handle-toctou-in-PipelineMain branch from af430d4 to eb8c64c Compare March 28, 2020 08:21
@retronym retronym merged commit da15c5a into scala:2.13.x Apr 8, 2020
@SethTisue SethTisue modified the milestones: 2.13.3, 2.13.2 Apr 8, 2020
@dwijnand dwijnand deleted the handle-toctou-in-PipelineMain branch April 8, 2020 06:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants