This is an automated email from the ASF dual-hosted git repository.
pjfanning pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko.git
The following commit(s) were added to refs/heads/main by this push:
new 42a4596780 fix: guard against int overflow in supervisor strategy and
frequency sketch (#3022)
42a4596780 is described below
commit 42a4596780cba7bc0b1fe88c2c99bc8cdbc47af0
Author: PJ Fanning <[email protected]>
AuthorDate: Sun May 31 20:47:27 2026 +0100
fix: guard against int overflow in supervisor strategy and frequency sketch
(#3022)
* fix: guard against int overflow in supervisor strategy and frequency
sketch
* fix: address PR #3022 review comments - width overflow in FrequencySketch
and regression tests for large withinTimeRange (#54)
* scalafmt
---------
Co-authored-by: copilot-swe-agent[bot]
<[email protected]>
---
.../org/apache/pekko/actor/SupervisorSpec.scala | 26 ++++++++++++++
.../apache/pekko/util/FrequencySketchSpec.scala | 42 ++++++++++++++++++++++
.../org/apache/pekko/actor/FaultHandling.scala | 6 ++--
.../org/apache/pekko/util/FrequencySketch.scala | 14 +++++---
4 files changed, 82 insertions(+), 6 deletions(-)
diff --git
a/actor-tests/src/test/scala/org/apache/pekko/actor/SupervisorSpec.scala
b/actor-tests/src/test/scala/org/apache/pekko/actor/SupervisorSpec.scala
index e37126bed7..f75b460433 100644
--- a/actor-tests/src/test/scala/org/apache/pekko/actor/SupervisorSpec.scala
+++ b/actor-tests/src/test/scala/org/apache/pekko/actor/SupervisorSpec.scala
@@ -613,4 +613,30 @@ class SupervisorSpec
killExpectNoRestart(pingpong)
}
+ "respects maxNrOfRetries with withinTimeRange whose toMillis exceeds
Int.MaxValue (OneForOneStrategy)" in {
+ // 30 days = 2,592,000,000 ms which silently overflowed to a negative Int
before the fix,
+ // causing retriesInWindowOkay to always return true (unlimited restarts)
+ val supervisor = system.actorOf(Props(
+ new Supervisor(OneForOneStrategy(maxNrOfRetries = 2, withinTimeRange =
30.days)(classOf[Exception] :: Nil))))
+
+ val pingpong = child(supervisor, Props(new PingPongActor(testActor)))
+
+ kill(pingpong)
+ kill(pingpong)
+ killExpectNoRestart(pingpong)
+ }
+
+ "respects maxNrOfRetries with withinTimeRange whose toMillis exceeds
Int.MaxValue (AllForOneStrategy)" in {
+ // 30 days = 2,592,000,000 ms which silently overflowed to a negative Int
before the fix,
+ // causing retriesInWindowOkay to always return true (unlimited restarts)
+ val supervisor = system.actorOf(Props(
+ new Supervisor(AllForOneStrategy(maxNrOfRetries = 2, withinTimeRange =
30.days)(classOf[Exception] :: Nil))))
+
+ val pingpong = child(supervisor, Props(new PingPongActor(testActor)))
+
+ kill(pingpong)
+ kill(pingpong)
+ killExpectNoRestart(pingpong)
+ }
+
}
diff --git
a/actor-tests/src/test/scala/org/apache/pekko/util/FrequencySketchSpec.scala
b/actor-tests/src/test/scala/org/apache/pekko/util/FrequencySketchSpec.scala
index 1e6b112884..fa036aa85a 100644
--- a/actor-tests/src/test/scala/org/apache/pekko/util/FrequencySketchSpec.scala
+++ b/actor-tests/src/test/scala/org/apache/pekko/util/FrequencySketchSpec.scala
@@ -264,6 +264,28 @@ class FrequencySketchSpec extends AnyWordSpec with
Matchers {
sketch.frequency("foo") shouldBe 2
sketch.frequency("bar") shouldBe 2
}
+
+ "reject capacity that would cause width to overflow Int" in {
+ // capacity > 536,870,912 with default widthMultiplier=4:
ceilingPowerOfTwo gives 2^30=1073741824, times 4 overflows
+ an[IllegalArgumentException] should be thrownBy
FrequencySketch[String](capacity = 536_870_913)
+ }
+
+ "detect width overflow that would have been missed by Int arithmetic" in {
+ // before the fix, widthMultiplier * ceilingPowerOfTwo(capacity) was
computed in Int and could silently overflow
+ val capacity = 536_870_913
+ val widthMultiplier = 4
+ // the old Int multiplication silently overflows to a non-power-of-two
or zero/negative value
+ val widthInt = widthMultiplier *
FrequencySketch.Bits.ceilingPowerOfTwo(capacity)
+ widthInt should be <= 0 // negative or zero due to Int overflow (old
broken behavior)
+ // the new Long multiplication correctly identifies the overflow
+ val widthLong = widthMultiplier.toLong *
FrequencySketch.Bits.ceilingPowerOfTwo(capacity).toLong
+ widthLong should be > Int.MaxValue.toLong
+ }
+
+ "reject capacity that would cause width to overflow Int with custom
widthMultiplier" in {
+ an[IllegalArgumentException] should be thrownBy
+ FrequencySketch[String](capacity = 100, widthMultiplier = Int.MaxValue)
+ }
}
"FastFrequencySketch" must {
@@ -370,5 +392,25 @@ class FrequencySketchSpec extends AnyWordSpec with
Matchers {
val accuracy = correct.toDouble / comparisons
accuracy should be > 0.95 // note: depends on the hash collisions, and
random distribution
}
+
+ "reject capacity that would cause width to overflow Int" in {
+ // capacity > 536,870,912 with default widthMultiplier=4:
ceilingPowerOfTwo gives 2^30=1073741824, times 4 overflows
+ an[IllegalArgumentException] should be thrownBy
FastFrequencySketch[String](capacity = 536_870_913)
+ }
+
+ "detect width overflow that would have been missed by Int arithmetic" in {
+ // before the fix, widthMultiplier * ceilingPowerOfTwo(capacity) was
computed in Int and could silently overflow
+ val capacity = 536_870_913
+ val widthMultiplier = 4
+ val widthInt = widthMultiplier *
FrequencySketch.Bits.ceilingPowerOfTwo(capacity)
+ widthInt should be <= 0 // negative or zero due to Int overflow (old
broken behavior)
+ val widthLong = widthMultiplier.toLong *
FrequencySketch.Bits.ceilingPowerOfTwo(capacity).toLong
+ widthLong should be > Int.MaxValue.toLong
+ }
+
+ "reject capacity that would cause width to overflow Int with custom
widthMultiplier" in {
+ an[IllegalArgumentException] should be thrownBy
+ FastFrequencySketch[String](capacity = 100, widthMultiplier =
Int.MaxValue)
+ }
}
}
diff --git a/actor/src/main/scala/org/apache/pekko/actor/FaultHandling.scala
b/actor/src/main/scala/org/apache/pekko/actor/FaultHandling.scala
index 01c5f92a2f..988b0e82c6 100644
--- a/actor/src/main/scala/org/apache/pekko/actor/FaultHandling.scala
+++ b/actor/src/main/scala/org/apache/pekko/actor/FaultHandling.scala
@@ -544,7 +544,8 @@ case class AllForOneStrategy(
* across actors and thus this field does not take up much space
*/
private val retriesWindow =
- (maxNrOfRetriesOption(maxNrOfRetries),
withinTimeRangeOption(withinTimeRange).map(_.toMillis.toInt))
+ (maxNrOfRetriesOption(maxNrOfRetries),
+ withinTimeRangeOption(withinTimeRange).map(d =>
math.min(Int.MaxValue.toLong, d.toMillis).toInt))
def handleChildTerminated(context: ActorContext, child: ActorRef, children:
Iterable[ActorRef]): Unit = ()
@@ -656,7 +657,8 @@ case class OneForOneStrategy(
*/
private val retriesWindow = (
SupervisorStrategy.maxNrOfRetriesOption(maxNrOfRetries),
-
SupervisorStrategy.withinTimeRangeOption(withinTimeRange).map(_.toMillis.toInt))
+ SupervisorStrategy.withinTimeRangeOption(withinTimeRange).map(d =>
+ math.min(Int.MaxValue.toLong, d.toMillis).toInt))
def handleChildTerminated(context: ActorContext, child: ActorRef, children:
Iterable[ActorRef]): Unit = ()
diff --git a/actor/src/main/scala/org/apache/pekko/util/FrequencySketch.scala
b/actor/src/main/scala/org/apache/pekko/util/FrequencySketch.scala
index 90d96df312..f7c7339341 100644
--- a/actor/src/main/scala/org/apache/pekko/util/FrequencySketch.scala
+++ b/actor/src/main/scala/org/apache/pekko/util/FrequencySketch.scala
@@ -60,8 +60,11 @@ private[pekko] object FrequencySketch {
resetMultiplier: Double = 10,
depth: Int = 4,
counterBits: Int = 4)(implicit hasher: Hasher[A]): FrequencySketch[A] = {
- val width = widthMultiplier * Bits.ceilingPowerOfTwo(capacity)
- val resetSize = (resetMultiplier * capacity).toInt
+ val widthLong = widthMultiplier.toLong *
Bits.ceilingPowerOfTwo(capacity).toLong
+ require(widthLong <= Int.MaxValue,
+ s"computed width ($widthLong) exceeds Int.MaxValue; reduce capacity or
widthMultiplier")
+ val width = widthLong.toInt
+ val resetSize = math.min(Int.MaxValue.toLong, (resetMultiplier *
capacity).toLong).toInt
new FrequencySketch(depth, width, counterBits, resetSize, hasher)
}
@@ -256,8 +259,11 @@ private[pekko] object FastFrequencySketch {
* @return a configured FastFrequencySketch
*/
def apply[A](capacity: Int, widthMultiplier: Int = 4, resetMultiplier:
Double = 10): FastFrequencySketch[A] = {
- val width = widthMultiplier *
FrequencySketch.Bits.ceilingPowerOfTwo(capacity)
- val resetSize = (resetMultiplier * capacity).toInt
+ val widthLong = widthMultiplier.toLong *
FrequencySketch.Bits.ceilingPowerOfTwo(capacity).toLong
+ require(widthLong <= Int.MaxValue,
+ s"computed width ($widthLong) exceeds Int.MaxValue; reduce capacity or
widthMultiplier")
+ val width = widthLong.toInt
+ val resetSize = math.min(Int.MaxValue.toLong, (resetMultiplier *
capacity).toLong).toInt
new FastFrequencySketch(width, resetSize)
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]