You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by ho...@apache.org on 2026/09/01 06:44:41 UTC
(spark) branch branch-4.x updated: [MINOR] Improve Transport Context Stream Manager Delegation
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.x by this push:
new 0356335660cf [MINOR] Improve Transport Context Stream Manager Delegation
0356335660cf is described below
commit 0356335660cfb43c63e32f426772552cdb5678a0
Author: Holden Karau <ho...@snowflake.com>
AuthorDate: Sat Aug 29 00:44:53 2026 +0000
[MINOR] Improve Transport Context Stream Manager Delegation
Co-authored-by: Holden Karau <ho...@pigscanfly.ca>
(cherry picked from commit 3b47eee2570ac130ff35c09ec205304df691f237)
---
.../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 95fde677624f..bb806a9c5371 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
@@ -109,4 +112,82 @@ public abstract class AbstractAuthRpcHandler extends RpcHandler {
public MergedBlockMetaReqHandler getMergedBlockMetaReqHandler() {
return delegate.getMergedBlockMetaReqHandler();
}
+
+ /**
+ * 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: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org