Skip to content

Commit 6b48e69

Browse files
authored
SslHandler wrap reentry bug fix (#11133)
Motivation: SslHandler's wrap method notifies the handshakeFuture and sends a SslHandshakeCompletionEvent user event down the pipeline before writing the plaintext that has just been wrapped. It is possible the application may write as a result of these events and re-enter into wrap to write more data. This will result in out of sequence data and result in alerts such as SSLV3_ALERT_BAD_RECORD_MAC. Modifications: - SslHandler wrap should write any pending data before notifying promises, generating user events, or anything else that may create a re-entry scenario. Result: SslHandler will wrap/write data in the same order.
1 parent e4dd6ee commit 6b48e69

File tree

3 files changed

+179
-87
lines changed

3 files changed

+179
-87
lines changed

handler/src/main/java/io/netty/handler/ssl/SslHandler.java

Lines changed: 47 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -821,17 +821,14 @@ private void wrapAndFlush(ChannelHandlerContext ctx) throws SSLException {
821821
// This method will not call setHandshakeFailure(...) !
822822
private void wrap(ChannelHandlerContext ctx, boolean inUnwrap) throws SSLException {
823823
ByteBuf out = null;
824-
ChannelPromise promise = null;
825824
ByteBufAllocator alloc = ctx.alloc();
826-
boolean needUnwrap = false;
827-
ByteBuf buf = null;
828825
try {
829826
final int wrapDataSize = this.wrapDataSize;
830827
// Only continue to loop if the handler was not removed in the meantime.
831828
// See https://github.com/netty/netty/issues/5860
832829
outer: while (!ctx.isRemoved()) {
833-
promise = ctx.newPromise();
834-
buf = wrapDataSize > 0 ?
830+
ChannelPromise promise = ctx.newPromise();
831+
ByteBuf buf = wrapDataSize > 0 ?
835832
pendingUnencryptedWrites.remove(alloc, wrapDataSize, promise) :
836833
pendingUnencryptedWrites.removeFirst(promise);
837834
if (buf == null) {
@@ -844,9 +841,31 @@ private void wrap(ChannelHandlerContext ctx, boolean inUnwrap) throws SSLExcepti
844841

845842
SSLEngineResult result = wrap(alloc, engine, buf, out);
846843

847-
if (result.getStatus() == Status.CLOSED) {
844+
if (buf.isReadable()) {
845+
pendingUnencryptedWrites.addFirst(buf, promise);
846+
// When we add the buffer/promise pair back we need to be sure we don't complete the promise
847+
// later. We only complete the promise if the buffer is completely consumed.
848+
promise = null;
849+
} else {
848850
buf.release();
849-
buf = null;
851+
}
852+
853+
// We need to write any data before we invoke any methods which may trigger re-entry, otherwise
854+
// writes may occur out of order and TLS sequencing may be off (e.g. SSLV3_ALERT_BAD_RECORD_MAC).
855+
if (out.isReadable()) {
856+
final ByteBuf b = out;
857+
out = null;
858+
if (promise != null) {
859+
ctx.write(b, promise);
860+
} else {
861+
ctx.write(b);
862+
}
863+
} else if (promise != null) {
864+
ctx.write(Unpooled.EMPTY_BUFFER, promise);
865+
}
866+
// else out is not readable we can re-use it and so save an extra allocation
867+
868+
if (result.getStatus() == Status.CLOSED) {
850869
// Make a best effort to preserve any exception that way previously encountered from the handshake
851870
// or the transport, else fallback to a general error.
852871
Throwable exception = handshakePromise.cause();
@@ -856,23 +875,9 @@ private void wrap(ChannelHandlerContext ctx, boolean inUnwrap) throws SSLExcepti
856875
exception = new SslClosedEngineException("SSLEngine closed already");
857876
}
858877
}
859-
promise.tryFailure(exception);
860-
promise = null;
861-
// SSLEngine has been closed already.
862-
// Any further write attempts should be denied.
863878
pendingUnencryptedWrites.releaseAndFailAll(ctx, exception);
864879
return;
865880
} else {
866-
if (buf.isReadable()) {
867-
pendingUnencryptedWrites.addFirst(buf, promise);
868-
// When we add the buffer/promise pair back we need to be sure we don't complete the promise
869-
// later in finishWrap. We only complete the promise if the buffer is completely consumed.
870-
promise = null;
871-
} else {
872-
buf.release();
873-
}
874-
buf = null;
875-
876881
switch (result.getHandshakeStatus()) {
877882
case NEED_TASK:
878883
if (!runDelegatedTasks(inUnwrap)) {
@@ -883,27 +888,11 @@ private void wrap(ChannelHandlerContext ctx, boolean inUnwrap) throws SSLExcepti
883888
break;
884889
case FINISHED:
885890
setHandshakeSuccess();
886-
// deliberate fall-through
891+
break;
887892
case NOT_HANDSHAKING:
888893
setHandshakeSuccessIfStillHandshaking();
889-
// deliberate fall-through
890-
case NEED_WRAP: {
891-
ChannelPromise p = promise;
892-
893-
// Null out the promise so it is not reused in the finally block in the cause of
894-
// finishWrap(...) throwing.
895-
promise = null;
896-
final ByteBuf b;
897-
898-
if (out.isReadable()) {
899-
// There is something in the out buffer. Ensure we null it out so it is not re-used.
900-
b = out;
901-
out = null;
902-
} else {
903-
// If out is not readable we can re-use it and so save an extra allocation
904-
b = null;
905-
}
906-
finishWrap(ctx, b, p, inUnwrap, false);
894+
break;
895+
case NEED_WRAP:
907896
// If we are expected to wrap again and we produced some data we need to ensure there
908897
// is something in the queue to process as otherwise we will not try again before there
909898
// was more added. Failing to do so may fail to produce an alert that can be
@@ -912,9 +901,10 @@ private void wrap(ChannelHandlerContext ctx, boolean inUnwrap) throws SSLExcepti
912901
pendingUnencryptedWrites.add(Unpooled.EMPTY_BUFFER);
913902
}
914903
break;
915-
}
916904
case NEED_UNWRAP:
917-
needUnwrap = true;
905+
// The underlying engine is starving so we need to feed it with more data.
906+
// See https://github.com/netty/netty/pull/5039
907+
readIfNeeded(ctx);
918908
return;
919909
default:
920910
throw new IllegalStateException(
@@ -923,37 +913,12 @@ private void wrap(ChannelHandlerContext ctx, boolean inUnwrap) throws SSLExcepti
923913
}
924914
}
925915
} finally {
926-
// Ownership of buffer was not transferred, release it.
927-
if (buf != null) {
928-
buf.release();
916+
if (out != null) {
917+
out.release();
918+
}
919+
if (inUnwrap) {
920+
needsFlush = true;
929921
}
930-
finishWrap(ctx, out, promise, inUnwrap, needUnwrap);
931-
}
932-
}
933-
934-
private void finishWrap(ChannelHandlerContext ctx, ByteBuf out, ChannelPromise promise, boolean inUnwrap,
935-
boolean needUnwrap) {
936-
if (out == null) {
937-
out = Unpooled.EMPTY_BUFFER;
938-
} else if (!out.isReadable()) {
939-
out.release();
940-
out = Unpooled.EMPTY_BUFFER;
941-
}
942-
943-
if (promise != null) {
944-
ctx.write(out, promise);
945-
} else {
946-
ctx.write(out);
947-
}
948-
949-
if (inUnwrap) {
950-
needsFlush = true;
951-
}
952-
953-
if (needUnwrap) {
954-
// The underlying engine is starving so we need to feed it with more data.
955-
// See https://github.com/netty/netty/pull/5039
956-
readIfNeeded(ctx);
957922
}
958923
}
959924

@@ -977,7 +942,6 @@ private boolean wrapNonAppData(final ChannelHandlerContext ctx, boolean inUnwrap
977942
out = allocateOutNetBuf(ctx, 2048, 1);
978943
}
979944
SSLEngineResult result = wrap(alloc, engine, Unpooled.EMPTY_BUFFER, out);
980-
HandshakeStatus status = result.getHandshakeStatus();
981945
if (result.bytesProduced() > 0) {
982946
ctx.write(out).addListener(new ChannelFutureListener() {
983947
@Override
@@ -989,21 +953,22 @@ public void operationComplete(ChannelFuture future) {
989953
}
990954
});
991955
if (inUnwrap) {
992-
// We may be here because we read data and discovered the remote peer initiated a renegotiation
993-
// and this write is to complete the new handshake. The user may have previously done a
994-
// writeAndFlush which wasn't able to wrap data due to needing the pending handshake, so we
995-
// attempt to wrap application data here if any is pending.
996-
if (status == HandshakeStatus.FINISHED && !pendingUnencryptedWrites.isEmpty()) {
997-
wrap(ctx, true);
998-
}
999956
needsFlush = true;
1000957
}
1001958
out = null;
1002959
}
1003960

961+
HandshakeStatus status = result.getHandshakeStatus();
1004962
switch (status) {
1005963
case FINISHED:
1006964
setHandshakeSuccess();
965+
// We may be here because we read data and discovered the remote peer initiated a renegotiation
966+
// and this write is to complete the new handshake. The user may have previously done a
967+
// writeAndFlush which wasn't able to wrap data due to needing the pending handshake, so we
968+
// attempt to wrap application data here if any is pending.
969+
if (inUnwrap && !pendingUnencryptedWrites.isEmpty()) {
970+
wrap(ctx, true);
971+
}
1007972
return false;
1008973
case NEED_TASK:
1009974
if (!runDelegatedTasks(inUnwrap)) {
@@ -1095,11 +1060,9 @@ private SSLEngineResult wrap(ByteBufAllocator alloc, SSLEngine engine, ByteBuf i
10951060
in.skipBytes(result.bytesConsumed());
10961061
out.writerIndex(out.writerIndex() + result.bytesProduced());
10971062

1098-
switch (result.getStatus()) {
1099-
case BUFFER_OVERFLOW:
1063+
if (result.getStatus() == Status.BUFFER_OVERFLOW) {
11001064
out.ensureWritable(engine.getSession().getPacketBufferSize());
1101-
break;
1102-
default:
1065+
} else {
11031066
return result;
11041067
}
11051068
}

handler/src/test/java/io/netty/handler/ssl/ApplicationProtocolNegotiationHandlerTest.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
*/
1616
package io.netty.handler.ssl;
1717

18+
import io.netty.buffer.ByteBuf;
1819
import io.netty.channel.ChannelHandler;
1920
import io.netty.channel.ChannelHandlerContext;
2021
import io.netty.channel.embedded.EmbeddedChannel;
2122
import io.netty.handler.codec.DecoderException;
23+
import io.netty.util.CharsetUtil;
2224
import org.junit.Test;
2325

2426
import javax.net.ssl.SSLContext;
@@ -66,13 +68,20 @@ protected void configurePipeline(ChannelHandlerContext ctx, String protocol) {
6668
};
6769

6870
SSLEngine engine = SSLContext.getDefault().createSSLEngine();
69-
engine.setUseClientMode(false);
71+
// This test is mocked/simulated and doesn't go through full TLS handshake. Currently only JDK SSLEngineImpl
72+
// client mode will generate a close_notify.
73+
engine.setUseClientMode(true);
7074

7175
EmbeddedChannel channel = new EmbeddedChannel(new SslHandler(engine), alpnHandler);
7276
channel.pipeline().fireUserEventTriggered(SslHandshakeCompletionEvent.SUCCESS);
7377
assertNull(channel.pipeline().context(alpnHandler));
7478
// Should produce the close_notify messages
75-
assertTrue(channel.finishAndReleaseAll());
79+
channel.releaseOutbound();
80+
channel.close();
81+
ByteBuf close_notify = channel.readOutbound();
82+
assertTrue("close_notify: " + close_notify.toString(CharsetUtil.UTF_8), close_notify.readableBytes() >= 7);
83+
close_notify.release();
84+
channel.finishAndReleaseAll();
7685
assertTrue(configureCalled.get());
7786
}
7887

0 commit comments

Comments
 (0)