@@ -3508,4 +3508,129 @@ public void testBufferOverflowSmallOutput()
35083508
35093509 pass ("\t \t ... passed" );
35103510 }
3511+
3512+ @ Test
3513+ public void testWrapPartialDrainOffsetUpdate ()
3514+ throws NoSuchProviderException , NoSuchAlgorithmException ,
3515+ KeyManagementException , KeyStoreException ,
3516+ CertificateException , IOException ,
3517+ UnrecoverableKeyException ,
3518+ NoSuchFieldException , IllegalAccessException {
3519+
3520+ /* Regression test for issue 3241: CopyOutPacket() must decrement
3521+ * internalIOSendBufOffset after a partial drain, otherwise stale
3522+ * ciphertext beyond the shifted region gets re-emitted on the
3523+ * next wrap() call and corrupts the TLS session.
3524+ *
3525+ * The partial-drain branch requires
3526+ * internalIOSendBufOffset > out.remaining() >= packetBufferSize
3527+ * which the getPacketBufferSize() precheck in wrap() makes
3528+ * unreachable with a single queued record. Reflection is used to
3529+ * inject a marker byte sequence spanning more than one
3530+ * packetBufferSize worth of queued data, simulating the
3531+ * multi-record burst (TLS 1.3 NewSessionTicket / alert +
3532+ * handshake fragment) condition called out in the issue. */
3533+ System .out .print ("\t Testing wrap() partial drain offset" );
3534+
3535+ this .ctx = tf .createSSLContext ("TLS" , engineProvider );
3536+ SSLEngine server = this .ctx .createSSLEngine ();
3537+ SSLEngine client = this .ctx .createSSLEngine ("wolfSSL test" , 11111 );
3538+
3539+ server .setUseClientMode (false );
3540+ server .setNeedClientAuth (false );
3541+ client .setUseClientMode (true );
3542+
3543+ server .beginHandshake ();
3544+ client .beginHandshake ();
3545+
3546+ int ret = tf .testConnection (server , client , null , null ,
3547+ "partial drain test" );
3548+ if (ret != 0 ) {
3549+ error ("\t ... failed" );
3550+ fail ("failed to create connection" );
3551+ }
3552+
3553+ /* Inject a known marker sequence into the client's internal
3554+ * send buffer. Size is packetBufferSize + 1000 so the first
3555+ * CopyOutPacket drains packetBufferSize bytes and leaves 1000
3556+ * bytes queued. */
3557+ int packetSz = client .getSession ().getPacketBufferSize ();
3558+ int queuedSz = packetSz + 1000 ;
3559+ byte [] marker = new byte [queuedSz ];
3560+ for (int i = 0 ; i < queuedSz ; i ++) {
3561+ marker [i ] = (byte )((i * 31 ) & 0xFF );
3562+ }
3563+
3564+ java .lang .reflect .Field bufField =
3565+ WolfSSLEngine .class .getDeclaredField ("internalIOSendBuf" );
3566+ java .lang .reflect .Field bufSzField =
3567+ WolfSSLEngine .class .getDeclaredField ("internalIOSendBufSz" );
3568+ java .lang .reflect .Field offField =
3569+ WolfSSLEngine .class .getDeclaredField (
3570+ "internalIOSendBufOffset" );
3571+ bufField .setAccessible (true );
3572+ bufSzField .setAccessible (true );
3573+ offField .setAccessible (true );
3574+
3575+ bufField .set (client , marker );
3576+ bufSzField .setInt (client , queuedSz );
3577+ offField .setInt (client , queuedSz );
3578+
3579+ /* First wrap: out buffer sized exactly to packetBufferSize so
3580+ * the guard passes but CopyOutPacket partial-drains. */
3581+ ByteBuffer empty = ByteBuffer .allocate (0 );
3582+ ByteBuffer firstOut = ByteBuffer .allocate (packetSz );
3583+ SSLEngineResult result = client .wrap (empty , firstOut );
3584+
3585+ if (result .bytesProduced () != packetSz ) {
3586+ error ("\t ... failed" );
3587+ fail ("first wrap expected " + packetSz +
3588+ " produced, got " + result .bytesProduced ());
3589+ }
3590+
3591+ /* Fix-sensitive check: unfixed code leaves offset at queuedSz. */
3592+ int offAfterFirst = offField .getInt (client );
3593+ if (offAfterFirst != queuedSz - packetSz ) {
3594+ error ("\t ... failed" );
3595+ fail ("internalIOSendBufOffset not decremented after " +
3596+ "partial drain: expected " + (queuedSz - packetSz ) +
3597+ ", got " + offAfterFirst );
3598+ }
3599+
3600+ /* Second wrap: must drain only the remaining 1000 bytes.
3601+ * Unfixed code would produce packetSz bytes again, re-emitting
3602+ * the stale tail of the buffer. */
3603+ ByteBuffer secondOut = ByteBuffer .allocate (packetSz );
3604+ result = client .wrap (empty , secondOut );
3605+
3606+ int expectedRemainder = queuedSz - packetSz ;
3607+ if (result .bytesProduced () != expectedRemainder ) {
3608+ error ("\t ... failed" );
3609+ fail ("second wrap expected " + expectedRemainder +
3610+ " produced (remainder only), got " +
3611+ result .bytesProduced () +
3612+ " — stale bytes re-sent after partial drain" );
3613+ }
3614+
3615+ if (offField .getInt (client ) != 0 ) {
3616+ error ("\t ... failed" );
3617+ fail ("internalIOSendBufOffset not reset to 0 after " +
3618+ "remainder drained" );
3619+ }
3620+
3621+ /* Full integrity check: concatenated drained output must equal
3622+ * the original injected marker exactly. */
3623+ firstOut .flip ();
3624+ secondOut .flip ();
3625+ byte [] drained = new byte [queuedSz ];
3626+ firstOut .get (drained , 0 , packetSz );
3627+ secondOut .get (drained , packetSz , expectedRemainder );
3628+ if (!java .util .Arrays .equals (marker , drained )) {
3629+ error ("\t ... failed" );
3630+ fail ("drained output does not match injected queue" );
3631+ }
3632+
3633+ pass ("\t ... passed" );
3634+ }
3635+
35113636}
0 commit comments