Copilot commented on code in PR #12483:
URL: https://github.com/apache/gluten/pull/12483#discussion_r3557963963


##########
gluten-substrait/src/main/scala/org/apache/gluten/execution/BatchScanExecTransformer.scala:
##########
@@ -113,6 +113,8 @@ abstract class BatchScanExecTransformerBase(
     postDriverMetrics()
   }
 
+  override def readerOptions: Map[String, String] = Map.empty

Review Comment:
   `BatchScanExecTransformerBase` currently hard-codes `readerOptions` to 
`Map.empty`, which means DSv2 `FileScan.options` are never included in the 
merged `fsConf` (even though the comment in `WholeStageTransformer` mentions 
DSv2).
   
   If FS credentials are provided via `DataFrameReader.option(...)` for DSv2 
sources, they still won’t reach native. Consider wiring `FileScan.options` 
through `readerOptions` here.



##########
gluten-substrait/src/main/scala/org/apache/gluten/execution/GlutenWholeStageColumnarRDD.scala:
##########
@@ -35,6 +35,50 @@ trait BaseGlutenPartition extends Partition with 
InputPartition {
   def plan: Array[Byte]
 }
 
+/**
+ * Wraps Hadoop/Velox filesystem credential key-value pairs (fs.azure.*, 
fs.s3a.*, fs.gs.*) so they
+ * cannot be accidentally exposed through logging, toString, exception 
messages, or other debug
+ * paths that might print an arbitrary object.
+ *
+ * `toString` is deliberately overridden to redact values - only key NAMES are 
shown (these are not
+ * secret; only the bearer values like access keys, secret keys, and OAuth 
client secrets are). This
+ * makes accidental leaks structurally harder: a future `logDebug(s"... 
$fsConfHolder")` or similar
+ * call will print "FsCredentialConf(3 keys: [fs.azure.account.auth.type, 
...], values redacted)"
+ * instead of the literal secret strings.
+ *
+ * The underlying map is also `private` so it cannot be reached by name from 
outside this file
+ * without going through `.unsafeValue`, which is named to discourage casual 
use.
+ */
+final case class FsCredentialConf private (private val raw: Map[String, 
String]) {
+
+  /** Number of credential entries held. Safe to log. */
+  def size: Int = raw.size
+
+  def isEmpty: Boolean = raw.isEmpty
+  def nonEmpty: Boolean = raw.nonEmpty
+
+  /**
+   * Returns the underlying map with real values. Named `unsafeValue` to make 
call sites grep-able
+   * and to discourage passing the result to logging code. Only the native JNI 
boundary (extraConf
+   * for NativePlanEvaluator) should call this.
+   */
+  def unsafeValue: Map[String, String] = raw
+
+  override def toString: String = {
+    if (raw.isEmpty) {
+      "FsCredentialConf(empty)"
+    } else {
+      s"FsCredentialConf(${raw.size} keys: 
[${raw.keys.toSeq.sorted.mkString(", ")}], " +
+        "values redacted)"
+    }
+  }
+}

Review Comment:
   `FsCredentialConf` is declared as a **case class**, which means Scala will 
generate `unapply`/`Product` accessors that can expose the underlying `raw` map 
(including secret values) without going through `unsafeValue`. That undermines 
the stated goal of making value access grep-able and structurally harder to 
leak.
   
   Consider making this a plain `final class` (not a case class) so the only 
intentional escape hatch is `unsafeValue`, while keeping the redacting 
`toString`.



##########
gluten-arrow/src/main/scala/org/apache/gluten/runtime/Runtimes.scala:
##########
@@ -18,10 +18,48 @@ package org.apache.gluten.runtime
 
 import org.apache.spark.task.TaskResources
 
