cloud-fan commented on code in PR #58962:
URL: https://github.com/apache/spark/pull/58962#discussion_r4068944306


##########
sql/connect/common/src/main/scala/org/apache/spark/sql/connect/common/UdfSerialization.scala:
##########
@@ -0,0 +1,108 @@
+/*
+ * 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.spark.sql.connect.common
+
+import java.io.{ByteArrayInputStream, InputStream, ObjectInputStream, 
ObjectStreamClass}
+
+/**
+ * Java deserialization for Scala UDF payloads that tolerates the 
`serialVersionUID` drift of
+ * `org.apache.spark.sql.types` classes between Spark versions.
+ *
+ * A Scala UDF payload embeds `org.apache.spark.sql.types` classes (the UDF's 
input/output schema).
+ * Those classes carry no explicit `@SerialVersionUID`, so the JVM 
auto-computes it from the whole
+ * class shape, which folds in compiler-synthesized members that are 
irrelevant to serialization --
+ * most notably the `$anonfun$` public static methods Scala emits for lambdas. 
A source change that
+ * only reshapes a lambda (e.g. rewriting a helper to use `existsRecursively { 
... }`) changes the
+ * auto-computed `serialVersionUID` without changing any serialized field, 
which makes a plain
+ * [[ObjectInputStream]] reject a payload produced by a different Spark 
version with an
+ * `InvalidClassException`, even though the payload is field-compatible.
+ *
+ * The property that actually governs compatibility is the serialized field 
layout. When a
+ * `sql.types` class arrives with a mismatched SUID but an identical 
serialized field layout, this
+ * reader rebinds the stream descriptor to the local class; any field-layout 
difference is left
+ * untouched so the standard SUID check still fails fast rather than 
misreading the stream.
+ *
+ * Only `org.apache.spark.sql.types` descriptors are treated tolerantly; every 
other class keeps
+ * the standard `serialVersionUID` compatibility check.
+ */
+private[spark] object UdfSerialization {
+
+  private val suidTolerantPackagePrefix = "org.apache.spark.sql.types."
+
+  /** Deserialize `bytes` resolving classes with `loader`, tolerating 
`sql.types` SUID drift. */
+  def deserialize[T](bytes: Array[Byte], loader: ClassLoader): T = {
+    val ois = new SuidTolerantObjectInputStream(new 
ByteArrayInputStream(bytes), loader)
+    try ois.readObject().asInstanceOf[T]
+    finally ois.close()
+  }
+
+  /** Deserialize from `in` with default class resolution, tolerating 
`sql.types` SUID drift. */
+  def deserialize[T](in: InputStream): T = {
+    new SuidTolerantObjectInputStream(in, null).readObject().asInstanceOf[T]
+  }
+
+  /**
+   * The complete serialized field layout of a descriptor: the set of 
persistent field name + JVM
+   * type signature. This is what governs whether [[ObjectInputStream]] can 
consume the producer's
+   * class-data block through the local descriptor. Every persistent slot is 
included, in
+   * particular the Scala lazy-val init `bitmap$*` slots: rebinding to a local 
descriptor whose
+   * slot shape differs would misalign the stream, so a bitmap difference must 
also block rebinding.
+   */
+  private[connect] def fieldSignature(desc: ObjectStreamClass): Set[String] = {
+    // getTypeString is null for primitives, where the single-char type code 
is the signature.
+    desc.getFields
+      .map(f => 
s"${f.getName}:${Option(f.getTypeString).getOrElse(f.getTypeCode.toString)}")
+      .toSet
+  }
+
+  private class SuidTolerantObjectInputStream(in: InputStream, loader: 
ClassLoader)
+    extends ObjectInputStream(in) {
+
+    override def resolveClass(desc: ObjectStreamClass): Class[_] = {
+      if (loader != null) {
+        // scalastyle:off classforname
+        Class.forName(desc.getName, false, loader)
+        // scalastyle:on classforname
+      } else {
+        super.resolveClass(desc)
+      }
+    }
+
+    override def readClassDescriptor(): ObjectStreamClass = {
+      val streamDesc = super.readClassDescriptor()
+      if (!streamDesc.getName.startsWith(suidTolerantPackagePrefix)) {
+        return streamDesc
+      }
+      val localClass =
+        try {
+          resolveClass(streamDesc)
+        } catch {
+          case _: ClassNotFoundException => return streamDesc
+        }
+      val localDesc = ObjectStreamClass.lookup(localClass)
+      if (localDesc == null ||
+        localDesc.getSerialVersionUID == streamDesc.getSerialVersionUID ||
+        fieldSignature(streamDesc) != fieldSignature(localDesc)) {

Review Comment:
   **Non-blocking (P2):** Persistent-field equality is not sufficient to 
replace the entire stream descriptor. `Metadata` is already a `sql.types` class 
with an explicit SUID, and `ObjectInputStream.readSerialData` also uses 
descriptor state such as `hasReadObjectMethod` and `hasWriteObjectData`. 
Returning `localDesc` here can therefore suppress a deliberate compatibility 
break or consume producer data under the local class's protocol, leading to a 
later stream error or incorrectly populated encoder state. This path should 
remain fail-closed unless the complete descriptor transition is known to be 
safe.
   
   **Recommended change:** Replace package-wide field-only eligibility with an 
exact audited class/from-SUID/to-SUID transition policy, retain the 
field-layout check as an additional guard, and add negative compatibility 
coverage for explicit-SUID classes and custom class-data protocols.
   
   **Why this works:** Key eligibility by class name plus the exact stream and 
local SUID pair for transitions whose producer and consumer are known to use 
the same default field-only Java serialization protocol. Require the existing 
persistent-field equality check as defense in depth. For any unlisted pair, 
including explicitly versioned production classes such as Metadata and classes 
with custom serialization hooks, return streamDesc so ObjectInputStream 
performs its standard compatibility rejection.
   
   **Scope:** Constrain whole-descriptor substitution to transitions whose SUID 
provenance and default serialization protocol have been audited, with 
regression-sensitive positive and negative fixtures.
   
   **Compatibility:** Classes outside sql.types, genuine field-layout 
mismatches, and authorized auto-computed-SUID drift retain their current helper 
outcomes.
   
   **Risks:** An incomplete transition set may reject a harmless drift until 
that exact pair is audited. A transition entry that omits either endpoint SUID 
could accidentally authorize a later incompatible version.
   
   **Constraints:** Do not depend on unsupported reflective access to private 
JDK descriptor flags. Keep standard ObjectInputStream rejection for every 
transition not affirmatively authorized.
   
   **Success:** A production sql.types class with an explicit SUID is not 
tolerated merely because its package and persistent fields match. A custom 
writeObject/readObject protocol difference cannot be hidden by local descriptor 
substitution. Each audited auto-SUID-only transition with an identical default 
field layout still deserializes successfully. Every unknown or incompatible 
transition retains streamDesc and uses standard ObjectInputStream checks.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to