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]
