-
Notifications
You must be signed in to change notification settings - Fork 29.4k
[SPARK-59636][CORE] Fix integer overflow in GcmTransportCipher #58909
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,7 +39,8 @@ | |
|
|
||
| public class GcmTransportCipher implements TransportCipher { | ||
| private static final String HKDF_ALG = "HmacSha256"; | ||
| private static final int LENGTH_HEADER_BYTES = 8; | ||
| @VisibleForTesting | ||
| static final int LENGTH_HEADER_BYTES = 8; | ||
| @VisibleForTesting | ||
| static final int CIPHERTEXT_BUFFER_SIZE = 32 * 1024; // 32KB | ||
| // Maximum plaintext bytes to accumulate before flushing to downstream handlers, even | ||
|
|
@@ -212,7 +213,7 @@ public boolean release(int decrement) { | |
|
|
||
| @Override | ||
| public long transferTo(WritableByteChannel target, long position) throws IOException { | ||
| int transferredThisCall = 0; | ||
| long transferredThisCall = 0; | ||
| // If the header has is not empty, try to write it out to the target. | ||
| if (headerByteBuffer.hasRemaining()) { | ||
| int written = target.write(headerByteBuffer); | ||
|
|
@@ -369,8 +370,9 @@ private boolean initializeExpectedLength(ByteBuf ciphertextNettyBuf) { | |
| } | ||
| expectedLengthBuffer.flip(); | ||
| expectedLength = expectedLengthBuffer.getLong(); | ||
| if (expectedLength < 0) { | ||
| throw new IllegalStateException("Invalid expected ciphertext length."); | ||
| if (expectedLength < LENGTH_HEADER_BYTES + (long) headerLength) { | ||
| throw new IllegalStateException( | ||
| "Invalid expected ciphertext length: " + expectedLength); | ||
| } | ||
| ciphertextRead += LENGTH_HEADER_BYTES; | ||
| } | ||
|
|
@@ -442,8 +444,13 @@ public void channelRead(ChannelHandlerContext ctx, Object ciphertextMessage) | |
| int readableBytes = Math.min( | ||
| nettyBufReadableBytes, | ||
| ciphertextBuffer.remaining()); | ||
| int expectedRemaining = (int) (expectedLength - ciphertextRead); | ||
| int bytesToRead = Math.min(readableBytes, expectedRemaining); | ||
| long expectedRemaining = expectedLength - ciphertextRead; | ||
| if (expectedRemaining <= 0) { | ||
| throw new IllegalStateException( | ||
| "Invalid ciphertext state: expectedLength=" + expectedLength | ||
| + ", ciphertextRead=" + ciphertextRead); | ||
| } | ||
|
Comment on lines
+448
to
+452
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this reachable? With the new lower-bound check in |
||
| int bytesToRead = (int) Math.min((long) readableBytes, expectedRemaining); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit. |
||
| // The smallest ciphertext size is 16 bytes for the auth tag | ||
| ciphertextBuffer.limit(ciphertextBuffer.position() + bytesToRead); | ||
| ciphertextNettyBuf.readBytes(ciphertextBuffer); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,9 @@ | |
|
|
||
| package org.apache.spark.network.crypto; | ||
|
|
||
| import com.google.common.primitives.Longs; | ||
| import com.google.crypto.tink.subtle.AesGcmHkdfStreaming; | ||
| import com.google.crypto.tink.subtle.StreamSegmentEncrypter; | ||
| import io.netty.buffer.ByteBuf; | ||
| import io.netty.buffer.Unpooled; | ||
| import io.netty.channel.ChannelHandlerContext; | ||
|
|
@@ -570,6 +573,58 @@ public void testSplitLengthPrefix() throws Exception { | |
| } | ||
| } | ||
|
|
||
| @Test | ||
| public void testCiphertextLengthLargerThanMaxInt() throws Exception { | ||
| TransportConf gcmConf = getConf(2, false); | ||
| try (AuthEngine client = new AuthEngine("appId", "secret", gcmConf); | ||
| AuthEngine server = new AuthEngine("appId", "secret", gcmConf)) { | ||
| AuthMessage clientChallenge = client.challenge(); | ||
| AuthMessage serverResponse = server.response(clientChallenge); | ||
| client.deriveSessionCipher(clientChallenge, serverResponse); | ||
| GcmTransportCipher cipher = (GcmTransportCipher) server.sessionCipher(); | ||
| GcmTransportCipher.DecryptionHandler decryptionHandler = cipher.getDecryptionHandler(); | ||
| AesGcmHkdfStreaming streaming = cipher.getAesGcmHkdfStreaming(); | ||
|
|
||
| long expectedLength = (long) GcmTransportCipher.LENGTH_HEADER_BYTES + | ||
| streaming.getHeaderLength() + Integer.MAX_VALUE + 1L; | ||
|
Comment on lines
+588
to
+589
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit. Could you add a comment explaining that the remaining ciphertext length is |
||
| StreamSegmentEncrypter encrypter = streaming.newStreamSegmentEncrypter( | ||
| Longs.toByteArray(expectedLength)); | ||
| ByteBuffer header = encrypter.getHeader(); | ||
| ByteBuf ciphertext = Unpooled.buffer(GcmTransportCipher.LENGTH_HEADER_BYTES + | ||
| header.remaining() + 1) | ||
| .writeLong(expectedLength) | ||
| .writeBytes(header) | ||
| .writeByte(0); | ||
| ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); | ||
|
|
||
| decryptionHandler.channelRead(ctx, ciphertext); | ||
|
|
||
| verify(ctx, never()).fireChannelRead(any()); | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| public void testInvalidExpectedCiphertextLength() throws Exception { | ||
| TransportConf gcmConf = getConf(2, false); | ||
| try (AuthEngine client = new AuthEngine("appId", "secret", gcmConf); | ||
| AuthEngine server = new AuthEngine("appId", "secret", gcmConf)) { | ||
| AuthMessage clientChallenge = client.challenge(); | ||
| AuthMessage serverResponse = server.response(clientChallenge); | ||
| client.deriveSessionCipher(clientChallenge, serverResponse); | ||
| GcmTransportCipher cipher = (GcmTransportCipher) server.sessionCipher(); | ||
| GcmTransportCipher.DecryptionHandler decryptionHandler = cipher.getDecryptionHandler(); | ||
| long invalidLength = (long) GcmTransportCipher.LENGTH_HEADER_BYTES + | ||
| cipher.getAesGcmHkdfStreaming().getHeaderLength() - 1; | ||
| ByteBuf ciphertext = Unpooled.buffer(8).writeLong(invalidLength); | ||
|
|
||
| IllegalStateException error = assertThrows( | ||
| IllegalStateException.class, | ||
| () -> decryptionHandler.channelRead(mock(ChannelHandlerContext.class), ciphertext)); | ||
|
|
||
| assertEquals("Invalid expected ciphertext length: " + invalidLength, error.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Regression test for the encryptedCount miscalculation that caused shuffle fetch stalls | ||
| * for plaintext sizes in (plaintextSegmentSize - getCiphertextOffset(), plaintextSegmentSize] | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit.
(long)looks unnecessary here because both operands are smallintvalues.