This is an automated email from the ASF dual-hosted git repository.
pjfanning pushed a commit to branch 1.7.x
in repository https://gitbox.apache.org/repos/asf/pekko.git
The following commit(s) were added to refs/heads/1.7.x by this push:
new f2b7bdbf08 fix: bound the number of entries in a compression table
advertisement (#3510) (#3521)
f2b7bdbf08 is described below
commit f2b7bdbf0813ccf087f432cd8470e2b5a21e072b
Author: PJ Fanning <[email protected]>
AuthorDate: Fri Sep 4 14:41:24 2026 +0100
fix: bound the number of entries in a compression table advertisement
(#3510) (#3521)
Motivation:
deserializeCompressionAdvertisement resolves every key in the advertised
table, and for actor refs that means parsing a path and populating the
resolve cache. The key list was unbounded, so the only limit was the
transport frame size. Measured, 10000 entries is 369 KB of wire and about
150 ms of CPU on the inbound control stream, against 9 KB for the 256
entry table a peer legitimately advertises.
Modification:
Reject an advertisement carrying more entries than
pekko.remote.artery.advanced.compression.<table>.max, the setting that
bounds the table on the sending side and is normally the same across a
cluster. When it is "off" locally there is no number to check against and
no bound is applied.
Result:
An oversized advertisement is reported as a serialization failure, which
the inbound stream logs and drops. Advertisements are resent periodically,
so a dropped one costs at most a delay in establishing compression.
---
.../serialization/ArteryMessageSerializer.scala | 35 ++++++++++++++++++++--
.../ArteryMessageSerializerSpec.scala | 26 +++++++++++++++-
2 files changed, 58 insertions(+), 3 deletions(-)
diff --git
a/remote/src/main/scala/org/apache/pekko/remote/serialization/ArteryMessageSerializer.scala
b/remote/src/main/scala/org/apache/pekko/remote/serialization/ArteryMessageSerializer.scala
index 5fe8b53bd1..73c1ccf37d 100644
---
a/remote/src/main/scala/org/apache/pekko/remote/serialization/ArteryMessageSerializer.scala
+++
b/remote/src/main/scala/org/apache/pekko/remote/serialization/ArteryMessageSerializer.scala
@@ -27,6 +27,7 @@ import pekko.remote.artery.compress.CompressionProtocol._
import pekko.serialization.{ BaseSerializer, Serialization,
SerializationExtension, SerializerWithStringManifest }
import pekko.remote.artery.Flush
import pekko.remote.artery.FlushAck
+import pekko.util.Helpers.toRootLowerCase
/** INTERNAL API */
private[pekko] object ArteryMessageSerializer {
@@ -60,6 +61,18 @@ private[pekko] final class ArteryMessageSerializer(val
system: ExtendedActorSyst
private lazy val serialization = SerializationExtension(system)
+ // `pekko.remote.artery.advanced.compression.<table>.max` bounds the number
of entries the
+ // sending side puts in a table, and is normally the same setting across a
cluster. Parsed the
+ // same way ArterySettings parses it, without building the whole settings
object here.
+ private def compressionMax(table: String): Int = {
+ val path = s"pekko.remote.artery.advanced.compression.$table.max"
+ if (toRootLowerCase(system.settings.config.getString(path)) == "off") 0
+ else system.settings.config.getInt(path)
+ }
+
+ private val maxActorRefCompressionEntries: Int = compressionMax("actor-refs")
+ private val maxClassManifestCompressionEntries: Int =
compressionMax("manifests")
+
override def manifest(o: AnyRef): String = o match { // most frequent ones
first
case _: SystemMessageDelivery.SystemMessageEnvelope =>
SystemMessageEnvelopeManifest
case _: SystemMessageDelivery.Ack =>
SystemMessageDeliveryAckManifest
@@ -123,7 +136,11 @@ private[pekko] final class ArteryMessageSerializer(val
system: ExtendedActorSyst
case ActorRefCompressionAdvertisementAckManifest =>
deserializeCompressionTableAdvertisementAck(bytes,
ActorRefCompressionAdvertisementAck.apply)
case ClassManifestCompressionAdvertisementManifest =>
- deserializeCompressionAdvertisement(bytes, identity,
ClassManifestCompressionAdvertisement.apply)
+ deserializeCompressionAdvertisement(
+ bytes,
+ identity,
+ maxClassManifestCompressionEntries,
+ ClassManifestCompressionAdvertisement.apply)
case ClassManifestCompressionAdvertisementAckManifest =>
deserializeCompressionTableAdvertisementAck(bytes,
ClassManifestCompressionAdvertisementAck.apply)
case ArteryHeartbeatManifest => RemoteWatcher.ArteryHeartbeat
@@ -158,7 +175,11 @@ private[pekko] final class ArteryMessageSerializer(val
system: ExtendedActorSyst
serializeCompressionAdvertisement(adv)(serializeActorRef)
def deserializeActorRefCompressionAdvertisement(bytes: Array[Byte]):
ActorRefCompressionAdvertisement =
- deserializeCompressionAdvertisement(bytes, deserializeActorRef,
ActorRefCompressionAdvertisement.apply)
+ deserializeCompressionAdvertisement(
+ bytes,
+ deserializeActorRef,
+ maxActorRefCompressionEntries,
+ ActorRefCompressionAdvertisement.apply)
def serializeCompressionAdvertisement[T](adv: CompressionAdvertisement[T])(
keySerializer: T => String):
ArteryControlFormats.CompressionTableAdvertisement = {
@@ -179,9 +200,19 @@ private[pekko] final class ArteryMessageSerializer(val
system: ExtendedActorSyst
def deserializeCompressionAdvertisement[T, U](
bytes: Array[Byte],
keyDeserializer: String => T,
+ maxEntries: Int,
create: (UniqueAddress, CompressionTable[T]) => U): U = {
val protoAdv =
ArteryControlFormats.CompressionTableAdvertisement.parseFrom(bytes)
+ // Every key is resolved, and for actor refs that means parsing a path and
populating the
+ // resolve cache, so a message with far more entries than a table can hold
is work out of
+ // proportion to its size. `maxEntries` is 0 when compression is switched
off here, and then
+ // there is no configured number to check against.
+ if (maxEntries > 0 && protoAdv.getKeysCount > maxEntries)
+ throw new NotSerializableException(
+ s"Compression table advertisement carries [${protoAdv.getKeysCount}]
entries, more than " +
+ s"the configured maximum of [$maxEntries]")
+
val kvs =
protoAdv.getKeysList.asScala
.map(keyDeserializer)
diff --git
a/remote/src/test/scala/org/apache/pekko/remote/serialization/ArteryMessageSerializerSpec.scala
b/remote/src/test/scala/org/apache/pekko/remote/serialization/ArteryMessageSerializerSpec.scala
index bc01ea2e78..88c6167881 100644
---
a/remote/src/test/scala/org/apache/pekko/remote/serialization/ArteryMessageSerializerSpec.scala
+++
b/remote/src/test/scala/org/apache/pekko/remote/serialization/ArteryMessageSerializerSpec.scala
@@ -19,7 +19,7 @@ import org.apache.pekko
import pekko.actor._
import pekko.remote.artery.Flush
import pekko.remote.artery.FlushAck
-import pekko.remote.{ RemoteWatcher, UniqueAddress }
+import pekko.remote.{ ArteryControlFormats, RemoteWatcher, UniqueAddress }
import pekko.remote.artery.{ ActorSystemTerminating,
ActorSystemTerminatingAck, Quarantined, SystemMessageDelivery }
import pekko.remote.artery.OutboundHandshake.{ HandshakeReq, HandshakeRsp }
import pekko.remote.artery.compress.CompressionProtocol.{
@@ -74,6 +74,30 @@ class ArteryMessageSerializerSpec extends PekkoSpec {
"not support UniqueAddresses without host/port set" in pending
+ "reject a compression table advertisement with more entries than the
configured maximum" in {
+ val serializer = new
ArteryMessageSerializer(system.asInstanceOf[ExtendedActorSystem])
+ val max =
system.settings.config.getInt("pekko.remote.artery.advanced.compression.actor-refs.max")
+
+ def advertisement(entries: Int): Array[Byte] = {
+ val builder =
ArteryControlFormats.CompressionTableAdvertisement.newBuilder
+ .setFrom(serializer.serializeUniqueAddress(uniqueAddress()))
+ .setOriginUid(17L)
+ .setTableVersion(1)
+ (0 until entries).foreach { i =>
+ builder.addKeys(s"pekko://sys@host:1234/user/a$i")
+ builder.addValues(i)
+ }
+ builder.build().toByteArray
+ }
+
+ // a table of exactly the configured size is what a peer legitimately
advertises
+ serializer.fromBinary(advertisement(max), "f") shouldBe
a[ActorRefCompressionAdvertisement]
+
+ intercept[NotSerializableException] {
+ serializer.fromBinary(advertisement(max + 1), "f")
+ }.getMessage should include(s"more than the configured maximum of
[$max]")
+ }
+
"reject invalid manifest" in {
intercept[IllegalArgumentException] {
val serializer = new
ArteryMessageSerializer(system.asInstanceOf[ExtendedActorSystem])
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]