@@ -110,6 +110,8 @@ public class WolfSSLEngine extends SSLEngine {
110110
111111 /* session stored (WOLFSSL_SESSION), relevant on client side */
112112 private boolean sessionStored = false ;
113+ /* Skip record header peek in next unwrap, continue same TLS record */
114+ private boolean contPartialRecord = false ;
113115
114116 /* TLS 1.3 session ticket received (on client side) */
115117 private boolean sessionTicketReceived = false ;
@@ -142,11 +144,6 @@ public class WolfSSLEngine extends SSLEngine {
142144 * and expanded only when a larger output window requires it. */
143145 private byte [] recvAppDataBuf = new byte [WolfSSL .MAX_RECORD_SIZE ];
144146
145- /* TLS record header is: type(1) + version(2) + length(2) */
146- private static final int TLS_RECORD_HEADER_LEN = 5 ;
147- private static final int TLS_RECORD_LEN_HI_OFF = 3 ;
148- private static final int TLS_RECORD_LEN_LO_OFF = 4 ;
149-
150147 /* Default size of internalIOSendBuf, 16k to match TLS record size.
151148 * TODO - add upper bound on I/O send buf resize allocations. */
152149 private static final int INTERNAL_IOSEND_BUF_SZ = WolfSSL .MAX_RECORD_SIZE ;
@@ -365,7 +362,7 @@ private List<SNIServerName> parseRequestedServerNamesFromNetData() {
365362
366363 synchronized (netDataLock ) {
367364 if (this .netData == null ||
368- this .netData .remaining () < TLS_RECORD_HEADER_LEN ) {
365+ this .netData .remaining () < WolfSSL . TLS_RECORD_HEADER_LEN ) {
369366 return null ;
370367 }
371368 in = this .netData .asReadOnlyBuffer ();
@@ -1289,6 +1286,70 @@ private byte[] getRecvAppDataBuf(int minSz) {
12891286 return this .recvAppDataBuf ;
12901287 }
12911288
1289+ /**
1290+ * Peek at the record header and return BUFFER_UNDERFLOW before calling
1291+ * into JNI when the full record is not yet present. Without this, native
1292+ * wolfSSL consumes partial record bytes via the I/O callback, violating
1293+ * the JSSE contract that BUFFER_UNDERFLOW must report
1294+ * bytesConsumed() == 0.
1295+ *
1296+ * Skipped if using DTLS.
1297+ *
1298+ * @param in input buffer.
1299+ * @param inRemaining bytes remaining in input buffer.
1300+ *
1301+ * @return true if buffer underflow detected, false otherwise.
1302+ */
1303+ private boolean peekTlsRecordHeader (ByteBuffer in , int inRemaining ) {
1304+ boolean bufferUnderflow = false ;
1305+ /* DTLS still relies on the native WANT_READ path. */
1306+ if (inRemaining > 0 && (this .ssl .dtls () == 0 )) {
1307+ int pos = in .position ();
1308+ if (inRemaining < WolfSSL .TLS_RECORD_HEADER_LEN ) {
1309+ /* Not enough for TLS record header */
1310+ bufferUnderflow = true ;
1311+ }
1312+ else {
1313+ /* Check if header is plausible. Content type between
1314+ * change_cipher_spec and heartbeat and
1315+ * version major corresponds to TLS. */
1316+ byte headerByte = in .get (pos );
1317+ if ((headerByte & 0xFF ) >= WolfSSL .TLS_RECORD_CT_MIN &&
1318+ (headerByte & 0xFF ) <= WolfSSL .TLS_RECORD_CT_MAX &&
1319+ (in .get (pos + WolfSSL .TLS_RECORD_VERS_OFF )
1320+ & 0xFF ) == WolfSSL .TLS_RECORD_VERS_MAJOR ) {
1321+ /* Peek at record length from header
1322+ * bytes 3-4 (big-endian). */
1323+ int recLen =
1324+ ((in .get (pos + WolfSSL .TLS_RECORD_LEN_HI_OFF )
1325+ & 0xFF ) << 8 ) |
1326+ (in .get (pos + WolfSSL .TLS_RECORD_LEN_LO_OFF )
1327+ & 0xFF );
1328+ /* Fall through if recLen exceeds max packet buffer size
1329+ * or inRemaining is less than reported record length */
1330+ int packetBufSz = this .getSession ().getPacketBufferSize ();
1331+ if (recLen <= packetBufSz &&
1332+ inRemaining < WolfSSL .TLS_RECORD_HEADER_LEN + recLen ) {
1333+ bufferUnderflow = true ;
1334+ }
1335+ else if (recLen > packetBufSz ) {
1336+ WolfSSLDebug .log (getClass (), WolfSSLDebug .INFO ,
1337+ () -> "recLen: " + recLen +
1338+ " exceeds max packet buffer size, " +
1339+ "skipping underflow check." );
1340+ }
1341+ }
1342+ else {
1343+ WolfSSLDebug .log (getClass (), WolfSSLDebug .INFO ,
1344+ () -> "Peeked content-type or version " +
1345+ "major outside TLS range, skipping " +
1346+ "underflow check" );
1347+ }
1348+ }
1349+ }
1350+ return bufferUnderflow ;
1351+ }
1352+
12921353 @ Override
12931354 public synchronized SSLEngineResult unwrap (ByteBuffer in , ByteBuffer out )
12941355 throws SSLException {
@@ -1313,6 +1374,7 @@ public synchronized SSLEngineResult unwrap(ByteBuffer in, ByteBuffer[] out,
13131374 long dtlsPrevDropCount = 0 ;
13141375 long dtlsCurrDropCount = 0 ;
13151376 int prevSessionTicketCount = 0 ;
1377+ boolean bufferUnderflow = false ;
13161378 final int tmpRet ;
13171379
13181380 /* Set initial status for SSLEngineResult return */
@@ -1463,38 +1525,26 @@ else if (hs == SSLEngineResult.HandshakeStatus.NEED_WRAP &&
14631525 if (this .handshakeFinished == false ) {
14641526 WolfSSLDebug .log (getClass (), WolfSSLDebug .INFO ,
14651527 () -> "starting or continuing handshake" );
1466- ret = DoHandshake (false );
1528+ if (!this .contPartialRecord ) {
1529+ /* Client only: TLS 1.3 ssl_accept() I/O callback
1530+ * patterns conflict with the pre-peek
1531+ * mid-handshake. Post-handshake server unwrap
1532+ * always applies peek. */
1533+ bufferUnderflow =
1534+ this .getUseClientMode () &&
1535+ peekTlsRecordHeader (in , inRemaining );
1536+ }
1537+ if (bufferUnderflow ) {
1538+ status = SSLEngineResult .Status .BUFFER_UNDERFLOW ;
1539+ }
1540+ else {
1541+ ret = DoHandshake (false );
1542+ }
14671543 }
14681544 else {
1469- /* TLS-only: peek at the record header and
1470- * return BUFFER_UNDERFLOW before calling into
1471- * JNI when the full record is not yet present.
1472- * Without this, native wolfSSL consumes partial
1473- * record bytes via the I/O callback, violating
1474- * the JSSE contract that BUFFER_UNDERFLOW must
1475- * report bytesConsumed() == 0. DTLS still
1476- * relies on the native WANT_READ path. */
1477- boolean bufferUnderflow = false ;
1478- if (inRemaining > 0 && (this .ssl .dtls () == 0 )) {
1479- synchronized (netDataLock ) {
1480- int pos = in .position ();
1481- if (inRemaining < TLS_RECORD_HEADER_LEN ) {
1482- /* Not enough for TLS record header */
1483- bufferUnderflow = true ;
1484- } else {
1485- /* Peek at record length from header
1486- * bytes 3-4 (big-endian) */
1487- int recLen =
1488- ((in .get (pos + TLS_RECORD_LEN_HI_OFF )
1489- & 0xFF ) << 8 ) |
1490- (in .get (pos + TLS_RECORD_LEN_LO_OFF )
1491- & 0xFF );
1492- if (inRemaining <
1493- TLS_RECORD_HEADER_LEN + recLen ) {
1494- bufferUnderflow = true ;
1495- }
1496- }
1497- }
1545+ if (!this .contPartialRecord ) {
1546+ bufferUnderflow =
1547+ peekTlsRecordHeader (in , inRemaining );
14981548 }
14991549
15001550 /* Serve stashed data from previous
@@ -1697,7 +1747,8 @@ else if (ret < 0 &&
16971747 }
16981748 }
16991749 else if (!this .handshakeFinished && (ret == 0 ) &&
1700- (err == 0 || err == WolfSSL .SSL_ERROR_ZERO_RETURN )) {
1750+ (err == 0 || err == WolfSSL .SSL_ERROR_ZERO_RETURN )
1751+ && !bufferUnderflow ) {
17011752
17021753 boolean gotCloseNotify = false ;
17031754 synchronized (ioLock ) {
@@ -2650,6 +2701,11 @@ protected synchronized int internalRecvCb(ByteBuffer toRead, int sz) {
26502701 if (this .ssl .dtls () == 1 && this .handshakeFinished ) {
26512702 this .nativeWantsToRead = 1 ;
26522703 }
2704+ /* Buffer exhausted mid-record: skip the header peek on the
2705+ * next unwrap() to pass continuation bytes to DoHandshake. */
2706+ if (this .netData != null && this .netData .remaining () == 0 ) {
2707+ this .contPartialRecord = true ;
2708+ }
26532709 return WolfSSL .WOLFSSL_CBIO_ERR_WANT_READ ;
26542710 }
26552711
@@ -2676,6 +2732,10 @@ protected synchronized int internalRecvCb(ByteBuffer toRead, int sz) {
26762732 toRead .position (toReadPos );
26772733 }
26782734
2735+ /* Reset to false when all requested bytes were delivered;
2736+ * set to true when the buffer was exhausted mid-record. */
2737+ this .contPartialRecord = (max < sz );
2738+
26792739 return max ;
26802740 }
26812741 }
0 commit comments