This is an automated email from the ASF dual-hosted git repository.
dongjoon-hyun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new 372ff4290fad [SPARK-58042][CONNECT] Validate the UDT jvm_class is a
UserDefinedType before instantiating it
372ff4290fad is described below
commit 372ff4290faddd05eb1254dd698fd3d04b91bcb4
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]>
---
.../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 dd3220d10b28..43938889fb98 100644
--- a/common/utils/src/main/resources/error/error-conditions.json
+++ b/common/utils/src/main/resources/error/error-conditions.json
@@ -1450,6 +1450,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 10ea4b3c4521..62ff4085bc73 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("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]