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

SteNicholas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/celeborn.git


The following commit(s) were added to refs/heads/main by this push:
     new d77543ef4b [CELEBORN-2382] Authorize `PbReviseLostShuffles` to prevent 
cross-application metadata modification
d77543ef4b is described below

commit d77543ef4bbd3a62aa8f247b6434e7c438d3850a
Author: Kalvin2077 <[email protected]>
AuthorDate: Wed Jul 15 17:56:32 2026 +0800

    [CELEBORN-2382] Authorize `PbReviseLostShuffles` to prevent 
cross-application metadata modification
    
    ### What changes were proposed in this pull request?
    
    Call `checkAuth(context, pb.getAppId)` before serving 
`PbReviseLostShuffles` in the Master, consistent with the other 
application-scoped handlers.
    
    ### Why are the changes needed?
    
    The Master handled `PbReviseLostShuffles` without an authorization check. 
With authentication enabled, an application could specify another application's 
ID and add shuffle IDs to its `registeredAppAndShuffles` metadata.
    
    ### Does this PR resolve a correctness bug?
    
    - [ ] Yes
    
    ### Does this PR introduce _any_ user-facing change?
    
    - [ ] Yes
    
    ### How was this patch tested?
    
    - New case in `MasterApplicationMetaAuthSuite` verifying same-application 
requests succeed and cross-application requests are rejected with 
authentication enabled
    - New case in `MasterSuite` verifying same-application requests succeed, 
cross-application requests are rejected, and authentication-disabled behavior 
is preserved.
    
    Closes #3761 from Kalvin2077/fix/pbrevise-checkauth.
    
    Authored-by: Kalvin2077 <[email protected]>
    Signed-off-by: Nicholas Jiang <[email protected]>
---
 .../celeborn/service/deploy/master/Master.scala    |  1 +
 .../master/MasterApplicationMetaAuthSuite.scala    | 35 +++++++++-
 .../service/deploy/master/MasterSuite.scala        | 79 ++++++++++++++++++----
 3 files changed, 99 insertions(+), 16 deletions(-)

diff --git 
a/master/src/main/scala/org/apache/celeborn/service/deploy/master/Master.scala 
b/master/src/main/scala/org/apache/celeborn/service/deploy/master/Master.scala
index 8960934dd3..4203041b74 100644
--- 
a/master/src/main/scala/org/apache/celeborn/service/deploy/master/Master.scala
+++ 
b/master/src/main/scala/org/apache/celeborn/service/deploy/master/Master.scala
@@ -599,6 +599,7 @@ private[celeborn] class Master(
         handleWorkerDecommission(context, workers, requestId))
 
     case pb: PbReviseLostShuffles =>
+      checkAuth(context, pb.getAppId)
       executeWithLeaderChecker(
         context,
         handleReviseLostShuffle(context, pb.getAppId, pb.getLostShufflesList, 
pb.getRequestId))
diff --git 
a/master/src/test/scala/org/apache/celeborn/service/deploy/master/MasterApplicationMetaAuthSuite.scala
 
b/master/src/test/scala/org/apache/celeborn/service/deploy/master/MasterApplicationMetaAuthSuite.scala
index b672851cb0..50662f0383 100644
--- 
a/master/src/test/scala/org/apache/celeborn/service/deploy/master/MasterApplicationMetaAuthSuite.scala
+++ 
b/master/src/test/scala/org/apache/celeborn/service/deploy/master/MasterApplicationMetaAuthSuite.scala
@@ -18,6 +18,7 @@
 package org.apache.celeborn.service.deploy.master
 
 import java.io.{PrintWriter, StringWriter}
+import java.util
 
 import scala.collection.mutable.ArrayBuffer
 
@@ -27,12 +28,13 @@ import org.scalatest.funsuite.AnyFunSuite
 import org.apache.celeborn.common.CelebornConf
 import org.apache.celeborn.common.metrics.source.Role
 import org.apache.celeborn.common.network.sasl.registration.RegistrationInfo
