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

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

commit 5a347f9874a68ef6c3263bf22ea9c60c846b18bf
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
    
    ### What changes were proposed in this pull request?
    
    `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.
    
    ### Why are the changes needed?
    
    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.
    
    ### Does this PR introduce _any_ user-facing change?
    
    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`.
    
    ### How was this patch tested?
    
    New test in `InvalidInputErrorsSuite` asserting a non-UserDefinedType 
`jvm_class` raises `CONNECT_INVALID_PLAN.UDT_JVM_CLASS_NOT_UDT`.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    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 cb69504d5ed9..07f369f1c8c3 100644
--- a/common/utils/src/main/resources/error/error-conditions.json
+++ b/common/utils/src/main/resources/error/error-conditions.json
@@ -1372,6 +1372,11 @@
           "Struct literal is not set."
         ]
       },
+      "UDT_JVM_CLASS_NOT_UDT" : {
+        "message" : [
+          "The 'jvm_class' <jvmClass> in the UDT is not a subclass of 
UserDefinedType."
+        ]
+      },
       "UDT_TYPE_FIELD_INVALID" : {
         "message" : [
           "UserDefinedType requires the 'type' field to be 'udt', but got 
'<udtType>'."
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 11b1b394b1ed..aad36e640f5c 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
@@ -158,10 +158,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("CONNECT_INVALID_PLAN.PYTHON_UDT_MISSING_FIELDS", Map.empty)
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 768a532a7a82..64866ad6ec66 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
@@ -139,6 +139,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")
+  }
+
   test("noHandlerFoundForExtension") {
     val ex = InvalidInputErrors.noHandlerFoundForExtension("foo.bar.Ext")
     assert(ex.getCondition.contains("NO_HANDLER_FOR_EXTENSION"))


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

Reply via email to