Skip to content

Commit f9c9a95

Browse files
committed
io.c: Close outer read file descriptor at cleanup in nested transformations.
In the case of a nested transformation, like compression on top of TLS encryption (a common paradigm in IMAP), when an inner transformation (e.g. compression) cleans up, we need to clean up its outer read file descriptor, i.e. close its read file descriptor towards the network. This is because this was a pipe file descriptor created by the next outer transformation (e.g. TLS). If the transformation is not nested (e.g. just compression), this isn't needed since that would be the node's actual file descriptor, and node.c will clean it up. But if the transformation is nested, then we need to clean it up. We now close this file descriptor if the transformation was nested inside another one. A test (test_imap_compress_tls) has been added to test this behavior. The actual I/O doesn't work properly, but it does trigger a case (first observed in the while) that previously caused a file descriptor leak.
1 parent baa7b49 commit f9c9a95

4 files changed

Lines changed: 164 additions & 4 deletions

File tree

bbs/io.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,26 @@ static void *io_thread(void *varg)
367367
* We need to do this before we exit, since this thread isn't joined immediately. */
368368
bbs_debug(4, "%s I/O thread exiting\n", t->name);
369369
t->funcs->io_finalize(tran->data);
370+
371+
if (tran->outer_rfd != tran->outer_wfd) {
372+
/* There is an added complication for layered I/O connections, where we also need to close the outer read file descriptor, since we don't need to read from the network side anymore.
373+
*
374+
* For example, if we are running compression on top of TLS, then we leak a file descriptor if we don't close z->orig_fd in io_compress (this is what test_imap_compress_tls tests)
375+
*
376+
* However, if we always do that, that will mess up sessions that only use compression without TLS (test_imap_compress tests that),
377+
* since in that case, z->orig_rfd is the node's actual file descriptor, which gets closed in node_cleanup.
378+
*
379+
* We need to handle both cases, because if there are no I/O transformations, node.c needs to close its own file descriptor,
380+
* but if we are running on top of another I/O module, then we are responsible for closing it.
381+
*
382+
* We can detect that if, when we set up a transformation, rfd != wfd. This indicates this is a nested transformation,
383+
* since if we were the first transformation on top of the node socket, rfd == wfd.
384+
* Just below, in bbs_io_transform_setup, before calling t->funcs->setup, we save the original rfd and set tran->outer_rfd to it,
385+
* so we have access to it via that variable now and can close it. */
386+
bbs_debug(5, "Closing file descriptor %d since this is a nested transformation\n", tran->outer_rfd);
387+
close(tran->outer_rfd); /* Nothing more that we are going to read from a lower I/O layer since we already closed write to the application side */
388+
}
389+
370390
return NULL;
371391
}
372392

tests/compress.c

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,14 @@ void z_client_free(struct z_data *z)
7878
free(z);
7979
}
8080

