Skip to content

[SPARK-59319][SQL][FOLLOWUP] Enforce restricted mode in the single-pass analyzer and avoid eager class loading for rejected calls - #58642

Closed
HyukjinKwon wants to merge 1 commit into
apache:masterfrom
HyukjinKwon:SPARK-59319-restricted-mode-followup
Closed

[SPARK-59319][SQL][FOLLOWUP] Enforce restricted mode in the single-pass analyzer and avoid eager class loading for rejected calls#58642
HyukjinKwon wants to merge 1 commit into
apache:masterfrom
HyukjinKwon:SPARK-59319-restricted-mode-followup

Conversation

@HyukjinKwon

Copy link
Copy Markdown
Member

What changes were proposed in this pull request?

This is a follow-up to #58604, which added the opt-in restricted SQL execution mode (spark.sql.restrictedMode.enabled). It addresses three issues found in post-merge review, all scoped to when restricted mode is enabled (no change to default behavior):

  1. Enforce the mode across both analyzer paths. CheckAnalysis.checkRestrictedMode runs only in the fixed-point analyzer. The single-pass resolver resolves reflect/java_method/try_reflect and marks the plan analyzed without calling checkAnalysis, so the mode was not enforced when the single-pass resolver was enabled. HybridAnalyzer.apply now routes a restricted-mode session to the fixed-point analyzer (the same branch used by default when the single-pass resolver is off) in every single-pass mode.

  2. Do not load the referenced class before rejecting a reflect call. Type-checking a reflect/java_method/try_reflect call resolves the referenced class via Utils.classForName, which runs its static initializer. That happened during analysis, before checkRestrictedMode rejected the call. CallMethodViaReflection.checkInputDataTypes now short-circuits in restricted mode so a call that is going to be rejected never loads the class.

  3. Traverse each subquery only once. checkRestrictedMode descended into subquery plans both through SubqueryExpression and through innerChildren (which already contains them), doubling the work at every nesting level and making validation exponential in subquery-nesting depth. It now recurses through innerChildren only for the non-subquery inner plans (analysis-only command bodies), keeping validation linear.

Why are the changes needed?

Restricted mode is a gate that blocks reflect/java_method/try_reflect and TRANSFORM ... USING. Without (1) the gate could be bypassed by enabling the single-pass resolver; without (2) a rejected call could still run a class's static initializer; (3) made the gate's cost blow up on deeply nested subqueries.

Does this PR introduce any user-facing change?

No. All changes only affect sessions that have opted into restricted mode; the default behavior is unchanged.

How was this patch tested?

New unit tests in RestrictedModeSuite (rejection without initializing the referenced class) and RestrictedModeCommandSuite (enforcement with the single-pass resolver enabled; a feature nested inside a scalar subquery is still reached; deeply nested subqueries with no restricted feature analyze in linear time). Existing CallMethodViaReflectionSuite still passes.

Was this patch authored or co-authored using generative AI tooling?

Yes, generated by Claude.

@sunchao sunchao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed e344145. No actionable correctness findings.

Source tracing supports all three fixes: restricted-mode analyzer routing, avoiding reflection class initialization, and removing duplicate subquery traversal while retaining command-body checks. I reviewed the full diff, callers, tests, and prior discussion.

Validation: I did not run Spark tests locally because Scala/SBT dependencies are absent. CI precompilation and lint checks passed, and other checks are still running. The SparkR and pyspark-connect-old-client jobs currently fail on fillna with StringType is not matched at fillValue; these failures appear unrelated to the restricted-mode changes, but CI is not yet green.

@sunchao

sunchao commented Sep 9, 2026

Copy link
Copy Markdown
Member

Thanks for the quick follow-up @HyukjinKwon !

…ss analyzer and avoid eager class loading for rejected calls

### What changes were proposed in this pull request?

This is a follow-up to apache#58604, which added the opt-in restricted SQL execution mode (`spark.sql.restrictedMode.enabled`). It addresses three issues found in post-merge review, all scoped to when restricted mode is enabled (no change to default behavior):

