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

github-actions[bot] pushed a commit to branch site
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/site by this push:
     new fe9109bcec [CALCITE-7760] Harden Spark engine activation: require 
opt-in via system property
fe9109bcec is described below

commit fe9109bcec551c8535ef40ac5b28ec6582e1fcc6
Author: Ruben Quesada Lopez <[email protected]>
AuthorDate: Tue Sep 1 12:06:29 2026 +0100

    [CALCITE-7760] Harden Spark engine activation: require opt-in via system 
property
---
 .../calcite/config/CalciteSystemProperty.java      | 37 ++++++++++++
 .../org/apache/calcite/jdbc/CalcitePrepare.java    | 12 +++-
 .../apache/calcite/jdbc/SparkHandlerGateTest.java  | 68 ++++++++++++++++++++++
 site/_docs/history.md                              |  5 ++
 site/_docs/security_threat_model.md                | 11 ++++
 spark/build.gradle.kts                             |  4 ++
 .../calcite/adapter/spark/SparkHandlerImpl.java    | 34 +++++++++--
 7 files changed, 166 insertions(+), 5 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/config/CalciteSystemProperty.java 
b/core/src/main/java/org/apache/calcite/config/CalciteSystemProperty.java
index b0efb1a05d..e438f920e3 100644
--- a/core/src/main/java/org/apache/calcite/config/CalciteSystemProperty.java
+++ b/core/src/main/java/org/apache/calcite/config/CalciteSystemProperty.java
@@ -473,6 +473,43 @@ public final class CalciteSystemProperty<T> {
   public static final CalciteSystemProperty<String> MODEL_CLASSES_DENIED =
       stringProperty("calcite.model.classes.denied", "");
 
+  /**
+   * Base directory that file-based connection models
+   * ({@code model=<path>}) must reside in.
+   *
+   * <p>The {@code model} connection property is settable by a query
+   * author, and a non-{@code inline:} value is read from the local
+   * filesystem before any schema is created. When this property is empty
+   * (the default), any local path is accepted. When it is set, relative
+   * model paths resolve under the configured directory and any model path
+   * that resolves outside it is rejected. The check is lexical and does
+   * not follow symbolic links.
+   *
+   * @see org.apache.calcite.model.ModelHandler
+   */
+  public static final CalciteSystemProperty<String> MODEL_BASE_DIRECTORY =
+      stringProperty("calcite.model.baseDirectory", "");
+
+  /**
+   * Maximum number of decimal digits that the plain-notation expansion of a 
{@code DECIMAL}
+   * literal may contain.
+   *
+   * <p>{@link java.math.BigDecimal} accepts any {@code int} exponent, so 
without a bound
+   * a ~15-character literal such as {@code DECIMAL '1E2147483647'} would ask
+   * {@link java.math.BigDecimal#toPlainString()} to materialize one character 
per digit,
+   * i.e. a multi-gigabyte allocation that would end in {@link 
OutOfMemoryError} inside the parser.
+   *
+   * <p>Default {@code 10000} is well beyond any dialect's realistic maximum 
{@code DECIMAL}
+   * precision while still bounding worst-case allocation to ~20 KB. Raise if 
a dialect
+   * legitimately needs more.
+   */
+  public static final CalciteSystemProperty<Integer> 
MAX_DECIMAL_LITERAL_PLAIN_DIGITS =
+      intProperty("calcite.parser.maxDecimalLiteralPlainDigits", 10_000, v -> 
v > 0);
+
+  /** Whether the Spark engine is enabled, making the {@code spark} connection 
property honored. */
+  public static final CalciteSystemProperty<Boolean> ENABLE_SPARK_ENGINE =
+      booleanProperty("calcite.enable.spark", false);
+
   private static CalciteSystemProperty<Boolean> booleanProperty(String key,
       boolean defaultValue) {
     // Note that "" -> true (convenient for command-lines flags like '-Dflag')
diff --git a/core/src/main/java/org/apache/calcite/jdbc/CalcitePrepare.java 
b/core/src/main/java/org/apache/calcite/jdbc/CalcitePrepare.java
index ec37fd4c7a..26e267ab47 100644
--- a/core/src/main/java/org/apache/calcite/jdbc/CalcitePrepare.java
+++ b/core/src/main/java/org/apache/calcite/jdbc/CalcitePrepare.java
@@ -22,6 +22,7 @@
 import org.apache.calcite.avatica.ColumnMetaData;
 import org.apache.calcite.avatica.Meta;
 import org.apache.calcite.config.CalciteConnectionConfig;
+import org.apache.calcite.config.CalciteSystemProperty;
 import org.apache.calcite.linq4j.Enumerable;
 import org.apache.calcite.linq4j.EnumerableDefaults;
 import org.apache.calcite.linq4j.Queryable;
@@ -167,8 +168,17 @@ private Dummy() {}
     /** Returns a spark handler. Returns a trivial handler, for which
      * {@link SparkHandler#enabled()} returns {@code false}, if {@code enable}
      * is {@code false} or if Spark is not on the class path. Never returns
-     * null. */
+     * null.
+     *
+     * <p>If {@code enable=true} this method requires the
+     * {@link CalciteSystemProperty#ENABLE_SPARK_ENGINE} to be active,
+     * otherwise an exception will be thrown. */
     public static synchronized SparkHandler getSparkHandler(boolean enable) {
+      if (enable && !CalciteSystemProperty.ENABLE_SPARK_ENGINE.value()) {
+        throw new SecurityException("The 'spark' property was set on a"
+            + " connection, but the Spark engine has not been enabled; set the 
JVM"
+            + " system property 'calcite.enable.spark' to 'true' to enable 
it");
+      }
       if (sparkHandler == null) {
         sparkHandler = enable ? createHandler() : new TrivialSparkHandler();
       }
diff --git 
a/core/src/test/java/org/apache/calcite/jdbc/SparkHandlerGateTest.java 
b/core/src/test/java/org/apache/calcite/jdbc/SparkHandlerGateTest.java
new file mode 100644
index 0000000000..fb3adbf614
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/jdbc/SparkHandlerGateTest.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.jdbc;
+
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.Statement;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Tests that the Spark engine requires the operator-level opt-in
+ * {@code calcite.enable.spark}.
+ *
+ * <p>These tests run without {@code -Dcalcite.enable.spark=true} (the
+ * default), so the gate is closed. The Spark module's own tests run with
+ * the opt-in set (see {@code spark/build.gradle.kts}).
+ */
+public class SparkHandlerGateTest {
+  @Test void testSparkHandlerRequiresOperatorOptIn() {
+    SecurityException e =
+        assertThrows(SecurityException.class, () ->
+            CalcitePrepare.Dummy.getSparkHandler(true));
+    assertThat(e.getMessage(), containsString("calcite.enable.spark"));
+  }
+
+  @Test void testSparkConnectionPropertyGatedBeforePrepare()
+      throws Exception {
+    try (Connection connection =
+             DriverManager.getConnection("jdbc:calcite:spark=true");
+         Statement statement = connection.createStatement()) {
+      Throwable e =
+          assertThrows(Throwable.class, () ->
+              statement.executeQuery("values (1, 2, 3, 4, 5, 6)"));
+      while (e != null && !(e instanceof SecurityException)) {
+        e = e.getCause();
+      }
+      assertThat("expected a SecurityException in the chain", e,
+          notNullValue());
+      assertThat(e.getMessage(), containsString("calcite.enable.spark"));
+    }
+  }
+
+  @Test void testTrivialHandlerStillAvailableWithoutOptIn() {
+    assertThat(CalcitePrepare.Dummy.getSparkHandler(false).enabled(),
+        is(false));
+  }
+}
diff --git a/site/_docs/history.md b/site/_docs/history.md
index 9ba5c7a42a..fabe61ed04 100644
--- a/site/_docs/history.md
+++ b/site/_docs/history.md
@@ -86,6 +86,11 @@ #### Breaking Changes
 requires exactly 16 bytes; a longer value used to be truncated. Blanks are not
 trimmed.
 
+* [<a 
href="https://issues.apache.org/jira/browse/CALCITE-7760";>CALCITE-7760</a>]
+The Spark engine now requires the operator-level opt-in system property
+`-Dcalcite.enable.spark=true`. The `spark` connection property alone no longer
+activates it; a connection using `spark=true` without the opt-in will fail.
+
 #### New features
 {: #new-features-1-43-0}
 
diff --git a/site/_docs/security_threat_model.md 
b/site/_docs/security_threat_model.md
index d9936197d4..feb5abd733 100644
--- a/site/_docs/security_threat_model.md
+++ b/site/_docs/security_threat_model.md
@@ -120,6 +120,17 @@ ## Not a vulnerability
   must add it on purpose.
 * A file, CSV, or JSON adapter reading the local path it was configured
   with. Opt-in, by the same reasoning as the os-adapter.
+* The Spark engine and its side effects: a local `JavaSparkContext`, a
+  local HTTP class server that serves compiled query classes, and setting
+  the `spark.repl.class.uri` JVM system property. The `spark` connection
+  property alone does not activate the engine; it additionally requires
+  the operator-set JVM system property `calcite.enable.spark=true`, a
+  capability a query author does not have (see
+  [Attacker and trust boundary](#attacker-and-trust-boundary)). Opt-in,
+  by the same reasoning as the os-adapter.
+* Reading the file or URL named by `model=<uri>`. The property names a
+  resource the model handler reads on connection; letting an untrusted
+  principal set connection properties authorises that read.
 * Anything that needs a changed system property or classpath. Both are
   outside the attacker's reach by assumption.
 * The behavior of a third-party driver once Calcite has connected to the
diff --git a/spark/build.gradle.kts b/spark/build.gradle.kts
index 4975686f45..cda178b36b 100644
--- a/spark/build.gradle.kts
+++ b/spark/build.gradle.kts
@@ -45,6 +45,10 @@
     testRuntimeOnly("org.apache.logging.log4j:log4j-slf4j-impl")
 
     tasks.withType<Test>().configureEach {
+        // The Spark engine requires an operator-level opt-in (see
+        // CalciteSystemProperty.ENABLE_SPARK_ENGINE); these tests exercise
+        // the engine deliberately
+        systemProperty("calcite.enable.spark", "true")
         if (JavaVersion.current() >= JavaVersion.VERSION_17) {
             jvmArgs("--add-exports=java.base/sun.nio.ch=ALL-UNNAMED")
         }
diff --git 
a/spark/src/main/java/org/apache/calcite/adapter/spark/SparkHandlerImpl.java 
b/spark/src/main/java/org/apache/calcite/adapter/spark/SparkHandlerImpl.java
index 4cdb715ccc..6d129b149e 100644
--- a/spark/src/main/java/org/apache/calcite/adapter/spark/SparkHandlerImpl.java
+++ b/spark/src/main/java/org/apache/calcite/adapter/spark/SparkHandlerImpl.java
@@ -31,9 +31,14 @@
 import org.apache.spark.api.java.JavaSparkContext;
 
 import java.io.File;
+import java.io.IOException;
 import java.io.Serializable;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.PosixFilePermissions;
 import java.util.Calendar;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -55,13 +60,34 @@ private static class Holder {
     private static final SparkHandlerImpl INSTANCE = new SparkHandlerImpl();
   }
 
-  private static final File CLASS_DIR = new File("build/sparkServer/classes");
+  private static final File CLASS_DIR = createClassDir();
+
+  @SuppressWarnings("java:S5443")
+  // Rule S5443 (temporary files in publicly writable directories) reports
+  // this call because it cannot prove, from the conditional FileAttribute
+  // argument, that the created directory receives owner-only permissions.
+  // It does on POSIX (rwx------ passed atomically at creation) and on
+  // Windows the JDK's default temp directory ACLs are per-user for
+  // interactive sessions; deployments running as SYSTEM or overriding
+  // TMP to a shared path are the operator's to secure.
+  private static File createClassDir() {
+    try {
+      final FileAttribute<?>[] attrs =
+          
FileSystems.getDefault().supportedFileAttributeViews().contains("posix")
+              ? new FileAttribute<?>[] {
+                  PosixFilePermissions.asFileAttribute(
+                      PosixFilePermissions.fromString("rwx------"))
+              }
+              : new FileAttribute<?>[0];
+      return Files.createTempDirectory("calcite-spark-classes", 
attrs).toFile();
+    } catch (IOException e) {
+      throw new IllegalStateException(
+          "Unable to create temporary folder for the Spark class server", e);
+    }
+  }
 
   /** Creates a SparkHandlerImpl. */
   private SparkHandlerImpl() {
-    if (!CLASS_DIR.isDirectory() && !CLASS_DIR.mkdirs()) {
-      System.err.println("Unable to create temporary folder " + CLASS_DIR);
-    }
     classServer = new HttpServer(CLASS_DIR);
 
     // Start the classServer and store its URI in a spark system property

Reply via email to