81+
ssize_t zlib_write(struct z_data *z, int line, const char *buf, size_t len)
82+
{
83+
return zlib_write_cb(z, line, buf, len, NULL, NULL);
84+
}
85+
8186
#pragma GCC diagnostic push
8287
#pragma GCC diagnostic ignored "-Wcast-qual"
83-
ssize_t zlib_write(struct z_data *z, int line, const char *buf, size_t len)
88+
ssize_t zlib_write_cb(struct z_data *z, int line, const char *buf, size_t len, ssize_t (*write_cb)(void *cbdata, const char *buf, size_t len), void *cbdata)
8489
{
8590
char output[BUFSIZ];
8691

@@ -102,7 +107,11 @@ ssize_t zlib_write(struct z_data *z, int line, const char *buf, size_t len)
102107
}
103108
comp_len = sizeof(output) - z->compressor->avail_out;
104109
bbs_debug(10, "Deflated to %lu bytes at line %d\n", comp_len, line);
105-
wres = write(z->fd, output, comp_len);
110+
if (write_cb) {
111+
wres = write_cb(cbdata, buf, len);
112+
} else {
113+
wres = write(z->fd, output, comp_len);
114+
}
106115
if (wres <= 0) {
107116
bbs_error("write returned %ld at line %d\n", wres, line);
108117
return -1;
@@ -114,14 +123,23 @@ ssize_t zlib_write(struct z_data *z, int line, const char *buf, size_t len)
114123
#pragma GCC diagnostic pop
115124

116125
ssize_t zlib_read(struct z_data *z, int line, char *buf, size_t len)
126+
{
127+
return zlib_read_cb(z, line, buf, len, NULL, NULL);
128+
}
129+
130+
ssize_t zlib_read_cb(struct z_data *z, int line, char *buf, size_t len, ssize_t (*read_cb)(void *cbdata, char *buf, size_t len), void *cbdata)
117131
{
118132
char input[BUFSIZ / 10]; /* Hopefully the compression doesn't reduce the size by more than 90%... */
119133
char output[BUFSIZ];
120134
int zres;
121135
ssize_t rres;
122136
size_t bytes = 0;
123137

124-
rres = read(z->fd, input, sizeof(input));
138+
if (read_cb) {
139+
rres = read_cb(cbdata, input, sizeof(input));
140+
} else {
141+
rres = read(z->fd, input, sizeof(input));
142+
}
125143
if (rres <= 0) {
126144
bbs_error("read returned %ld at line %d\n", rres, line);
127145
return -1;
@@ -204,7 +222,18 @@ int test_z_client_expect_eventually(struct z_data *z, int ms, const char *restri
204222
return test_z_client_expect_eventually_buf(z, ms, s, line, buf, sizeof(buf));
205223
}
206224

225+
int test_z_client_expect_eventually_readcb(struct z_data *z, int ms, const char *restrict s, int line, ssize_t (*read_cb)(void *cbdata, char *buf, size_t len), void *cbdata)
226+
{
227+
char buf[4096];
228+
return test_z_client_expect_eventually_buf_readcb(z, ms, s, line, buf, sizeof(buf), read_cb, cbdata);
229+
}
230+
207231
int test_z_client_expect_eventually_buf(struct z_data *z, int ms, const char *restrict s, int line, char *restrict buf, size_t len)
232+
{
233+
return test_z_client_expect_eventually_buf_readcb(z, ms, s, line, buf, len, NULL, NULL);
234+
}
235+
236+
int test_z_client_expect_eventually_buf_readcb(struct z_data *z, int ms, const char *restrict s, int line, char *restrict buf, size_t len, ssize_t (*read_cb)(void *cbdata, char *buf, size_t len), void *cbdata)
208237
{
209238
struct pollfd pfd;
210239

@@ -223,7 +252,7 @@ int test_z_client_expect_eventually_buf(struct z_data *z, int ms, const char *re
223252
}
224253
if (res > 0 && pfd.revents) {
225254
ssize_t bytes;
226-
bytes = zlib_read(z, line, buf, len - 1);
255+
bytes = zlib_read_cb(z, line, buf, len - 1, read_cb, cbdata);
227256
if (bytes <= 0) {
228257
return -1;
229258
}

tests/compress.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,24 @@ void z_client_free(struct z_data *z);
2222
#define ZLIB_CLIENT_SHUTDOWN(z) if (z) { z_client_free(z); z = NULL; }
2323

2424
ssize_t zlib_write(struct z_data *z, int line, const char *buf, size_t len);
25+
ssize_t zlib_write_cb(struct z_data *z, int line, const char *buf, size_t len, ssize_t (*write_cb)(void *cbdata, const char *buf, size_t len), void *cbdata);
2526

2627
ssize_t zlib_read(struct z_data *z, int line, char *buf, size_t len);
28+
ssize_t zlib_read_cb(struct z_data *z, int line, char *buf, size_t len, ssize_t (*read_cb)(void *cbdata, char *buf, size_t len), void *cbdata);
2729

2830
int test_z_client_expect(struct z_data *z, int ms, const char *s, int line);
2931
int test_z_client_expect_buf(struct z_data *z, int ms, const char *s, int line, char *buf, size_t len);
32+
3033
int test_z_client_expect_eventually(struct z_data *z, int ms, const char *s, int line);
34+
int test_z_client_expect_eventually_readcb(struct z_data *z, int ms, const char *restrict s, int line, ssize_t (*read_cb)(void *cbdata, char *buf, size_t len), void *cbdata);
35+
3136
int test_z_client_expect_eventually_buf(struct z_data *z, int ms, const char *s, int line, char *buf, size_t len);
37+
int test_z_client_expect_eventually_buf_readcb(struct z_data *z, int ms, const char *restrict s, int line, char *restrict buf, size_t len, ssize_t (*read_cb)(void *cbdata, char *buf, size_t len), void *cbdata);
3238

3339
#define ZLIB_CLIENT_EXPECT(z, s) if (test_z_client_expect(z, SEC_MS(5), s, __LINE__)) { goto cleanup; }
3440
#define ZLIB_CLIENT_EXPECT_BUF(z, s, buf) if (test_z_client_expect_buf(z, SEC_MS(5), s, __LINE__, buf, sizeof(buf))) { goto cleanup; }
3541
#define ZLIB_CLIENT_EXPECT_EVENTUALLY(z, s) if (test_z_client_expect_eventually(z, SEC_MS(5), s, __LINE__)) { goto cleanup; }
42+
#define ZLIB_CLIENT_EXPECT_EVENTUALLY_READCB(cb, cbdata, z, s) if (test_z_client_expect_eventually_readcb(z, SEC_MS(5), s, __LINE__, cb, cbdata)) { goto cleanup; }
3643

3744
#define ZLIB_SWRITE(z, s) if (zlib_write(z, __LINE__, s, STRLEN(s)) < 0) { goto cleanup; }
45+
#define ZLIB_SWRITE_CB(cb, cbdata, z, s) if (zlib_write_cb(z, __LINE__, s, STRLEN(s), cb, cbdata) < 0) { goto cleanup; }

tests/test_imap_compress_tls.c

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* LBBS -- The Lightweight Bulletin Board System
3+
*
4+
* Copyright (C) 2026, Naveen Albert
5+
*
6+
* Naveen Albert <bbs@phreaknet.org>
7+
*
8+
* This program is free software, distributed under the terms of
9+
* the GNU General Public License Version 2. See the LICENSE file
10+
* at the top of the source tree.
11+
*/
12+
13+
/*! \file
14+
*
15+
* \brief IMAP COMPRESS/TLS Tests
16+
*
17+
* \author Naveen Albert <bbs@phreaknet.org>
18+
*/
19+
20+
#include "test.h"
21+
#include "tls.h"
22+
#include "compress.h"
23+
24+
#include <stdlib.h>
25+
#include <stdio.h>
26+
#include <unistd.h>
27+
#include <string.h>
28+
#include <sys/stat.h>
29+
30+
static int pre(void)
31+
{
32+
test_preload_module("io_tls.so");
33+
test_preload_module("io_compress.so");
34+
test_preload_module("mod_mail.so");
35+
test_preload_module("mod_mimeparse.so");
36+
test_load_module("net_imap.so");
37+
38+
TEST_ADD_CONFIG("tls.conf");
39+
TEST_ADD_CONFIG("mod_mail.conf");
40+
TEST_ADD_SUBCONFIG("tls", "net_imap.conf");
41+
42+
TEST_RESET_MKDIR(TEST_MAIL_DIR);
43+
return 0;
44+
}
45+
46+
static ssize_t zlib_tls_write(void *cbdata, const char *buf, size_t len)
47+
{
48+
SSL *ssl = cbdata;
49+
return tls_write(ssl, __LINE__, buf, len);
50+
}
51+
52+
static ssize_t zlib_tls_read(void *cbdata, char *buf, size_t len)
53+
{
54+
SSL *ssl = cbdata;
55+
return tls_read(ssl, __LINE__, buf, len);
56+
}
57+
58+
static int run(void)
59+
{
60+
SSL *ssl = NULL;
61+
struct z_data *z = NULL;
62+
int clientfd = -1;
63+
int res = -1;
64+
65+
clientfd = test_make_socket(993);
66+
REQUIRE_FD(clientfd);
67+
68+
/* Connect and immediately set up TLS */
69+
ssl = tls_client_new(clientfd);
70+
REQUIRE_SSL(ssl);
71+
72+
/* Log in */
73+
TLS_CLIENT_EXPECT(ssl, "* OK [CAPABILITY");
74+
TLS_SWRITE(ssl, "a1 LOGIN \"" TEST_USER "\" \"" TEST_PASS "\"" ENDL);
75+
TLS_CLIENT_EXPECT_EVENTUALLY(ssl, "a1 OK");
76+
77+
TLS_SWRITE(ssl, "a2 ID (\"name\" \"lbbs.test.client\" \"version\" \"" BBS_VERSION "\" NIL)" ENDL);
78+
TLS_CLIENT_EXPECT_EVENTUALLY(ssl, "a2 OK");
79+
80+
/* Now, set up compression on top of the TLS session */
81+
TLS_SWRITE(ssl, "a3 COMPRESS DEFLATE" ENDL);
82+
TLS_CLIENT_EXPECT_EVENTUALLY(ssl, "a3 OK"); /* The tagged response is without compress. After this, we use compression for the remainder of the session. */
83+
84+
z = z_client_new(clientfd); /* We pass in the socket file descriptor, but since we enabled compression on top of encryption, we need to use zlib on top of OpenSSL */
85+
REQUIRE_ZLIB_CLIENT(z);
86+
87+
ZLIB_SWRITE_CB(zlib_tls_write, ssl, z, "a4 NAMESPACE" ENDL);
88+
ZLIB_CLIENT_EXPECT_EVENTUALLY_READCB(zlib_tls_read, ssl, z, "a4 OK");
89+
90+
res = 0;
91+
92+
cleanup:
93+
/* This test doesn't actually work, and it doesn't really matter. The point is to ensure there are no file descriptor leaks regardless of what the client does.
94+
* Therefore, we always return 0, and when the test is run under valgrind, it should pass as long as we don't leak any file descriptors
95+
* due to the nested transformation. */
96+
res = 0;
97+
ZLIB_CLIENT_SHUTDOWN(z);
98+
SSL_SHUTDOWN(ssl);
99+
close_if(clientfd);
100+
return res;
101+
}
102+
103+
TEST_MODULE_INFO_STANDARD("IMAP COMPRESS/TLS Tests");

0 commit comments

Comments
 (0)