Fix StackOverflowError when reporting self-referencing exceptions (#8508)

Add circular reference detection to StackTrace.trimmedLines using an
IdentityHashMap-backed Set, similar to how the JDK handles this in
Throwable.printStackTrace().

When a circular reference is detected, the method now appends a
[CIRCULAR REFERENCE: ...] message instead of recursing infinitely.

Fixes #7509
This commit is contained in:
MkDev11 2026-01-12 23:53:44 -05:00 committed by GitHub
parent f2a5ae7219
commit 02dcab80b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 69 additions and 1 deletions

View File

@ -10,6 +10,7 @@ package sbt.internal.util
import sbt.io.IO
import scala.collection.mutable.ListBuffer
import java.util.{ IdentityHashMap, Collections }
object StackTrace {
def isSbtClass(name: String) = name.startsWith("sbt.") || name.startsWith("xsbt.")
@ -30,6 +31,8 @@ object StackTrace {
def trimmedLines(t: Throwable, d: Int): List[String] = {
require(d >= 0)
val b = new ListBuffer[String]()
val seen: java.util.Set[Throwable] =
Collections.newSetFromMap(new IdentityHashMap[Throwable, java.lang.Boolean]())
def appendStackTrace(t: Throwable, first: Boolean): Unit = {
@ -58,11 +61,16 @@ object StackTrace {
}
appendStackTrace(t, true)
seen.add(t)
var c = t
while (c.getCause() != null) {
while (c.getCause() != null && !seen.contains(c.getCause())) {
c = c.getCause()
seen.add(c)
appendStackTrace(c, false)
}
if (c.getCause() != null && seen.contains(c.getCause())) {
b.append("[CIRCULAR REFERENCE: " + c.getCause().toString + "]")
}
b.toList
}

View File

@ -0,0 +1,60 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal.util
import org.scalatest.flatspec.AnyFlatSpec
class StackTraceSpec extends AnyFlatSpec {
"StackTrace.trimmedLines" should "handle normal exceptions" in {
val exception = new RuntimeException("test exception")
val lines = StackTrace.trimmedLines(exception, 3)
assert(lines.nonEmpty)
assert(lines.head.contains("test exception"))
}
it should "handle exceptions with causes" in {
val cause = new RuntimeException("cause exception")
val exception = new RuntimeException("test exception", cause)
val lines = StackTrace.trimmedLines(exception, 3)
assert(lines.exists(_.contains("test exception")))
assert(lines.exists(_.contains("Caused by:")))
assert(lines.exists(_.contains("cause exception")))
}
it should "handle self-referencing exceptions without StackOverflowError" in {
val exception = new SelfReferencingException("self-referencing exception")
val lines = StackTrace.trimmedLines(exception, 3)
assert(lines.nonEmpty)
assert(lines.head.contains("self-referencing exception"))
assert(lines.exists(_.contains("[CIRCULAR REFERENCE:")))
}
it should "handle circular exception chains without StackOverflowError" in {
val exception1 = new ChainableException("exception 1")
val exception2 = new ChainableException("exception 2")
exception1.setCauseException(exception2)
exception2.setCauseException(exception1)
val lines = StackTrace.trimmedLines(exception1, 3)
assert(lines.nonEmpty)
assert(lines.exists(_.contains("exception 1")))
assert(lines.exists(_.contains("exception 2")))
assert(lines.exists(_.contains("[CIRCULAR REFERENCE:")))
}
}
class SelfReferencingException(message: String) extends RuntimeException(message) {
override def getCause: Throwable = this
}
class ChainableException(message: String) extends RuntimeException(message) {
import scala.compiletime.uninitialized
private var causeException: Throwable = uninitialized
def setCauseException(cause: Throwable): Unit = causeException = cause
override def getCause: Throwable = causeException
}