-import org.apache.celeborn.common.protocol.{PbApplicationMeta, 
PbApplicationMetaRequest, RpcNameConstants, TransportModuleConstants}
+import org.apache.celeborn.common.protocol.{PbApplicationMeta, 
PbApplicationMetaRequest, PbReviseLostShufflesResponse, RpcNameConstants, 
TransportModuleConstants}
+import 
org.apache.celeborn.common.protocol.message.ControlMessages.ReviseLostShuffles
 import org.apache.celeborn.common.rpc.{ClientSaslContextBuilder, RpcAddress, 
RpcEndpointRef, RpcEnv, RpcSecurityContextBuilder}
 
 /**
- * End-to-end authorization check for PbApplicationMetaRequest with auth 
enabled, driving
- * the real SASL registration path rather than a mocked client id (as in 
[[MasterSuite]]).
+ * End-to-end authorization checks for application-scoped Master RPCs with 
auth enabled,
+ * driving the real SASL registration path rather than a mocked client id (as 
in [[MasterSuite]]).
  *
  * It covers the whole chain the security guarantee rests on: registration 
sets the
  * connection's client id, and checkAuth enforces it. A regression that 
stopped setting
@@ -143,4 +145,31 @@ class MasterApplicationMetaAuthSuite extends AnyFunSuite
       
workerInternalRef().askSync[PbApplicationMeta](metaRequest(victimApp)).getSecret
         == victimSecret)
   }
+
+  test("PbReviseLostShuffles is authorized against the registered 
application") {
+    val victimApp = "revise-victim-app"
+    val attackerApp = "revise-attacker-app"
+
+    val victimRef = registeredAppRef(victimApp, "revise-victim-secret")
+    val ownResponse = victimRef.askSync[PbReviseLostShufflesResponse](
+      ReviseLostShuffles(victimApp, util.Arrays.asList[Integer](1), 
"victim-request"))
+    assert(ownResponse.getSuccess)
+
+    val victimShuffles = 
master.statusSystem.registeredAppAndShuffles.get(victimApp)
+    assert(victimShuffles.size() == 1)
+    assert(victimShuffles.contains(1))
+
+    val attackerRef = registeredAppRef(attackerApp, "revise-attacker-secret")
+    val attackerResponse = attackerRef.askSync[PbReviseLostShufflesResponse](
+      ReviseLostShuffles(attackerApp, util.Arrays.asList[Integer](2), 
"attacker-request"))
+    assert(attackerResponse.getSuccess)
+
+    val e = intercept[Exception] {
+      attackerRef.askSync[PbReviseLostShufflesResponse](
+        ReviseLostShuffles(victimApp, util.Arrays.asList[Integer](3), 
"cross-app-request"))
+    }
+    assert(stackTraceOf(e).contains(s"not authorized for application 
$victimApp"))
+    assert(victimShuffles.size() == 1)
+    assert(!victimShuffles.contains(3))
+  }
 }
diff --git 
a/master/src/test/scala/org/apache/celeborn/service/deploy/master/MasterSuite.scala
 
b/master/src/test/scala/org/apache/celeborn/service/deploy/master/MasterSuite.scala
index d915320c95..48ad9fe8d9 100644
--- 
a/master/src/test/scala/org/apache/celeborn/service/deploy/master/MasterSuite.scala
+++ 
b/master/src/test/scala/org/apache/celeborn/service/deploy/master/MasterSuite.scala
@@ -29,7 +29,7 @@ import org.apache.celeborn.common.CelebornConf
 import org.apache.celeborn.common.identity.UserIdentifier
 import org.apache.celeborn.common.network.client.{RpcResponseCallback, 
TransportClient}
 import org.apache.celeborn.common.protocol.{PbApplicationMetaRequest, 
PbCheckForWorkerTimeout, PbRegisterWorker}
-import 
org.apache.celeborn.common.protocol.message.ControlMessages.{RequestSlots, 
RequestSlotsResponse}
+import 
org.apache.celeborn.common.protocol.message.ControlMessages.{RequestSlots, 
RequestSlotsResponse, ReviseLostShuffles}
 import org.apache.celeborn.common.protocol.message.StatusCode
 import org.apache.celeborn.common.rpc.{RpcAddress, RpcCallContext}
 import org.apache.celeborn.common.rpc.netty.{NettyRpcEnv, 
RemoteNettyRpcCallContext}
@@ -40,6 +40,18 @@ class MasterSuite extends AnyFunSuite
   with BeforeAndAfterEach
   with MasterClusterFeature {
 
+  // Builds a remote call context whose connection is authenticated as 
`clientId`;
+  // null models a connection when authentication is disabled or an internal 
worker connection.
+  private def contextForClient(clientId: String): RemoteNettyRpcCallContext = {
+    val client = mock(classOf[TransportClient])
+    when(client.getClientId).thenReturn(clientId)
+    new RemoteNettyRpcCallContext(
+      mock(classOf[NettyRpcEnv]),
+      mock(classOf[RpcResponseCallback]),
+      RpcAddress("localhost", 1234),
+      client)
+  }
+
   def getTmpDir(): String = {
     val tmpDir = Files.createTempDirectory(null).toFile
     tmpDir.deleteOnExit()
@@ -212,18 +224,6 @@ class MasterSuite extends AnyFunSuite
     val masterArgs = new MasterArguments(args, conf)
     val master = new Master(conf, masterArgs)
 
-    // Builds a remote call context whose connection is authenticated as 
`clientId`;
-    // null models a worker on the internal channel, which sets no client id.
-    def contextForClient(clientId: String): RemoteNettyRpcCallContext = {
-      val client = mock(classOf[TransportClient])
-      when(client.getClientId).thenReturn(clientId)
-      new RemoteNettyRpcCallContext(
-        mock(classOf[NettyRpcEnv]),
-        mock(classOf[RpcResponseCallback]),
-        RpcAddress("localhost", 1234),
-        client)
-    }
-
     val request = 
PbApplicationMetaRequest.newBuilder().setAppId("victim-app").build()
     val unhandled = (_: Any) => fail("PbApplicationMetaRequest was not 
handled")
 
@@ -241,4 +241,57 @@ class MasterSuite extends AnyFunSuite
       master.rpcEnv.shutdown()
     }
   }
+
+  test("PbReviseLostShuffles authorizes application metadata changes") {
+    val conf = new CelebornConf()
+    val randomMasterPort = selectRandomPort()
+    val randomHttpPort = selectRandomPort()
+    conf.set(CelebornConf.HA_ENABLED.key, "false")
+    conf.set(CelebornConf.MASTER_HTTP_HOST.key, "127.0.0.1")
+    conf.set(CelebornConf.MASTER_HTTP_PORT.key, randomHttpPort.toString)
+
+    val args = Array("-h", "localhost", "-p", randomMasterPort.toString)
+    val masterArgs = new MasterArguments(args, conf)
+    val master = new Master(conf, masterArgs)
+
+    val victimApp = "revise-victim-app"
+    val ownRequest = ReviseLostShuffles(
+      victimApp,
+      util.Arrays.asList[Integer](1),
+      "own-request")
+    val crossAppRequest = ReviseLostShuffles(
+      victimApp,
+      util.Arrays.asList[Integer](2),
+      "cross-app-request")
+    val authDisabledApp = "auth-disabled-app"
+    val authDisabledRequest = ReviseLostShuffles(
+      authDisabledApp,
+      util.Arrays.asList[Integer](3),
+      "auth-disabled-request")
+    val unhandled = (_: Any) => fail("PbReviseLostShuffles was not handled")
+
+    try {
+      
master.receiveAndReply(contextForClient(victimApp)).applyOrElse(ownRequest, 
unhandled)
+      val victimShuffles = 
master.statusSystem.registeredAppAndShuffles.get(victimApp)
+      assert(victimShuffles.size() == 1)
+      assert(victimShuffles.contains(1))
+
+      val e = intercept[IllegalStateException] {
+        master.receiveAndReply(contextForClient("attacker-app"))
+          .applyOrElse(crossAppRequest, unhandled)
+      }
+      assert(e.getMessage.contains(s"not authorized for application 
$victimApp"))
+      assert(victimShuffles.size() == 1)
+      assert(!victimShuffles.contains(2))
+
+      // Authentication-disabled connections have no client id, so existing 
behavior is preserved.
+      
master.receiveAndReply(contextForClient(null)).applyOrElse(authDisabledRequest, 
unhandled)
+      val authDisabledShuffles =
+        master.statusSystem.registeredAppAndShuffles.get(authDisabledApp)
+      assert(authDisabledShuffles.size() == 1)
+      assert(authDisabledShuffles.contains(3))
+    } finally {
+      master.rpcEnv.shutdown()
+    }
+  }
 }

Reply via email to