@@ -6308,6 +6308,222 @@ static void TestShutdownFlushesWithNoChannel(void)
63086308}
63096309
63106310
6311+ /* Two packets can share one flush, so step over the first to reach the
6312+ * second: a length prefix and the bytes it counts. */
6313+ static word32 NextPacketOffset (const byte * pkt , word32 sz )
6314+ {
6315+ word32 packetLen ;
6316+
6317+ AssertTrue (sz >= LENGTH_SZ );
6318+ packetLen = ReadUint32 (pkt );
6319+ AssertTrue (sz - LENGTH_SZ >= packetLen );
6320+
6321+ return packetLen + LENGTH_SZ ;
6322+ }
6323+
6324+
6325+ #ifndef NO_WOLFSSH_SERVER
6326+
6327+ static int RejectUserAuthCb (byte authType , WS_UserAuthData * authData ,
6328+ void * ctx )
6329+ {
6330+ WOLFSSH_UNUSED (authType );
6331+ WOLFSSH_UNUSED (authData );
6332+ WOLFSSH_UNUSED (ctx );
6333+
6334+ return WOLFSSH_USERAUTH_REJECTED ;
6335+ }
6336+
6337+
6338+ /* A rejected authentication owes the peer a USERAUTH_FAILURE, RFC 4252
6339+ * section 5.1. Below the auth-failure cap nothing queues a disconnect to
6340+ * drag the buffer out, and authentication never completed so there is no
6341+ * channel: teardown is the only thing left that can push the reply. */
6342+ static void TestShutdownFlushesQueuedUserAuthFailure (void )
6343+ {
6344+ ChannelOpenHarness harness ;
6345+ byte in [128 ];
6346+ word32 inSz ;
6347+
6348+ inSz = BuildUserAuthPasswordRequest ("alice" , "pw" , in , sizeof (in ));
6349+
6350+ InitUserAuthHarness (& harness , in , inSz );
6351+ wolfSSH_SetUserAuth (harness .ctx , RejectUserAuthCb );
6352+ wolfSSH_SetIOSend (harness .ctx , MemSendWantWrite );
6353+
6354+ /* The failure is bundled, then the socket refuses it. */
6355+ MemSendWantWriteCount = 1 ;
6356+ AssertIntEQ (DoReceive (harness .ssh ), WS_FATAL_ERROR );
6357+ AssertIntEQ (harness .ssh -> error , WS_USER_AUTH_E );
6358+ AssertIntEQ (harness .io .outSz , 0 );
6359+ AssertTrue (wolfSSH_OutputPending (harness .ssh ));
6360+
6361+ /* Neither of the two things that would have flushed it anyway. */
6362+ AssertFalse (harness .ssh -> disconnectTxd );
6363+ AssertNull (harness .ssh -> channelList );
6364+
6365+ /* The socket takes bytes now, so only the gate stands between the
6366+ * queued failure and the peer. */
6367+ AssertIntEQ (wolfSSH_shutdown (harness .ssh ), WS_CHANNEL_CLOSED );
6368+ AssertFalse (wolfSSH_OutputPending (harness .ssh ));
6369+
6370+ /* The failure, and nothing bundled behind it. */
6371+ AssertIntEQ (ParseMsgId (harness .io .out , harness .io .outSz ),
6372+ MSGID_USERAUTH_FAILURE );
6373+ AssertIntEQ (harness .io .outSz ,
6374+ NextPacketOffset (harness .io .out , harness .io .outSz ));
6375+
6376+ FreeChannelOpenHarness (& harness );
6377+ }
6378+
6379+ #endif /* !NO_WOLFSSH_SERVER */
6380+
6381+
6382+ /* DoChannelClose() retires the channel once the close is bundled, so a close
6383+ * the socket refused is left with no channel to carry the retry. Teardown
6384+ * still owes the peer that close, RFC 4254 section 5.3. */
6385+ static void TestShutdownFlushesQueuedChannelClose (void )
6386+ {
6387+ WOLFSSH_CTX * ctx ;
6388+ WOLFSSH * ssh ;
6389+ MemIo io ;
6390+ byte in [128 ];
6391+ byte out [512 ];
6392+ word32 inSz ;
6393+ word32 channelId ;
6394+ word32 closeOff ;
6395+
6396+ ctx = wolfSSH_CTX_new (WOLFSSH_ENDPOINT_CLIENT , NULL );
6397+ AssertNotNull (ctx );
6398+
6399+ wolfSSH_SetIORecv (ctx , MemRecv );
6400+ wolfSSH_SetIOSend (ctx , MemSendWantWrite );
6401+
6402+ ssh = wolfSSH_new (ctx );
6403+ AssertNotNull (ssh );
6404+ AddSessionChannel (ssh );
6405+ channelId = ssh -> channelList -> channel ;
6406+ /* Past userauth, or the message filter turns the close away and the
6407+ * wire checks below prove nothing. */
6408+ ssh -> connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE ;
6409+
6410+ inSz = BuildChannelClosePacket (channelId , in , sizeof (in ));
6411+ MemIoInit (& io , in , inSz , out , sizeof (out ));
6412+ wolfSSH_SetIOReadCtx (ssh , & io );
6413+ wolfSSH_SetIOWriteCtx (ssh , & io );
6414+
6415+ /* Our EOF and close answer the peer's close. Refuse both sends, or the
6416+ * second one carries the first out with it. */
6417+ MemSendWantWriteCount = 2 ;
6418+ AssertIntEQ (DoReceive (ssh ), WS_CHANNEL_CLOSED );
6419+ AssertIntEQ (io .outSz , 0 );
6420+ AssertTrue (wolfSSH_OutputPending (ssh ));
6421+
6422+ /* The channel that would have retried the send is already gone. */
6423+ AssertNull (ssh -> channelList );
6424+ AssertFalse (ssh -> disconnectTxd );
6425+
6426+ AssertIntEQ (wolfSSH_shutdown (ssh ), WS_CHANNEL_CLOSED );
6427+ AssertFalse (wolfSSH_OutputPending (ssh ));
6428+
6429+ /* Both halves reached the peer, EOF ahead of the close, and nothing
6430+ * else: the two packets account for every byte written. */
6431+ AssertIntEQ (ParseMsgId (out , io .outSz ), MSGID_CHANNEL_EOF );
6432+ closeOff = NextPacketOffset (out , io .outSz );
6433+ AssertIntEQ (ParseMsgId (out + closeOff , io .outSz - closeOff ),
6434+ MSGID_CHANNEL_CLOSE );
6435+ AssertIntEQ (io .outSz ,
6436+ closeOff + NextPacketOffset (out + closeOff , io .outSz - closeOff ));
6437+
6438+ wolfSSH_free (ssh );
6439+ wolfSSH_CTX_free (ctx );
6440+ }
6441+
6442+
6443+ /* The flush is no longer tied to a disconnect, so it can fire on a live
6444+ * session with the channel still listed and then short-send itself. The
6445+ * teardown sends bundle in behind bytes the socket has not taken, and the
6446+ * caller has to come back: the retry owes the peer the rest of the buffer
6447+ * and not a second copy of the EOF and close. */
6448+ static void TestShutdownFlushShortSendsWithChannel (void )
6449+ {
6450+ WOLFSSH_CTX * ctx ;
6451+ WOLFSSH * ssh ;
6452+ WOLFSSH_CHANNEL * channel ;
6453+ MemIo io ;
6454+ byte out [1024 ];
6455+ byte data [32 ];
6456+ word32 eofOff ;
6457+ word32 reqOff ;
6458+ word32 closeOff ;
6459+
6460+ ctx = wolfSSH_CTX_new (WOLFSSH_ENDPOINT_CLIENT , NULL );
6461+ AssertNotNull (ctx );
6462+
6463+ wolfSSH_SetIORecv (ctx , MemRecv );
6464+ wolfSSH_SetIOSend (ctx , MemSendWantWrite );
6465+
6466+ ssh = wolfSSH_new (ctx );
6467+ AssertNotNull (ssh );
6468+ AddSessionChannel (ssh );
6469+ channel = ssh -> channelList ;
6470+ ssh -> connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE ;
6471+
6472+ MemIoInit (& io , NULL , 0 , out , sizeof (out ));
6473+ wolfSSH_SetIOReadCtx (ssh , & io );
6474+ wolfSSH_SetIOWriteCtx (ssh , & io );
6475+
6476+ /* Channel data the socket will not take, on a session that is neither
6477+ * disconnected nor out of channels. */
6478+ WMEMSET (data , 'x' , sizeof (data ));
6479+ MemSendWantWriteCount = 5 ;
6480+ AssertIntEQ (wolfSSH_stream_send (ssh , data , sizeof (data )),
6481+ (int )sizeof (data ));
6482+ AssertIntEQ (wolfSSH_get_error (ssh ), WS_WANT_WRITE );
6483+ AssertTrue (wolfSSH_OutputPending (ssh ));
6484+ AssertFalse (ssh -> disconnected );
6485+ AssertNotNull (ssh -> channelList );
6486+
6487+ /* The flush and all three teardown sends are refused, so the unfinished
6488+ * flush owns the return and the whole teardown is still queued. */
6489+ AssertIntEQ (wolfSSH_shutdown (ssh ), WS_WANT_WRITE );
6490+ AssertIntEQ (io .outSz , 0 );
6491+ AssertTrue (wolfSSH_OutputPending (ssh ));
6492+ AssertIntEQ (MemSendWantWriteCount , 0 );
6493+
6494+ /* Bundled counts as sent, so the retry must not emit them again. The
6495+ * list comes first: a channel retired here would be freed, and the
6496+ * reads below it would be of freed memory. */
6497+ AssertNotNull (ssh -> channelList );
6498+ AssertTrue (channel -> eofTxd );
6499+ AssertTrue (channel -> closeTxd );
6500+
6501+ /* The socket takes bytes now. The retry drains what was queued and
6502+ * lands on the wait for the peer's close, not on a stale want-write
6503+ * for a buffer that is already empty. */
6504+ AssertIntEQ (wolfSSH_shutdown (ssh ), WS_FATAL_ERROR );
6505+ AssertIntEQ (wolfSSH_get_error (ssh ), WS_WANT_READ );
6506+ AssertFalse (wolfSSH_OutputPending (ssh ));
6507+
6508+ /* One copy of each, in order, accounting for every byte written. */
6509+ AssertIntEQ (ParseMsgId (out , io .outSz ), MSGID_CHANNEL_DATA );
6510+ eofOff = NextPacketOffset (out , io .outSz );
6511+ AssertIntEQ (ParseMsgId (out + eofOff , io .outSz - eofOff ),
6512+ MSGID_CHANNEL_EOF );
6513+ reqOff = eofOff + NextPacketOffset (out + eofOff , io .outSz - eofOff );
6514+ AssertIntEQ (ParseMsgId (out + reqOff , io .outSz - reqOff ),
6515+ MSGID_CHANNEL_REQUEST );
6516+ closeOff = reqOff + NextPacketOffset (out + reqOff , io .outSz - reqOff );
6517+ AssertIntEQ (ParseMsgId (out + closeOff , io .outSz - closeOff ),
6518+ MSGID_CHANNEL_CLOSE );
6519+ AssertIntEQ (io .outSz ,
6520+ closeOff + NextPacketOffset (out + closeOff , io .outSz - closeOff ));
6521+
6522+ wolfSSH_free (ssh );
6523+ wolfSSH_CTX_free (ctx );
6524+ }
6525+
6526+
63116527/* The highwater callback is the application's, and its return propagates out
63126528 * of wolfSSH_SendPacket(). A mark firing on the disconnect packet must not
63136529 * fail a send that went out fine, whoever owns the callback. */
@@ -10966,6 +11182,11 @@ int main(int argc, char** argv)
1096611182 TestPeerDisconnectKeepsTrafficQueued (0 );
1096711183 TestPeerDisconnectKeepsTrafficQueued (1 );
1096811184 TestShutdownFlushesWithNoChannel ();
11185+ #ifndef NO_WOLFSSH_SERVER
11186+ TestShutdownFlushesQueuedUserAuthFailure ();
11187+ #endif
11188+ TestShutdownFlushesQueuedChannelClose ();
11189+ TestShutdownFlushShortSendsWithChannel ();
1096911190 TestQueuedDisconnectFlushes ();
1097011191 TestShutdownFlushesQueuedDisconnect ();
1097111192 TestShutdownKeepsFlushWantWrite ();
0 commit comments