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

viirya pushed a commit to branch branch-4.0
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.0 by this push:
     new 5e5d40f5698c [MINOR][4.0][CORE] Improve Transport Context Stream 
Manager Delegation
5e5d40f5698c is described below

commit 5e5d40f5698ccd903bdabf4741b5304044428c2a
Author: Liang-Chi Hsieh <[email protected]>
AuthorDate: Tue Oct 27 22:16:17 2026 -0700

    [MINOR][4.0][CORE] Improve Transport Context Stream Manager Delegation
    
    ### What changes were proposed in this pull request?
    
    Backport of 3b47eee2570ac130ff35c09ec205304df691f237, which was missed on 
branch-4.0.
    
    ### Why are the changes needed?
    
    Missed backport.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Same as the original commit.
    
    ### How was this patch tested?
    
    Existing tests.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    No.
    
    Closes #59850 from viirya/backport-3b47eee-branch-4.0.
    
    Lead-authored-by: Liang-Chi Hsieh <[email protected]>
    Co-authored-by: Holden Karau <[email protected]>
    Signed-off-by: Liang-Chi Hsieh <[email protected]>
---
 .../org/apache/spark/network/TransportContext.java |  5 +-
 .../network/server/AbstractAuthRpcHandler.java     | 83 +++++++++++++++++-
 .../server/AbstractAuthRpcHandlerSuite.java        | 98 ++++++++++++++++++++++
 3 files changed, 184 insertions(+), 2 deletions(-)

diff --git 
a/common/network-common/src/main/java/org/apache/spark/network/TransportContext.java
 
b/common/network-common/src/main/java/org/apache/spark/network/TransportContext.java
index e8ce6840e3fc..2b99e76b49a1 100644
--- 
a/common/network-common/src/main/java/org/apache/spark/network/TransportContext.java
+++ 
b/common/network-common/src/main/java/org/apache/spark/network/TransportContext.java
@@ -237,8 +237,11 @@ public class TransportContext implements Closeable {
         .addLast("handler", channelHandler);
       // Use a separate EventLoopGroup to handle ChunkFetchRequest messages 
for shuffle rpcs.
       if (chunkFetchWorkers != null) {
+        // Use the per-channel handler (which may be wrapped by an 
authentication bootstrap),
+        // not the context-level one, so chunk fetches also fail closed until 
the channel has
+        // authenticated.
         ChunkFetchRequestHandler chunkFetchHandler = new 
ChunkFetchRequestHandler(
-          channelHandler.getClient(), rpcHandler.getStreamManager(),
+          channelHandler.getClient(), channelRpcHandler.getStreamManager(),
           conf.maxChunksBeingTransferred(), true /* syncModeEnabled */);
         pipeline.addLast(chunkFetchWorkers, "chunkFetchHandler", 
chunkFetchHandler);
       }
diff --git 
a/common/network-common/src/main/java/org/apache/spark/network/server/AbstractAuthRpcHandler.java
 
b/common/network-common/src/main/java/org/apache/spark/network/server/AbstractAuthRpcHandler.java
index 6914a3f03eef..1798814f3a2a 100644
--- 
a/common/network-common/src/main/java/org/apache/spark/network/server/AbstractAuthRpcHandler.java
+++ 
b/common/network-common/src/main/java/org/apache/spark/network/server/AbstractAuthRpcHandler.java
@@ -19,6 +19,9 @@ package org.apache.spark.network.server;
 
 import java.nio.ByteBuffer;
 
+import io.netty.channel.Channel;
+
+import org.apache.spark.network.buffer.ManagedBuffer;
 import org.apache.spark.network.client.RpcResponseCallback;
 import org.apache.spark.network.client.StreamCallbackWithID;
 import org.apache.spark.network.client.TransportClient;
@@ -83,7 +86,7 @@ public abstract class AbstractAuthRpcHandler extends 
RpcHandler {
 
   @Override
   public StreamManager getStreamManager() {
-    return delegate.getStreamManager();
+    return new AuthCheckingStreamManager(delegate.getStreamManager());
   }
 
   @Override
@@ -117,4 +120,82 @@ public abstract class AbstractAuthRpcHandler extends 
RpcHandler {
       }
     };
   }