+import java.security.MessageDigest
 import java.util
 
 object Runtimes {
 
+  /**
+   * Produce a stable, value-free cache key for a (backendName, name, 
extraConf) triple.
+   *
+   * Two problems with the old `s"$backendName:$name:$extraConf"` key:
+   *
+   *   1. **Credential leakage** – `Map.toString` embeds secret values (e.g. 
`fs.s3a.secret.key`,
+   *      `fs.azure.account.oauth2.client.secret`) in a plain heap string that 
can appear in logs,
+   *      thread dumps, and heap snapshots.
+   *   2. **Nondeterminism** – Scala `Map.toString` does not guarantee 
insertion order, so two maps
+   *      with identical entries can produce different strings, causing 
spurious duplicate
+   *      `VeloxRuntime` registrations within a task.
+   *
+   * Fix: sort keys, hash them with SHA-256, and use only the hex digest as 
the key. Values are
+   * intentionally excluded from the digest – distinct configs (different 
credentials for the same
+   * key set) that need separate runtimes are already separated at the task 
level through
+   * `GlutenPartition.fsConf`. Within a single task the key set is stable, so 
the digest is stable.
+   */
+  private def stableKey(
+      backendName: String,
+      name: String,
+      extraConf: util.Map[String, String]): String = {
+    val digest = MessageDigest.getInstance("SHA-256")
+    digest.update(backendName.getBytes("UTF-8"))
+    digest.update(0.toByte) // field separator
+    digest.update(name.getBytes("UTF-8"))
+    digest.update(0.toByte)
+    // Sort keys for determinism; hash only keys, not values, to avoid leaking 
secrets.
+    val sortedKeys = new java.util.ArrayList(extraConf.keySet)
+    java.util.Collections.sort(sortedKeys)
+    sortedKeys.forEach {
+      k =>
+        digest.update(k.getBytes("UTF-8"))
+        digest.update(0.toByte)
+    }

Review Comment:
   `stableKey` hashes only the *keys* of `extraConf`. Since `RuntimeImpl` 
merges `extraConf` **values** into the serialized native config (see 
`Runtime.scala`), two different `extraConf` maps that share the same key set 
(but different values/credentials) will collide and can cause `TaskResources` 
to incorrectly reuse a `Runtime` configured with the wrong credentials/settings.
   
   Also, the scaladoc rationale references `GlutenPartition.fsConf`, but 
`GlutenPartition` does not carry `fsConf` in this PR (it’s stored on the RDD 
instead), so the “values intentionally excluded” justification is no longer 
accurate.
   
   Hash sorted (key,value) pairs so the cache key stays deterministic and does 
not embed secrets as a string, while still separating distinct configurations.



##########
gluten-substrait/src/main/scala/org/apache/gluten/execution/WholeStageTransformer.scala:
##########
@@ -367,6 +367,56 @@ case class WholeStageTransformer(child: SparkPlan, 
materializeInput: Boolean = f
         allSplitInfos,
         leafTransformers)
 
+    // Compute fsConf once here on the driver from the leaf transformers' 
session
+    // Hadoop configuration.  It is stored as a plain field on the RDD and
+    // serialized once per executor via the existing task closure - no 
per-partition
+    // copies, no Broadcast overhead (no BlockManager registration, no HTTP 
fetch,
+    // no master round-trip).  GlutenPartition never carries fsConf.
+    //
+    // Two sources merged (getPropsWithPrefix avoids full conf iteration):
+    //   1. sparkContext.hadoopConfiguration - spark.hadoop.* and 
sc.hadoopConf.set()
+    //   2. SQLConf - spark.conf.set("fs.*", ...) keys that live only in 
SQLConf
+    val fsConf: Map[String, String] = {
+      import scala.collection.JavaConverters._
+      val fsPrefixes = Seq("fs.azure.", "fs.s3a.", "fs.gs.")
+
+      // Source 1: sparkContext.hadoopConfiguration (spark.hadoop.* and 
sc.hadoopConf.set()).
+      // Read directly from the mutable base Configuration via 
getPropsWithPrefix
+      // rather than sessionState.newHadoopConf(), which would make a full copy
+      // of the entire Hadoop config (hundreds of entries from core-site.xml,
+      // hdfs-site.xml, etc.) just to look up a handful of fs.* keys.
+      // getPropsWithPrefix reads directly from the internal map and returns 
only
+      // matching entries - cost proportional to matches, not total config 
size.
+      // scalastyle:off hadoopconfiguration
+      val baseHadoopConf = sparkContext.hadoopConfiguration
+      // scalastyle:on hadoopconfiguration
+      val fromHadoop: Map[String, String] = fsPrefixes.flatMap {
+        prefix =>
+          baseHadoopConf.getPropsWithPrefix(prefix).asScala
+            .map { case (suffix, v) => (prefix + suffix) -> v }
+      }.toMap
+
+      // Source 2: SQLConf (spark.conf.set("fs.*", ...) at session level).
+      val fromSqlConf: Map[String, String] =
+        org.apache.spark.sql.internal.SQLConf.get.getAllConfs.filter {
+          case (k, _) => fsPrefixes.exists(k.startsWith)
+        }

Review Comment:
   `fromSqlConf` only picks up keys that start with `fs.azure.`, `fs.s3a.`, or 
`fs.gs.`. In Spark, session-level Hadoop FS configs are very commonly set as 
`spark.hadoop.fs.*` via `spark.conf.set(...)`; those will be missed here, so 
the PR still won’t pass many session-level FS settings to native.
   
   Consider normalizing `spark.hadoop.*` keys from `SQLConf` by stripping the 
prefix and then applying the same `fs.*` prefix filter, so both `fs.*` and 
`spark.hadoop.fs.*` session configs are propagated.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to