@@ -358,7 +358,7 @@ static void save_traversal(struct imap_session *imap, struct imap_traversal *tra
358358#undef TRAVERSAL_PERSIST
359359}
360360
361- #define imap_send_update (imap , s , len ) __imap_send_update(imap, s, len, 0, 0, 0)
361+ #define imap_send_update (imap , s , len ) bbs_mutex_lock(&imap->updatelock); __imap_send_update(imap, s, len, 0, 0, 0); bbs_mutex_unlock(&imap->updatelock);
362362#define __imap_send_update (imap , s , len , forcenow , is_expunge , invalidate ) __imap_send_update_log(imap, s, len, forcenow, invalidate, is_expunge, __LINE__)
363363
364364/*! \note Must be called with imap locked */
@@ -370,20 +370,44 @@ static void __imap_send_update_log(struct imap_session *imap, const char *s, siz
370370 * EXPUNGE is a special case - RFC 3501 5.3 dictates EXPUNGE responses are not allowed if no command is in progress. */
371371 delay = (!imap_sequence_numbers_prohibited (imap ) && !imap -> idle ) || (is_expunge && !imap -> command_inprogress );
372372
373- /* Since we're locked in this function, we CANNOT use imap_send */
374- if (delay && !forcenow ) {
375- imap_debug (4 , "%d: %p (delayed) <= %s" , line , imap , s ); /* Already ends in CR LF */
376- bbs_node_any_fd_write (imap -> node , imap -> pfd [1 ], s , len );
377- imap -> pending = 1 ;
378- if (is_expunge ) {
379- imap -> expungepending = 1 ;
380- }
381- if (invalidate ) {
382- reset_saved_search (imap ); /* Since messages were expunged, invalidate any saved search */
373+ /* Do not use imap_send here!
374+ *
375+ * Initially, this was because we held imap->lock here, and thus calling imap_send would result in a recursive lock attempt.
376+ *
377+ * We now use a separate lock on this path (imap->updatelock), since imap->lock could be held for long periods of
378+ * time if a slow client issues a FETCH command and it takes a long time to complete the response
379+ * (e.g. sendfile via send_message in imap_server_fetch.c)
380+ * That would result in blocking here until the response completes and that lock is released;
381+ * however, if the session is blocked, we are going to delay our write anyways, so we don't really need that lock.
382+ * We only use it below in the non-delayed case.
383+ *
384+ * This change is exercised by test_imap_fetch_slow, which would result in a backtrace
385+ * if imap->lock were unconditionally locked on this path. */
386+
387+ if (!delay || forcenow ) {
388+ bbs_mutex_lock (& imap -> lock );
389+ /* Since imap->idle is guarded by imap->lock, not imap->updatelock,
390+ * after obtaining the lock, double-check that we're really good to write, just to be sure. */
391+ delay = (!imap_sequence_numbers_prohibited (imap ) && !imap -> idle ) || (is_expunge && !imap -> command_inprogress );
392+ if (!delay || forcenow ) {
393+ imap_debug (4 , "%d: %p <= %s" , line , imap , s ); /* Already ends in CR LF */
394+ bbs_node_any_fd_write (imap -> node , imap -> node -> wfd , s , (unsigned int ) len );
395+ bbs_mutex_unlock (& imap -> lock );
396+ return ;
383397 }
384- } else {
385- imap_debug (4 , "%d: %p <= %s" , line , imap , s ); /* Already ends in CR LF */
386- bbs_node_any_fd_write (imap -> node , imap -> node -> wfd , s , (unsigned int ) len );
398+ /* If delay was initially false but became true before we checked again,
399+ * then fall through to the delayed case. */
400+ bbs_mutex_unlock (& imap -> lock );
401+ }
402+
403+ imap_debug (4 , "%d: %p (delayed) <= %s" , line , imap , s ); /* Already ends in CR LF */
404+ bbs_node_any_fd_write (imap -> node , imap -> pfd [1 ], s , len );
405+ imap -> pending = 1 ;
406+ if (is_expunge ) {
407+ imap -> expungepending = 1 ;
408+ }
409+ if (invalidate ) {
410+ reset_saved_search (imap ); /* Since messages were expunged, invalidate any saved search */
387411 }
388412}
389413
@@ -530,7 +554,6 @@ void send_untagged_fetch(struct imap_session *imap, const char *maildir, int seq
530554 generate_status (imap , mboxname , status_items , sizeof (status_items ), "UNSEEN MESSAGES UIDVALIDITY HIGHESTMODSEQ" );
531555 didstatus = 1 ;
532556 }
533- bbs_mutex_lock (& s -> lock );
534557 if (res == -1 ) { /* Not currently selected */
535558 char statusmsgfull [256 ];
536559 /* The same STATUS response can be used for all clients, but the mailbox name might be different */
@@ -539,7 +562,6 @@ void send_untagged_fetch(struct imap_session *imap, const char *maildir, int seq
539562 } else { /* Currently selected */
540563 imap_send_update (s , s -> condstore ? condstoremsg : normalmsg , s -> condstore ? condlen : normallen );
541564 }
542- bbs_mutex_unlock (& s -> lock );
543565 }
544566 RWLIST_UNLOCK (& sessions );
545567}
@@ -587,11 +609,12 @@ static void send_untagged_expunge(struct bbs_node *node, struct mailbox *mbox, c
587609 generate_status (s , mboxname , status_items , sizeof (status_items ), "UIDNEXT MESSAGES HIGHESTMODSEQ" );
588610 didstatus = 1 ;
589611 }
590- bbs_mutex_lock (& s -> lock );
591612 if (res == -1 ) {
592613 char statusmsgfull [256 ];
593614 size_t statuslenfull = (size_t ) snprintf (statusmsgfull , sizeof (statusmsgfull ), "* STATUS \"%s\" (%s)\r\n" , mboxname , status_items );
615+ bbs_mutex_lock (& s -> updatelock );
594616 __imap_send_update (s , statusmsgfull , statuslenfull , forcenow , 1 , 0 );
617+ bbs_mutex_unlock (& s -> updatelock );
595618 } else {
596619 if (s -> qresync ) { /* VANISHED */
597620 if (!str ) {
@@ -615,14 +638,15 @@ static void send_untagged_expunge(struct bbs_node *node, struct mailbox *mbox, c
615638 __imap_send_update (s , str , slen , forcenow , 1 , 0 );
616639 } else { /* EXPUNGE */
617640 int i ;
641+ bbs_mutex_lock (& s -> updatelock );
618642 for (i = 0 ; i < length ; i ++ ) {
619643 char normalmsg [64 ];
620644 size_t normallen = (size_t ) snprintf (normalmsg , sizeof (normalmsg ), "* %u EXPUNGE\r\n" , seqno [i ]);
621645 __imap_send_update (s , normalmsg , normallen , forcenow , 1 , 1 );
622646 }
647+ bbs_mutex_unlock (& s -> updatelock );
623648 }
624649 }
625- bbs_mutex_unlock (& s -> lock );
626650 }
627651 RWLIST_UNLOCK (& sessions );
628652 free_if (str );
@@ -692,16 +716,13 @@ static void send_untagged_exists(struct bbs_node *node, struct mailbox *mbox, co
692716 * but we'd need to send a FETCH per matching message.
693717 * Again for \Recent messages this is going to be tricky/impossible. */
694718
695- bbs_mutex_lock (& s -> lock );
696719 /* RFC 3501 Section 7: unilateral response */
697720 if (res == -1 ) {
698721 char statusmsgfull [256 ];
699722 size_t statuslenfull = (size_t ) snprintf (statusmsgfull , sizeof (statusmsgfull ), "* STATUS \"%s\" (%s)\r\n" , mboxname , status_items );
700723 imap_send_update (s , statusmsgfull , statuslenfull );
701- bbs_mutex_unlock (& s -> lock );
702724 } else {
703725 imap_send_update (s , buf , len );
704- bbs_mutex_unlock (& s -> lock );
705726 /* Unlock because send_fetch_response assumes an unlocked session.
706727 * XXX Since sessions, the session technically can't disappear on us,
707728 * but this does leave open the possibility of interleaved writes. */
@@ -767,9 +788,7 @@ static void send_untagged_list(struct bbs_node *node, enum mailbox_event_type ty
767788 break ;
768789 }
769790
770- bbs_mutex_lock (& s -> lock );
771791 imap_send_update (s , buf , len );
772- bbs_mutex_unlock (& s -> lock );
773792 }
774793 RWLIST_UNLOCK (& sessions );
775794}
@@ -5273,6 +5292,7 @@ static void imap_handler(struct bbs_node *node, int secure)
52735292 }
52745293
52755294 bbs_mutex_init (& imap .lock , NULL );
5295+ bbs_mutex_init (& imap .updatelock , NULL );
52765296 RWLIST_HEAD_INIT (& imap .clients );
52775297
52785298 /* Add to session list (for IDLE) */
@@ -5296,6 +5316,7 @@ static void imap_handler(struct bbs_node *node, int secure)
52965316
52975317cleanup :
52985318 imap_destroy (& imap );
5319+ bbs_mutex_destroy (& imap .updatelock );
52995320 bbs_mutex_destroy (& imap .lock );
53005321}
53015322
0 commit comments