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]
