sunchao commented on code in PR #57525:
URL: https://github.com/apache/spark/pull/57525#discussion_r3706641145
##########
core/src/main/scala/org/apache/spark/scheduler/cluster/CoarseGrainedSchedulerBackend.scala:
##########
@@ -140,6 +140,20 @@ class CoarseGrainedSchedulerBackend(scheduler:
TaskSchedulerImpl, val rpcEnv: Rp
ThreadUtils.newDaemonSingleThreadScheduledExecutor("cleanup-decommission-execs")
}
+ private def appIdMismatch(executorAppId: Option[String]):
Option[SparkException] = {
+ val resolvedDriverAppId = this.realApplicationId()
+ val execAppId = executorAppId.filter(_.nonEmpty)
+ (execAppId, resolvedDriverAppId) match {
+ case (Some(exec), Some(driver)) if exec != driver =>
Review Comment:
[P1] Do not treat reusable application IDs as unique application-instance
identities
This comparison assumes that equal application IDs identify the same
driver/application instance, but Spark Standalone explicitly supports
`spark.master.useAppNameAsAppId.enabled=true`. With that documented option,
`Master.createApplication` derives the ID only from the normalized application
name, so successive runs of the same job receive the same nonempty ID;
configurable application-ID patterns and modulo can also repeat IDs. If an
executor from the old run connects after a replacement driver for the same job
binds the released address, both the bootstrap check and the registration check
accept it, exposing the replacement driver's encryption key/delegation tokens
and permitting wrong-instance execution. Please bind the checks to a
per-application-instance/driver nonce, or explicitly reject configurations that
cannot provide unique IDs, and add a same-name driver-swap regression.
##########
core/src/main/scala/org/apache/spark/scheduler/cluster/CoarseGrainedSchedulerBackend.scala:
##########
@@ -353,14 +393,37 @@ class CoarseGrainedSchedulerBackend(scheduler:
TaskSchedulerImpl, val rpcEnv: Rp
triggeredByExecutor = true))
case RetrieveSparkAppConfig(resourceProfileId) =>
- val rp =
scheduler.sc.resourceProfileManager.resourceProfileFromId(resourceProfileId)
- val reply = SparkAppConfig(
- sparkProperties,
- SparkEnv.get.securityManager.getIOEncryptionKey(),
- Option(delegationTokens.get()),
- rp,
- currentLogLevel)
- context.reply(reply)
+ if (!SparkEnv.get.securityManager.isAuthenticationEnabled()) {
+ // External callers that still use the original one-field message do
+ // not supply an appId for validation. Only return credentials (I/O
+ // encryption key, delegation tokens) when RPC authentication is
+ // enabled -- otherwise the identity is unverifiable and credential
+ // disclosure to a wrong driver is possible.
+ context.sendFailure(new SparkException("Executor did not supply an
application ID " +
+ "and RPC authentication is not enabled (spark.authenticate=false).
" +
+ "RPC authentication must be enabled for this legacy request."))
+ } else {
+ replySparkAppConfig(resourceProfileId, context)
Review Comment:
[P1] Require application-scoped authentication before returning legacy
bootstrap credentials
This branch treats `spark.authenticate=true` as proof that the caller
belongs to this application and returns `SparkAppConfig`, including the I/O
encryption key and Hadoop delegation credentials, without validating any
application ID. However, `docs/security.md:44-50` explicitly states that
standalone and other deployments can share the same configured
`spark.authenticate.secret` across all applications and daemons;
`SecurityManager.getSecretKey(appId)` also ignores `appId`. Consequently, an
executor from application A can authenticate to replacement driver B with their
shared secret, send the legacy `RetrieveSparkAppConfig`, and obtain B's
bootstrap secrets. RayDP still uses that exact legacy request, and the new
authenticated-legacy test currently asserts this vulnerable behavior. Please
require application identity for credential-bearing legacy requests unless
authentication is demonstrably scoped to one application, and add a
cross-application shared-secret regression test.
##########
core/src/main/scala/org/apache/spark/internal/config/package.scala:
##########
@@ -2695,6 +2695,20 @@ package object config {
.booleanConf
.createWithDefault(false)
+ private[spark] val EXECUTOR_IDENTITY_VERIFICATION_ENABLED =
+ ConfigBuilder("spark.executor.identityVerification.enabled")
+ .doc("When enabled, the driver validates the executor's application ID
before " +
+ "returning bootstrap credentials (I/O encryption key, Hadoop
delegation " +
+ "tokens) and before accepting executor registration. This prevents an
" +
+ "executor from connecting to the wrong driver in a port-reuse
scenario. " +
+ "External cluster managers that do not supply an application ID in the
" +
+ "identity-carrying RPC messages can set this to false for backward " +
+ "compatibility. The legacy bootstrap RPC always requires RPC " +
+ "authentication (spark.authenticate) to return credentials.")
+ .version("4.3.0")
Review Comment:
[P1] Declare the required binding policy for the new configuration
This new `ConfigBuilder` does not set a binding policy, so existing
`SparkConfigBindingPolicySuite.Config enforcement for bindingPolicy`
deterministically fails. The exact-head test-results check reports
`spark.executor.identityVerification.enabled` as its sole failing
configuration, and the Hive job is red for this reason. Since executor identity
verification does not affect SQL view/UDF/procedure resolution, add
`.withBindingPolicy(ConfigBindingPolicy.NOT_APPLICABLE)` before `.booleanConf`;
do not add the setting to the frozen exceptions list.
##########
core/src/main/scala/org/apache/spark/scheduler/cluster/CoarseGrainedSchedulerBackend.scala:
##########
@@ -352,15 +365,20 @@ class CoarseGrainedSchedulerBackend(scheduler:
TaskSchedulerImpl, val rpcEnv: Rp
adjustTargetNumExecutors = false,
triggeredByExecutor = true))
- case RetrieveSparkAppConfig(resourceProfileId) =>
- val rp =
scheduler.sc.resourceProfileManager.resourceProfileFromId(resourceProfileId)
- val reply = SparkAppConfig(
- sparkProperties,
- SparkEnv.get.securityManager.getIOEncryptionKey(),
- Option(delegationTokens.get()),
- rp,
- currentLogLevel)
- context.reply(reply)
+ case RetrieveSparkAppConfig(resourceProfileId, appId) =>
+ // Validate identity before returning bootstrap credentials.
+ appIdMismatch(Option(appId)) match {
Review Comment:
[P1] This issue remains partially unfixed on
`d47091b0f25ac40cbf9c7a0fb353b17cb9d5d726`: the new code rejects missing or
empty application IDs for `RetrieveSparkAppConfigWithIdentity`, but
`RegisterExecutor` still calls
`appIdMismatch(attributes.get(EXECUTOR_APP_ID_ATTR))`. An absent attribute
becomes `None`; an empty attribute is filtered to `None`; and the wildcard
branch reports no mismatch, including when the driver's application ID is
unavailable. The new `accept RegisterExecutor without appId in attributes for
backward compatibility` test explicitly proves that registration succeeds with
verification enabled and RPC authentication disabled. Thus a legacy/external
executor can still register with the wrong driver after a post-bootstrap port
swap, bypassing the second protection layer. Please reject missing/empty
registration identities unless the connection has genuinely application-scoped
authentication, and change the acceptance test into a rejection regression.
##########
core/src/main/scala/org/apache/spark/scheduler/cluster/CoarseGrainedSchedulerBackend.scala:
##########
@@ -247,8 +247,12 @@ class CoarseGrainedSchedulerBackend(scheduler:
TaskSchedulerImpl, val rpcEnv: Rp
override def receiveAndReply(context: RpcCallContext):
PartialFunction[Any, Unit] = {
case RegisterExecutor(executorId, executorRef, hostname, cores, logUrls,
- attributes, resources, resourceProfileId) =>
- if (executorDataMap.contains(executorId)) {
+ attributes, resources, resourceProfileId, appId) =>
+ if (Option(appId).exists(_ != scheduler.applicationId())) {
Review Comment:
[P1] This race still survives the new retry commit
`d47091b0f25ac40cbf9c7a0fb353b17cb9d5d726`. Both bootstrap and registration
hard-code `MAX_APP_ID_RETRIES = 3` with `APP_ID_RETRY_DELAY_MS = 100`, so an
executor gives up after approximately 300 ms. The standalone master sends
`RegisteredApplication` asynchronously and immediately schedules executors;
`StandaloneAppClient` processes that callback on the shared RPC dispatcher,
while the driver endpoint rejecting executor requests runs on a dedicated loop.
Shared-dispatcher backlog, network delay, or a JVM pause can therefore keep the
driver's ID unpublished past all four attempts even though
`StandaloneAppClient` itself allows 20-second registration windows. The
resulting executor exits still count toward `spark.deploy.maxExecutorRetries`,
allowing a startup burst to fail the whole application. The added tests cover
only one immediately recovered rejection. Please wait for actual driver
readiness or use a configurable startup/RPC-s
cale timeout, and add a regression that delays ID publication beyond 300 ms.
--
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]