1. **Enforce the mode across both analyzer paths.** `CheckAnalysis.checkRestrictedMode` runs only in the fixed-point analyzer. The single-pass resolver resolves `reflect`/`java_method`/`try_reflect` and marks the plan analyzed without calling `checkAnalysis`, so the mode was not enforced when the single-pass resolver was enabled. `HybridAnalyzer.apply` now routes a restricted-mode session to the fixed-point analyzer (the same branch used by default when the single-pass resolver is off) in every single-pass mode.

2. **Do not load the referenced class before rejecting a reflect call.** Type-checking a `reflect`/`java_method`/`try_reflect` call resolves the referenced class via `Utils.classForName`, which runs its static initializer. That happened during analysis, before `checkRestrictedMode` rejected the call. `CallMethodViaReflection.checkInputDataTypes` now short-circuits in restricted mode so a call that is going to be rejected never loads the class.

3. **Traverse each subquery only once.** `checkRestrictedMode` descended into subquery plans both through `SubqueryExpression` and through `innerChildren` (which already contains them), doubling the work at every nesting level and making validation exponential in subquery-nesting depth. It now recurses through `innerChildren` only for the non-subquery inner plans (analysis-only command bodies), keeping validation linear.

### Why are the changes needed?

Restricted mode is a gate that blocks `reflect`/`java_method`/`try_reflect` and `TRANSFORM ... USING`. Without (1) the gate could be bypassed by enabling the single-pass resolver; without (2) a rejected call could still run a class's static initializer; (3) made the gate's cost blow up on deeply nested subqueries.

### Does this PR introduce _any_ user-facing change?

No. All changes only affect sessions that have opted into restricted mode; the default behavior is unchanged.

### How was this patch tested?

New unit tests in `RestrictedModeSuite` (rejection without initializing the referenced class) and `RestrictedModeCommandSuite` (enforcement with the single-pass resolver enabled; a feature nested inside a scalar subquery is still reached; deeply nested subqueries with no restricted feature analyze in linear time). Existing `CallMethodViaReflectionSuite` still passes.

### Was this patch authored or co-authored using generative AI tooling?

Yes, generated by Claude.
@HyukjinKwon
HyukjinKwon force-pushed the SPARK-59319-restricted-mode-followup branch from e344145 to 4372e8f Compare September 9, 2026 05:51
@uros-b

uros-b commented Sep 9, 2026

Copy link
Copy Markdown
Member

Thank you @HyukjinKwon and @sunchao!

HyukjinKwon added a commit that referenced this pull request Sep 9, 2026
…ss analyzer and avoid eager class loading for rejected calls

### What changes were proposed in this pull request?

This is a follow-up to #58604, which added the opt-in restricted SQL execution mode (`spark.sql.restrictedMode.enabled`). It addresses three issues found in post-merge review, all scoped to when restricted mode is enabled (no change to default behavior):

1. **Enforce the mode across both analyzer paths.** `CheckAnalysis.checkRestrictedMode` runs only in the fixed-point analyzer. The single-pass resolver resolves `reflect`/`java_method`/`try_reflect` and marks the plan analyzed without calling `checkAnalysis`, so the mode was not enforced when the single-pass resolver was enabled. `HybridAnalyzer.apply` now routes a restricted-mode session to the fixed-point analyzer (the same branch used by default when the single-pass resolver is off) in every single-pass mode.

2. **Do not load the referenced class before rejecting a reflect call.** Type-checking a `reflect`/`java_method`/`try_reflect` call resolves the referenced class via `Utils.classForName`, which runs its static initializer. That happened during analysis, before `checkRestrictedMode` rejected the call. `CallMethodViaReflection.checkInputDataTypes` now short-circuits in restricted mode so a call that is going to be rejected never loads the class.

3. **Traverse each subquery only once.** `checkRestrictedMode` descended into subquery plans both through `SubqueryExpression` and through `innerChildren` (which already contains them), doubling the work at every nesting level and making validation exponential in subquery-nesting depth. It now recurses through `innerChildren` only for the non-subquery inner plans (analysis-only command bodies), keeping validation linear.

### Why are the changes needed?

Restricted mode is a gate that blocks `reflect`/`java_method`/`try_reflect` and `TRANSFORM ... USING`. Without (1) the gate could be bypassed by enabling the single-pass resolver; without (2) a rejected call could still run a class's static initializer; (3) made the gate's cost blow up on deeply nested subqueries.