+
+  /**
+   * Wraps the delegate's StreamManager so that no chunk or stream is served 
on a channel that
+   * has not completed authentication. Historically only receive() and 
receiveStream() were
+   * gated on authentication, which left StreamRequest and ChunkFetchRequest 
served pre-auth by
+   * StreamManagers whose checkAuthorization is a no-op (e.g. the NettyRpcEnv 
file server that
+   * distributes jars, files and REPL classes), so enabling spark.authenticate 
did not protect
+   * the file-distribution channel. This wrapper makes every StreamManager 
behind an
+   * authentication bootstrap fail closed instead. Lifecycle and accounting 
callbacks are
+   * always delegated so per-channel state is cleaned up regardless of 
authentication state.
+   */
+  private class AuthCheckingStreamManager extends StreamManager {
+    private final StreamManager delegate;
+
+    AuthCheckingStreamManager(StreamManager delegate) {
+      this.delegate = delegate;
+    }
+
+    private void checkAuthenticated() {
+      if (!isAuthenticated) {
+        throw new SecurityException("Unauthenticated call to stream manager.");
+      }
+    }
+
+    @Override
+    public ManagedBuffer getChunk(long streamId, int chunkIndex) {
+      checkAuthenticated();
+      return delegate.getChunk(streamId, chunkIndex);
+    }
+
+    @Override
+    public ManagedBuffer openStream(String streamId) {
+      checkAuthenticated();
+      return delegate.openStream(streamId);
+    }
+
+    @Override
+    public void checkAuthorization(TransportClient client, long streamId) {
+      checkAuthenticated();
+      delegate.checkAuthorization(client, streamId);
+    }
+
+    @Override
+    public void checkAuthorization(TransportClient client, String streamId) {
+      checkAuthenticated();
+      delegate.checkAuthorization(client, streamId);
+    }
+
+    @Override
+    public void connectionTerminated(Channel channel) {
+      delegate.connectionTerminated(channel);
+    }
+
+    @Override
+    public long chunksBeingTransferred() {
+      return delegate.chunksBeingTransferred();
+    }
+
+    @Override
+    public void chunkBeingSent(long streamId) {
+      delegate.chunkBeingSent(streamId);
+    }
+
+    @Override
+    public void streamBeingSent(String streamId) {
+      delegate.streamBeingSent(streamId);
+    }
+
+    @Override
+    public void chunkSent(long streamId) {
+      delegate.chunkSent(streamId);
+    }
+
+    @Override
+    public void streamSent(String streamId) {
+      delegate.streamSent(streamId);
+    }
+  }
 }
diff --git 
a/common/network-common/src/test/java/org/apache/spark/network/server/AbstractAuthRpcHandlerSuite.java
 
b/common/network-common/src/test/java/org/apache/spark/network/server/AbstractAuthRpcHandlerSuite.java
new file mode 100644
index 000000000000..385c584b3e03
--- /dev/null
+++ 
b/common/network-common/src/test/java/org/apache/spark/network/server/AbstractAuthRpcHandlerSuite.java
@@ -0,0 +1,98 @@
+/*
+ * 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.spark.network.server;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.nio.ByteBuffer;
+
+import io.netty.channel.Channel;
+import org.junit.jupiter.api.Test;
+
+import org.apache.spark.network.client.RpcResponseCallback;
+import org.apache.spark.network.client.TransportClient;
+
+/**
+ * Tests that RPC handlers behind an authentication bootstrap fail closed: no 
chunk or stream
+ * request may be served on a channel that has not completed authentication, 
regardless of how
+ * lax the delegate StreamManager's own authorization checks are (the default
+ * StreamManager.checkAuthorization is a no-op).
+ */
+public class AbstractAuthRpcHandlerSuite {
+
+  private static class TestAuthRpcHandler extends AbstractAuthRpcHandler {
+    TestAuthRpcHandler(RpcHandler delegate) {
+      super(delegate);
+    }
+
+    @Override
+    protected boolean doAuthChallenge(
+        TransportClient client,
+        ByteBuffer message,
+        RpcResponseCallback callback) {
+      return true;
+    }
+  }
+
+  @Test
+  public void testStreamManagerFailsClosedBeforeAuth() {
+    RpcHandler delegate = mock(RpcHandler.class);
+    StreamManager delegateManager = mock(StreamManager.class);
+    when(delegate.getStreamManager()).thenReturn(delegateManager);
+    AbstractAuthRpcHandler handler = new TestAuthRpcHandler(delegate);
+
+    StreamManager sm = handler.getStreamManager();
+    TransportClient client = mock(TransportClient.class);
+
+    assertThrows(SecurityException.class, () -> 
sm.openStream("/jars/app.jar"));
+    assertThrows(SecurityException.class, () -> sm.getChunk(0L, 0));
+    assertThrows(SecurityException.class, () -> sm.checkAuthorization(client, 
0L));
+    assertThrows(SecurityException.class, () -> sm.checkAuthorization(client, 
"/jars/app.jar"));
+    verify(delegateManager, never()).openStream(anyString());
+    verify(delegateManager, never()).getChunk(anyLong(), anyInt());
+
+    // Lifecycle callbacks still reach the delegate so per-channel state can 
be cleaned up
+    // even for channels that never authenticated.
+    Channel channel = mock(Channel.class);
+    sm.connectionTerminated(channel);
+    verify(delegateManager).connectionTerminated(channel);
+  }
+
+  @Test
+  public void testStreamManagerDelegatesAfterAuth() {
+    RpcHandler delegate = mock(RpcHandler.class);
+    StreamManager delegateManager = mock(StreamManager.class);
+    when(delegate.getStreamManager()).thenReturn(delegateManager);
+    AbstractAuthRpcHandler handler = new TestAuthRpcHandler(delegate);
+    StreamManager sm = handler.getStreamManager();
+
+    // Complete the (test) auth handshake; the wrapper obtained pre-auth must 
observe it.
+    handler.receive(mock(TransportClient.class), ByteBuffer.allocate(0),
+      mock(RpcResponseCallback.class));
+    assertTrue(handler.isAuthenticated());
+
+    sm.openStream("/jars/app.jar");
+    verify(delegateManager).openStream("/jars/app.jar");
+    sm.getChunk(1L, 2);
+    verify(delegateManager).getChunk(1L, 2);
+    TransportClient client = mock(TransportClient.class);
+    sm.checkAuthorization(client, 1L);
+    verify(delegateManager).checkAuthorization(client, 1L);
+  }
+}


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

Reply via email to