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 cffd027b89 fix: don't resolve a wire-supplied manifest class the
serializer will not use (#3495)
cffd027b89 is described below
commit cffd027b89937115ee9a6a7c5b9f8e37df53a6cd
Author: PJ Fanning <[email protected]>
AuthorDate: Mon Aug 31 22:01:39 2026 +0100
fix: don't resolve a wire-supplied manifest class the serializer will not
use (#3495)
Motivation:
Serialization.deserializeByteArray resolved the manifest string from the
wire
into a Class via dynamicAccess.getClassFor for every plain Serializer,
without
checking whether that serializer wants a type hint. Serializers.manifestFor
shows a conforming peer only sends a manifest when includeManifest is true,
so
for a serializer declaring includeManifest = false the class name can only
have
come from a non-conforming or hostile sender - and the loaded class is then
discarded. ByteArraySerializer and NullSerializer are bound by default and
reachable by serializer id, so a peer could name any class on the classpath
and
have it loaded: a classpath-probing oracle, and metaspace and manifest-cache
growth that is never released.
Modification:
Pass None to a plain Serializer that declares includeManifest = false
instead of
resolving the wire-supplied name, which is exactly what a conforming sender
produces. Serializers that do ask for the hint are unchanged.
Result:
A peer can no longer drive class loading through a serializer that ignores
the
type hint. No behaviour change for conforming messages.
Tests:
- sbt "actor-tests/testOnly
org.apache.pekko.serialization.WireManifestClassLoadingSpec" - new; the first
case fails without this change with "Cannot find manifest class
[com.example.NotOnTheClasspath]"
- sbt "actor-tests/testOnly org.apache.pekko.serialization.SerializeSpec" -
existing spec passes unchanged
- sbt "actor/mimaReportBinaryIssues" - no issues
References:
Refs #3478
---
.../WireManifestClassLoadingSpec.scala | 85 ++++++++++++++++++++++
.../apache/pekko/serialization/Serialization.scala | 6 +-
2 files changed, 90 insertions(+), 1 deletion(-)
diff --git
a/actor-tests/src/test/scala/org/apache/pekko/serialization/WireManifestClassLoadingSpec.scala
b/actor-tests/src/test/scala/org/apache/pekko/serialization/WireManifestClassLoadingSpec.scala
new file mode 100644
index 0000000000..1cf16a7ca6
--- /dev/null
+++
b/actor-tests/src/test/scala/org/apache/pekko/serialization/WireManifestClassLoadingSpec.scala
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.pekko.serialization
+
+import java.io.NotSerializableException
+import java.nio.charset.StandardCharsets.UTF_8
+
+import org.apache.pekko
+import pekko.actor.ExtendedActorSystem
+import pekko.testkit.PekkoSpec
+
+object WireManifestClassLoadingSpec {
+
+ /** A plain `Serializer` that does not want a type hint. */
+ class NoManifestSerializer(@annotation.nowarn("msg=never used") val system:
ExtendedActorSystem) extends Serializer {
+ override def identifier: Int = 9911
+ override def includeManifest: Boolean = false
+ override def toBinary(o: AnyRef): Array[Byte] = o.toString.getBytes(UTF_8)
+ override def fromBinary(bytes: Array[Byte], manifest: Option[Class[?]]):
AnyRef = {
+ // a serializer declaring includeManifest = false must never be handed a
type hint
+ if (manifest.isDefined)
+ throw new AssertionError(s"unexpected type hint
[${manifest.get.getName}]")
+ new String(bytes, UTF_8)
+ }
+ }
+
+ /** A plain `Serializer` that does want a type hint. */
+ class WithManifestSerializer(@annotation.nowarn("msg=never used") val
system: ExtendedActorSystem)
+ extends Serializer {
+ override def identifier: Int = 9912
+ override def includeManifest: Boolean = true
+ override def toBinary(o: AnyRef): Array[Byte] = o.toString.getBytes(UTF_8)
+ override def fromBinary(bytes: Array[Byte], manifest: Option[Class[?]]):
AnyRef =
+ new String(bytes, UTF_8) + ":" +
manifest.map(_.getName).getOrElse("none")
+ }
+}
+
+class WireManifestClassLoadingSpec
+ extends PekkoSpec(s"""
+ pekko.actor.serializers {
+ no-manifest =
"${classOf[WireManifestClassLoadingSpec.NoManifestSerializer].getName}"
+ with-manifest =
"${classOf[WireManifestClassLoadingSpec.WithManifestSerializer].getName}"
+ }
+ """) {
+
+ private val serialization = SerializationExtension(system)
+ private val payload = "hello".getBytes(UTF_8)
+
+ "Deserialization of a wire-supplied manifest" must {
+
+ "not resolve a class for a serializer that declares includeManifest =
false" in {
+ // A hostile or non-conforming peer can put any string in the manifest
field. For a
+ // serializer that ignores the hint there is no reason to turn it into a
class load.
+ val result = serialization.deserialize(payload, 9911,
"com.example.NotOnTheClasspath").get
+ result should ===("hello")
+ }
+
+ "still resolve a class for a serializer that declares includeManifest =
true" in {
+ val result = serialization.deserialize(payload, 9912,
classOf[String].getName).get
+ result should ===("hello:java.lang.String")
+ }
+
+ "still fail for an unknown manifest class when the serializer wants the
hint" in {
+ val ex = intercept[NotSerializableException] {
+ serialization.deserialize(payload, 9912,
"com.example.NotOnTheClasspath").get
+ }
+ ex.getMessage should include("com.example.NotOnTheClasspath")
+ }
+ }
+}
diff --git
a/actor/src/main/scala/org/apache/pekko/serialization/Serialization.scala
b/actor/src/main/scala/org/apache/pekko/serialization/Serialization.scala
index 9515ec1b23..2443a28600 100644
--- a/actor/src/main/scala/org/apache/pekko/serialization/Serialization.scala
+++ b/actor/src/main/scala/org/apache/pekko/serialization/Serialization.scala
@@ -206,7 +206,11 @@ class Serialization(val system: ExtendedActorSystem)
extends Extension {
serializer match {
case s2: SerializerWithStringManifest => s2.fromBinary(bytes, manifest)
case s1 =>
- if (manifest == "")
+ if (manifest == "" || !s1.includeManifest)
+ // A conforming peer only sends a manifest for a serializer that
asks for one
+ // (see `Serializers.manifestFor`), and a serializer with
`includeManifest = false`
+ // ignores the type hint anyway. Resolving a class named by the
wire that will not
+ // be used only exposes class loading to whoever wrote the message.
s1.fromBinary(bytes, None)
else {
val cache = manifestCache.get
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]