### Does this PR introduce _any_ user-facing change?

No. All changes only affect sessions that have opted into restricted mode; the default behavior is unchanged.

### How was this patch tested?

New unit tests in `RestrictedModeSuite` (rejection without initializing the referenced class) and `RestrictedModeCommandSuite` (enforcement with the single-pass resolver enabled; a feature nested inside a scalar subquery is still reached; deeply nested subqueries with no restricted feature analyze in linear time). Existing `CallMethodViaReflectionSuite` still passes.

### Was this patch authored or co-authored using generative AI tooling?

Yes, generated by Claude.

Closes #58642 from HyukjinKwon/SPARK-59319-restricted-mode-followup.

Authored-by: Hyukjin Kwon <gurwls223@apache.org>
Signed-off-by: Hyukjin Kwon <hyukjin.kwon@databricks.com>
(cherry picked from commit 2e878ec)
Signed-off-by: Hyukjin Kwon <hyukjin.kwon@databricks.com>
HyukjinKwon added a commit that referenced this pull request Sep 9, 2026
…ss analyzer and avoid eager class loading for rejected calls

### What changes were proposed in this pull request?

This is a follow-up to #58604, which added the opt-in restricted SQL execution mode (`spark.sql.restrictedMode.enabled`). It addresses three issues found in post-merge review, all scoped to when restricted mode is enabled (no change to default behavior):

1. **Enforce the mode across both analyzer paths.** `CheckAnalysis.checkRestrictedMode` runs only in the fixed-point analyzer. The single-pass resolver resolves `reflect`/`java_method`/`try_reflect` and marks the plan analyzed without calling `checkAnalysis`, so the mode was not enforced when the single-pass resolver was enabled. `HybridAnalyzer.apply` now routes a restricted-mode session to the fixed-point analyzer (the same branch used by default when the single-pass resolver is off) in every single-pass mode.

2. **Do not load the referenced class before rejecting a reflect call.** Type-checking a `reflect`/`java_method`/`try_reflect` call resolves the referenced class via `Utils.classForName`, which runs its static initializer. That happened during analysis, before `checkRestrictedMode` rejected the call. `CallMethodViaReflection.checkInputDataTypes` now short-circuits in restricted mode so a call that is going to be rejected never loads the class.

3. **Traverse each subquery only once.** `checkRestrictedMode` descended into subquery plans both through `SubqueryExpression` and through `innerChildren` (which already contains them), doubling the work at every nesting level and making validation exponential in subquery-nesting depth. It now recurses through `innerChildren` only for the non-subquery inner plans (analysis-only command bodies), keeping validation linear.

### Why are the changes needed?

Restricted mode is a gate that blocks `reflect`/`java_method`/`try_reflect` and `TRANSFORM ... USING`. Without (1) the gate could be bypassed by enabling the single-pass resolver; without (2) a rejected call could still run a class's static initializer; (3) made the gate's cost blow up on deeply nested subqueries.

### Does this PR introduce _any_ user-facing change?

No. All changes only affect sessions that have opted into restricted mode; the default behavior is unchanged.

### How was this patch tested?

New unit tests in `RestrictedModeSuite` (rejection without initializing the referenced class) and `RestrictedModeCommandSuite` (enforcement with the single-pass resolver enabled; a feature nested inside a scalar subquery is still reached; deeply nested subqueries with no restricted feature analyze in linear time). Existing `CallMethodViaReflectionSuite` still passes.

### Was this patch authored or co-authored using generative AI tooling?

Yes, generated by Claude.

Closes #58642 from HyukjinKwon/SPARK-59319-restricted-mode-followup.

Authored-by: Hyukjin Kwon <gurwls223@apache.org>
Signed-off-by: Hyukjin Kwon <hyukjin.kwon@databricks.com>
(cherry picked from commit 2e878ec)
Signed-off-by: Hyukjin Kwon <hyukjin.kwon@databricks.com>
@HyukjinKwon

Copy link
Copy Markdown
Member Author

Merge Summary:

Posted by merge_spark_pr.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants