This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/spark.git

commit 84edbdd7e501af6faf478d9b22338a0cd93b69d7
Author: Peter Toth <[email protected]>
AuthorDate: Wed Jul 8 17:26:25 2026 -0700

    [SPARK-58042][CONNECT] Validate the UDT jvm_class is a UserDefinedType 
before instantiating it
    
    `DataTypeProtoConverter.toCatalystUDT` deserializes a client-supplied 
`DataType.UDT` proto. When the UDT carries a `jvm_class`, it loaded and 
instantiated that class with 
`classForName(jvmClass).getConstructor().newInstance()` and only then relied on 
the (erased) cast to `UserDefinedType[_]`. So the class's no-arg constructor - 
and, because `classForName` defaults to `initialize = true`, its static 
initializers - ran before any type check, and a `jvm_class` that is not a 
`UserDefinedT [...]
    
    This change loads the class without initializing it, checks 
`classOf[UserDefinedType[_]].isAssignableFrom(clazz)` first, and throws a clear 
`InvalidPlanInput` (`CONNECT_INVALID_PLAN.UDT_JVM_CLASS_NOT_UDT`) if it is not 
a `UserDefinedType`. A valid UDT is instantiated exactly as before.
    
    Only a class that is actually a `UserDefinedType` should be instantiated 
when converting a UDT proto. Checking the type first gives a clear error for an 
invalid `jvm_class` instead of a `ClassCastException`, and avoids running the 
constructor/static initializer of an unrelated class named in the request.
    
    Yes, a small error change: an invalid non-UDT `jvm_class` in a 
`DataType.UDT` now raises `CONNECT_INVALID_PLAN.UDT_JVM_CLASS_NOT_UDT` instead 
of a `ClassCastException`.
    
    New test in `InvalidInputErrorsSuite` asserting a non-UserDefinedType 
`jvm_class` raises `CONNECT_INVALID_PLAN.UDT_JVM_CLASS_NOT_UDT`.
    
    Generated-by: Claude Code (Opus 4.8)
    
    Closes #57130 from peter-toth/SPARK-58042-validate-udt-jvm-class.
    
    Authored-by: Peter Toth <[email protected]>
    Signed-off-by: Dongjoon Hyun <[email protected]>
    (cherry picked from commit 372ff4290faddd05eb1254dd698fd3d04b91bcb4)
---
 .../src/main/resources/error/error-conditions.json     |  5 +++++
 .../sql/connect/common/DataTypeProtoConverter.scala    | 16 ++++++++++++----
 .../sql/connect/planner/InvalidInputErrorsSuite.scala  | 18 ++++++++++++++++++
 3 files changed, 35 insertions(+), 4 deletions(-)

diff --git a/common/utils/src/main/resources/error/error-conditions.json 
b/common/utils/src/main/resources/error/error-conditions.json
index 43e293ef8013..4e0f2a397319 100644
--- a/common/utils/src/main/resources/error/error-conditions.json
+++ b/common/utils/src/main/resources/error/error-conditions.json
@@ -919,6 +919,11 @@
           "This typically occurs when building very complex queries with many 
operations, large literals, or deeply nested expressions.",
           "Consider splitting the query into smaller parts using temporary 
views for intermediate results or reducing the number of operations."
         ]
+      },
+      "UDT_JVM_CLASS_NOT_UDT" : {
+        "message" : [
+          "The 'jvm_class' <jvmClass> in the UDT is not a subclass of 
UserDefinedType."
+        ]
       }
     },
     "sqlState" : "56K00"
diff --git 
a/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/common/DataTypeProtoConverter.scala
 
b/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/common/DataTypeProtoConverter.scala
index ac69f084c307..6c24b2b3861b 100644
--- 
a/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/common/DataTypeProtoConverter.scala
+++ 
b/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/common/DataTypeProtoConverter.scala
@@ -151,10 +151,18 @@ object DataTypeProtoConverter {
     }
 
     if (t.hasJvmClass) {
-      SparkClassUtils
-        .classForName[UserDefinedType[_]](t.getJvmClass)
-        .getConstructor()
-        .newInstance()
+      // Verify the class is a UserDefinedType before constructing it. 
newInstance() runs the
+      // class's no-arg constructor, so load it without initializing and check 
the type first,
+      // rather than instantiating an arbitrary client-provided class name and 
relying on a
+      // later cast (which happens only after the constructor has already run).
+      val clazz =
+        SparkClassUtils.classForName[UserDefinedType[_]](t.getJvmClass, 
initialize = false)
+      if (!classOf[UserDefinedType[_]].isAssignableFrom(clazz)) {
+        throw InvalidPlanInput(
+          "CONNECT_INVALID_PLAN.UDT_JVM_CLASS_NOT_UDT",
+          Map("jvmClass" -> t.getJvmClass))
+      }
+      clazz.getConstructor().newInstance()
     } else {
       if (!t.hasPythonClass || !t.hasSerializedPythonClass || !t.hasSqlType) {
         throw InvalidPlanInput(
diff --git 
a/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/planner/InvalidInputErrorsSuite.scala
 
b/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/planner/InvalidInputErrorsSuite.scala
index 07c377a77df5..548b8a786c66 100644
--- 
a/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/planner/InvalidInputErrorsSuite.scala
+++ 
b/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/planner/InvalidInputErrorsSuite.scala
@@ -135,6 +135,24 @@ class InvalidInputErrorsSuite extends PlanTest with 
SparkConnectPlanTest {
     }
   }
 
+  test("SPARK-58042: UDT jvm_class must be a UserDefinedType") {
+    // A jvm_class that is not a UserDefinedType must be rejected before it is 
instantiated.
+    val udt = proto.DataType.UDT
+      .newBuilder()
+      .setType("udt")
+      .setJvmClass("java.lang.String")
+      .build()
+    val dataType = proto.DataType.newBuilder().setUdt(udt).build()
+    val exception = intercept[InvalidPlanInput] {
+      DataTypeProtoConverter.toCatalystType(dataType)
+    }
+    checkError(
+      exception = exception,
+      condition = "CONNECT_INVALID_PLAN.UDT_JVM_CLASS_NOT_UDT",
+      parameters = Map("jvmClass" -> "java.lang.String"))
+    assert(exception.getSqlState == "56K00")
+  }
+
   // Helper case class to define test cases
   case class TestCase(
       name: String,


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

Reply via email to