@@ -265,6 +265,31 @@ static WS_MAYBE_UNUSED word32 BuildExtInfoSigAlgs(byte* buf, word32 bufSz,
265265 return AppendString (buf , bufSz , idx , sigAlgs );
266266}
267267
268+ static word32 BuildChannelClosePacket (word32 peerChannelId , byte * out ,
269+ word32 outSz )
270+ {
271+ byte payload [16 ];
272+ word32 idx = 0 ;
273+
274+ idx = AppendUint32 (payload , sizeof (payload ), idx , peerChannelId );
275+
276+ return WrapPacket (MSGID_CHANNEL_CLOSE , payload , idx , out , outSz );
277+ }
278+
279+
280+ static word32 BuildChannelDataPacket (word32 peerChannelId , const char * data ,
281+ byte * out , word32 outSz )
282+ {
283+ byte payload [64 ];
284+ word32 idx = 0 ;
285+
286+ idx = AppendUint32 (payload , sizeof (payload ), idx , peerChannelId );
287+ idx = AppendString (payload , sizeof (payload ), idx , data );
288+
289+ return WrapPacket (MSGID_CHANNEL_DATA , payload , idx , out , outSz );
290+ }
291+
292+
268293#ifdef WOLFSSH_FWD
269294static word32 BuildDirectTcpipExtra (const char * host , word32 hostPort ,
270295 const char * origin , word32 originPort , byte * out , word32 outSz )
@@ -4194,6 +4219,68 @@ static void TestDisconnectOutranksRekey(void)
41944219}
41954220
41964221
4222+ /* wolfSSH_worker() is the other drive loop, and the one the SFTP and SCP
4223+ * layers turn. Once the session is over it has nothing to drive: the
4224+ * dispatch is skipped, so a post-disconnect message would leave
4225+ * ssh->error at WS_SUCCESS and the worker would keep reporting a healthy
4226+ * session for as long as the peer talks. It also outranks a rekey the peer
4227+ * abandoned, which only NEWKEYS could clear. RFC 4253 section 11.1. */
4228+ static void TestWorkerReportsDisconnect (void )
4229+ {
4230+ WOLFSSH_CTX * ctx ;
4231+ WOLFSSH * ssh ;
4232+ MemIo io ;
4233+ byte in [256 ];
4234+ byte out [256 ];
4235+ word32 inSz ;
4236+ word32 idx ;
4237+
4238+ ctx = wolfSSH_CTX_new (WOLFSSH_ENDPOINT_CLIENT , NULL );
4239+ AssertNotNull (ctx );
4240+
4241+ wolfSSH_SetIORecv (ctx , MemRecv );
4242+ wolfSSH_SetIOSend (ctx , MemSend );
4243+
4244+ ssh = wolfSSH_new (ctx );
4245+ AssertNotNull (ssh );
4246+ AddSessionChannel (ssh );
4247+ ssh -> connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE ;
4248+
4249+ /* The peer's KEXINIT, the way DoKexInit records it. Nothing clears it
4250+ * after the disconnect below, so it latches for the session. */
4251+ ssh -> isKeying |= WOLFSSH_PEER_IS_KEYING ;
4252+
4253+ /* The peer's disconnect, then a message behind it. An IGNORE draws no
4254+ * reply of its own, so what goes out can only come from the worker. */
4255+ idx = BuildDisconnectPacket (WOLFSSH_DISCONNECT_BY_APPLICATION ,
4256+ in , sizeof (in ));
4257+ inSz = idx + BuildPacket (MSGID_IGNORE , in + idx , sizeof (in ) - idx );
4258+ MemIoInit (& io , in , inSz , out , sizeof (out ));
4259+ wolfSSH_SetIOReadCtx (ssh , & io );
4260+ wolfSSH_SetIOWriteCtx (ssh , & io );
4261+
4262+ AssertIntEQ (wolfSSH_worker (ssh , NULL ), WS_FATAL_ERROR );
4263+ AssertIntEQ (wolfSSH_get_error (ssh ), WS_DISCONNECT );
4264+ AssertTrue (ssh -> disconnected );
4265+ AssertTrue (ssh -> isKeying != 0 );
4266+ io .outSz = 0 ;
4267+
4268+ /* The message behind it is still queued, and every further pass reports
4269+ * the disconnect rather than the WS_SUCCESS of a skipped dispatch or the
4270+ * WS_REKEYING of a rekey that cannot finish. */
4271+ AssertIntEQ (wolfSSH_worker (ssh , NULL ), WS_FATAL_ERROR );
4272+ AssertIntEQ (wolfSSH_get_error (ssh ), WS_DISCONNECT );
4273+ AssertIntEQ (wolfSSH_worker (ssh , NULL ), WS_FATAL_ERROR );
4274+ AssertIntEQ (wolfSSH_get_error (ssh ), WS_DISCONNECT );
4275+
4276+ /* Nothing went out on any of them. */
4277+ AssertIntEQ (io .outSz , 0 );
4278+
4279+ wolfSSH_free (ssh );
4280+ wolfSSH_CTX_free (ctx );
4281+ }
4282+
4283+
41974284
41984285#ifndef NO_WOLFSSH_SERVER
41994286
@@ -4410,6 +4497,198 @@ static void TestDisconnectGatesConnect(void)
44104497#endif /* !NO_WOLFSSH_CLIENT */
44114498
44124499
4500+ /* The public senders sit behind the disconnect gate, but the replies the
4501+ * library builds in answer to inbound traffic did not. A channel close
4502+ * draws an EOF and a close of ours out of DoChannelClose(), and a channel
4503+ * open a confirmation or a failure out of DoChannelOpen(); on a session
4504+ * that is already over, none of that may reach the peer. RFC 4253
4505+ * section 11.1. */
4506+ static void TestDisconnectSilencesInboundReplies (void )
4507+ {
4508+ WOLFSSH_CTX * ctx ;
4509+ WOLFSSH * ssh ;
4510+ WOLFSSH_CHANNEL * channel ;
4511+ MemIo io ;
4512+ byte in [256 ];
4513+ byte out [512 ];
4514+ word32 inSz ;
4515+ word32 quietSz ;
4516+ word32 channelId ;
4517+
4518+ ctx = wolfSSH_CTX_new (WOLFSSH_ENDPOINT_CLIENT , NULL );
4519+ AssertNotNull (ctx );
4520+
4521+ wolfSSH_SetIORecv (ctx , MemRecv );
4522+ wolfSSH_SetIOSend (ctx , MemSend );
4523+
4524+ ssh = wolfSSH_new (ctx );
4525+ AssertNotNull (ssh );
4526+ AddSessionChannel (ssh );
4527+ channel = ssh -> channelList ;
4528+ channelId = channel -> channel ;
4529+ /* Past userauth, or the message filter turns the inbound messages away
4530+ * on its own and the wire check below proves nothing. */
4531+ ssh -> connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE ;
4532+
4533+ inSz = BuildChannelClosePacket (channelId , in , sizeof (in ));
4534+ MemIoInit (& io , in , inSz , out , sizeof (out ));
4535+ wolfSSH_SetIOReadCtx (ssh , & io );
4536+ wolfSSH_SetIOWriteCtx (ssh , & io );
4537+
4538+ /* Our own disconnect goes out first, and is the last thing that may. */
4539+ AssertIntEQ (wolfSSH_SendDisconnect (ssh , WOLFSSH_DISCONNECT_BY_APPLICATION ),
4540+ WS_SUCCESS );
4541+ AssertTrue (ssh -> disconnected );
4542+ quietSz = io .outSz ;
4543+ AssertTrue (quietSz > 0 );
4544+
4545+ /* The peer's close arrives anyway: a caller's own wolfSSH_worker() loop
4546+ * still reads after a disconnect, and shutdown's pump can find packets
4547+ * queued behind the peer's DISCONNECT. A skipped packet
4548+ * is not an error; assert that, so the quiet-wire checks below cannot
4549+ * pass on a receive that never reached DoPacket(). */
4550+ AssertIntEQ (DoReceive (ssh ), WS_SUCCESS );
4551+
4552+ /* No EOF and no close went out, and the handler never ran, so the
4553+ * channel it would have torn down is still on the list. */
4554+ AssertIntEQ (io .outSz , quietSz );
4555+ AssertNotNull (ssh -> channelList );
4556+ AssertIntEQ (ssh -> channelList -> channel , channelId );
4557+ AssertFalse (channel -> eofTxd );
4558+ AssertFalse (channel -> closeTxd );
4559+
4560+ /* A channel open is the other half: it answers with a confirmation or
4561+ * a failure, and neither may go out now. */
4562+ inSz = BuildChannelOpenPacket ("session" , 99 , 1024 , 1024 , NULL , 0 ,
4563+ in , sizeof (in ));
4564+ MemIoInit (& io , in , inSz , out , sizeof (out ));
4565+ io .outSz = quietSz ;
4566+ wolfSSH_SetIOReadCtx (ssh , & io );
4567+ wolfSSH_SetIOWriteCtx (ssh , & io );
4568+
4569+ AssertIntEQ (DoReceive (ssh ), WS_SUCCESS );
4570+ AssertIntEQ (io .outSz , quietSz );
4571+
4572+ wolfSSH_free (ssh );
4573+ wolfSSH_CTX_free (ctx );
4574+ }
4575+
4576+
4577+ /* Skipping the dispatch is all the gate does: the frame advance steps over
4578+ * the whole packet on its own, so a packet behind a skipped one is still
4579+ * found where it should be. And a DISCONNECT is not skipped -- DoDisconnect()
4580+ * sends nothing, and it is what latches WS_DISCONNECT for the caller, so
4581+ * swallowing the peer's would report a live session on a dead one. */
4582+ static void TestDisconnectKeepsStreamInStep (void )
4583+ {
4584+ WOLFSSH_CTX * ctx ;
4585+ WOLFSSH * ssh ;
4586+ WOLFSSH_CHANNEL * channel ;
4587+ MemIo io ;
4588+ byte in [256 ];
4589+ byte out [512 ];
4590+ word32 inSz ;
4591+ word32 quietSz ;
4592+ word32 channelId ;
4593+
4594+ ctx = wolfSSH_CTX_new (WOLFSSH_ENDPOINT_CLIENT , NULL );
4595+ AssertNotNull (ctx );
4596+
4597+ wolfSSH_SetIORecv (ctx , MemRecv );
4598+ wolfSSH_SetIOSend (ctx , MemSend );
4599+
4600+ ssh = wolfSSH_new (ctx );
4601+ AssertNotNull (ssh );
4602+ AddSessionChannel (ssh );
4603+ channel = ssh -> channelList ;
4604+ channelId = channel -> channel ;
4605+ ssh -> connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE ;
4606+
4607+ /* A channel close for the gate to skip, with the peer's disconnect behind
4608+ * it in the same read. */
4609+ inSz = BuildChannelClosePacket (channelId , in , sizeof (in ));
4610+ inSz += BuildDisconnectPacket (WOLFSSH_DISCONNECT_BY_APPLICATION ,
4611+ in + inSz , (word32 )sizeof (in ) - inSz );
4612+ MemIoInit (& io , in , inSz , out , sizeof (out ));
4613+ wolfSSH_SetIOReadCtx (ssh , & io );
4614+ wolfSSH_SetIOWriteCtx (ssh , & io );
4615+
4616+ /* Ours goes out first, and is the last thing that may. */
4617+ AssertIntEQ (wolfSSH_SendDisconnect (ssh , WOLFSSH_DISCONNECT_BY_APPLICATION ),
4618+ WS_SUCCESS );
4619+ AssertTrue (ssh -> disconnected );
4620+ quietSz = io .outSz ;
4621+ AssertTrue (quietSz > 0 );
4622+
4623+ /* The close is skipped, so no reply and the channel stays. */
4624+ AssertIntEQ (DoReceive (ssh ), WS_SUCCESS );
4625+ AssertIntEQ (io .outSz , quietSz );
4626+ AssertNotNull (ssh -> channelList );
4627+ AssertIntEQ (ssh -> channelList -> channel , channelId );
4628+ AssertFalse (channel -> eofTxd );
4629+ AssertFalse (channel -> closeTxd );
4630+
4631+ /* The disconnect behind it decodes and latches, and still answers
4632+ * nothing. */
4633+ AssertIntEQ (DoReceive (ssh ), WS_FATAL_ERROR );
4634+ AssertIntEQ (wolfSSH_get_error (ssh ), WS_DISCONNECT );
4635+ AssertIntEQ (io .outSz , quietSz );
4636+
4637+ wolfSSH_free (ssh );
4638+ wolfSSH_CTX_free (ctx );
4639+ }
4640+
4641+
4642+ /* Skipping the dispatch drops what arrives, it does not queue it: the gate
4643+ * covers inbound data, not only replies. A caller that can no longer send has
4644+ * nothing to do with it. Pinned because the read path still hands back data
4645+ * that arrived before the disconnect, and the two are easy to confuse. */
4646+ static void TestDisconnectDropsLateChannelData (void )
4647+ {
4648+ WOLFSSH_CTX * ctx ;
4649+ WOLFSSH * ssh ;
4650+ MemIo io ;
4651+ byte in [256 ];
4652+ byte out [512 ];
4653+ byte buf [32 ];
4654+ word32 inSz ;
4655+ word32 channelId ;
4656+
4657+ ctx = wolfSSH_CTX_new (WOLFSSH_ENDPOINT_CLIENT , NULL );
4658+ AssertNotNull (ctx );
4659+
4660+ wolfSSH_SetIORecv (ctx , MemRecv );
4661+ wolfSSH_SetIOSend (ctx , MemSend );
4662+
4663+ ssh = wolfSSH_new (ctx );
4664+ AssertNotNull (ssh );
4665+ AddSessionChannel (ssh );
4666+ channelId = ssh -> channelList -> channel ;
4667+ ssh -> connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE ;
4668+
4669+ inSz = BuildChannelDataPacket (channelId , "late" , in , sizeof (in ));
4670+ MemIoInit (& io , in , inSz , out , sizeof (out ));
4671+ wolfSSH_SetIOReadCtx (ssh , & io );
4672+ wolfSSH_SetIOWriteCtx (ssh , & io );
4673+
4674+ AssertIntEQ (wolfSSH_SendDisconnect (ssh , WOLFSSH_DISCONNECT_BY_APPLICATION ),
4675+ WS_SUCCESS );
4676+ AssertTrue (ssh -> disconnected );
4677+
4678+ /* Read it, and it is gone: nothing buffered on the channel. */
4679+ AssertIntEQ (DoReceive (ssh ), WS_SUCCESS );
4680+ AssertNotNull (ssh -> channelList );
4681+ AssertIntEQ (ssh -> channelList -> inputBuffer .length
4682+ - ssh -> channelList -> inputBuffer .idx , 0 );
4683+
4684+ /* So the read reports the dead session rather than the dropped bytes. */
4685+ AssertIntEQ (wolfSSH_stream_read (ssh , buf , sizeof (buf )), WS_FATAL_ERROR );
4686+ AssertIntEQ (wolfSSH_get_error (ssh ), WS_DISCONNECT );
4687+
4688+ wolfSSH_free (ssh );
4689+ wolfSSH_CTX_free (ctx );
4690+ }
4691+
44134692/* disconnectTxd means "a flush is owed", not "a disconnect was sent". Once
44144693 * ours has gone out, a teardown call must not push whatever the internal
44154694 * senders queued behind it. */
@@ -8301,6 +8580,9 @@ int main(int argc, char** argv)
83018580#ifndef NO_WOLFSSH_CLIENT
83028581 TestDisconnectGatesConnect ();
83038582#endif
8583+ TestDisconnectSilencesInboundReplies ();
8584+ TestDisconnectKeepsStreamInStep ();
8585+ TestDisconnectDropsLateChannelData ();
83048586 TestDisconnectQuietWindowAdjust ();
83058587 TestDisconnectBlocksChannelAndFwdSends ();
83068588 TestStreamExitReportsDisconnect ();
@@ -8315,6 +8597,7 @@ int main(int argc, char** argv)
83158597 TestShutdownKeepsFlushWantWrite ();
83168598 TestDisconnectTxdClearsOnFlush ();
83178599 TestDisconnectOutranksRekey ();
8600+ TestWorkerReportsDisconnect ();
83188601#if defined(WOLFSSH_TERM ) && !defined(NO_FILESYSTEM )
83198602 TestTerminalResizeBlockedAfterDisconnect ();
83208603#endif
0 commit comments