Skip to content

Commit 6fe2693

Browse files
committed
net_imap: Use separate lock for updates to avoid blocking.
Commit a0ba56f fixed bbs_sendfile to ensure that sendfile() would not block indefinitely if a client stalled or stopped reading. However, even with this fix, clients can still get blocked up to the sendfile timeout (60 seconds, to accomodate slow connections) since the slow thread holds the IMAP session's lock, which was needed during an update traversal, even if we would delay the write. Since updates present a unique case, use a separate lock for update writes that continues to ensure atomicity of writes to the node and the delay pipe, while also ensuring that the main session lock isn't obtained unless we actually intend to write to the node (which shouldn't happen in a blocking case like above). Add a test for this scenario which fails prior to this change due to other threads getting blocked but succeeds now. Also fix a preexisting off-nominal memory leak exposed by the test execution.
1 parent f4364d8 commit 6fe2693

8 files changed

Lines changed: 311 additions & 26 deletions

File tree

nets/net_imap.c

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -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

52975317
cleanup:
52985318
imap_destroy(&imap);
5319+
bbs_mutex_destroy(&imap.updatelock);
52995320
bbs_mutex_destroy(&imap.lock);
53005321
}
53015322

nets/net_imap/imap.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ struct imap_session {
102102
unsigned int command_inprogress:1; /* Whether a command is currently in progress */
103103
struct imap_notify *notify; /* NOTIFY events */
104104
bbs_mutex_t lock; /* Lock for IMAP session */
105+
bbs_mutex_t updatelock; /* Lock for updates sent to a session (potentially by a different thread) */
105106
RWLIST_ENTRY(imap_session) entry; /* Next active session */
106107
};
107108

nets/net_imap/imap_server_fetch.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,6 +1053,12 @@ static int process_fetch(struct imap_session *imap, int usinguid, struct fetch_r
10531053
free(entry);
10541054
break;
10551055
}
1056+
1057+
/* In case we broke the loop early, free the remaining entries: */
1058+
while (fno < files && (entry = entries[fno++])) {
1059+
free(entry);
1060+
}
1061+
10561062
free(entries);
10571063
if (!fetched) {
10581064
bbs_debug(6, "FETCH command did not return any matching results\n");
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[general]
2+
relayin=yes
3+
relayout=no ; Don't let email leave the server for testing purposes
4+
requirefromhelomatch=no
5+
maxsize=15000000 ; this is the only thing different in this file
6+
7+
[smtps]
8+
enabled=no
9+
10+
[msa]
11+
requirestarttls=no
12+
13+
[blacklist]
14+
example.org = no
15+
16+
[bounce_redirects]
17+
nobounce@bbs.example.com = testuser2
18+
nobounce2@bbs.example.com = nobounce2@bbs.example.com ; alias for testuser3

tests/test.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ int test_client_expect_eventually_buf(int fd, int ms, const char *restrict s, in
428428
return -1;
429429
}
430430

431-
static void get_live_backtrace(void)
431+
void test_get_live_backtrace(void)
432432
{
433433
/* Don't try to use gdb if we're already stracing.
434434
* Can't do multiple ptraces. */
@@ -532,7 +532,7 @@ static void *io_relay(void *varg)
532532
/* At this point, something is likely "stuck".
533533
* The BBS won't trigger this itself, but we should get a backtrace of the
534534
* running process to see what's up. */
535-
get_live_backtrace();
535+
test_get_live_backtrace();
536536
}
537537
}
538538
if (rand_alloc_fails && strstr(expectbuf, "Simulated allocation failure")) {
@@ -1089,7 +1089,7 @@ static void *stop_stuck_bbs(void *unused)
10891089
return NULL; /* Maybe it just exited */
10901090
}
10911091

1092-
get_live_backtrace();
1092+
test_get_live_backtrace();
10931093

10941094
if (current_child) {
10951095
send_signal(bbs_pid(current_child), SIGTERM);

tests/test.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ struct test_module *TEST_MODULE_SELF_SYM(void);
2929

3030
int running_under_valgrind(void);
3131

32+
void test_get_live_backtrace(void);
33+
3234
/* Don't be fooled.
3335
* This program is not linked to the main BBS binary,
3436
* so arbitrarily including headers for the BBS

0 commit comments

Comments
 (0)