diff options
| author | Dirk Engling <erdgeist@erdgeist.org> | 2026-10-30 01:00:52 +0100 |
|---|---|---|
| committer | Dirk Engling <erdgeist@erdgeist.org> | 2026-10-30 01:00:52 +0100 |
| commit | 79f4110365fe85f4377c50f0893134a8d5e45575 (patch) | |
| tree | e69f5ceae67e4f283bede83f6649b7df0ded7076 | |
| parent | d92945fe3a7960cfc7ddfd70a9a37d6db666c1e2 (diff) | |
Consolidate all tcp/http tear down into a single http_close function to prevent a rare UAF for expiring connections
| -rw-r--r-- | Makefile | 4 | ||||
| -rw-r--r-- | opentracker.c | 27 | ||||
| -rw-r--r-- | ot_http.c | 36 | ||||
| -rw-r--r-- | ot_http.h | 3 | ||||
| -rw-r--r-- | tests/integration/test_expiry.py | 42 | ||||
| -rw-r--r-- | trackerlogic.h | 4 |
6 files changed, 83 insertions, 33 deletions
| @@ -128,7 +128,9 @@ check-sanitize: $(TESTBIN).sanitize | |||
| 128 | # cannot reach. Built with the sanitizers on, so the tracker reports its own | 128 | # cannot reach. Built with the sanitizers on, so the tracker reports its own |
| 129 | # memory errors and the harness turns those into failures. | 129 | # memory errors and the harness turns those into failures. |
| 130 | PYTHON?=python3 | 130 | PYTHON?=python3 |
| 131 | FEATURES_integration=$(FEATURES) -DWANT_IP_FROM_QUERY_STRING -DWANT_ACCESSLIST_BLACK -DWANT_KEEPALIVE | 131 | # The client timeout is cut right down so that tests can reach the expiry |
| 132 | # sweep. tests/integration/test_expiry.py mirrors these two numbers. | ||
| 133 | FEATURES_integration=$(FEATURES) -DWANT_IP_FROM_QUERY_STRING -DWANT_ACCESSLIST_BLACK -DWANT_KEEPALIVE -DOT_CLIENT_TIMEOUT=5 -DOT_CLIENT_TIMEOUT_CHECKINTERVAL=1 | ||
| 132 | CFLAGS_integration=$(CFLAGS) $(OPTS_debug) $(FEATURES_integration) -fsanitize=address,undefined -fno-omit-frame-pointer | 134 | CFLAGS_integration=$(CFLAGS) $(OPTS_debug) $(FEATURES_integration) -fsanitize=address,undefined -fno-omit-frame-pointer |
| 133 | LDFLAGS_integration=$(LDFLAGS) -fsanitize=address,undefined | 135 | LDFLAGS_integration=$(LDFLAGS) -fsanitize=address,undefined |
| 134 | 136 | ||
diff --git a/opentracker.c b/opentracker.c index e0f16d7..6160696 100644 --- a/opentracker.c +++ b/opentracker.c | |||
| @@ -115,29 +115,12 @@ static ssize_t header_complete(char *request, ssize_t byte_count) { | |||
| 115 | return 0; | 115 | return 0; |
| 116 | } | 116 | } |
| 117 | 117 | ||
| 118 | static void handle_dead(const int64_t sock) { | ||
| 119 | struct http_data *cookie = fdstate_cookie(sock); | ||
| 120 | if (cookie) { | ||
| 121 | if (cookie->batch) | ||
| 122 | batch_reset(cookie->batch); | ||
| 123 | free(cookie->batch); | ||
| 124 | buf_reset(&cookie->request); | ||
| 125 | if (cookie->flag & (STRUCT_HTTP_FLAG_WAITINGFORTASK | STRUCT_HTTP_FLAG_CHUNKED_IN_TRANSFER)) | ||
| 126 | mutex_workqueue_canceltask(sock); | ||
| 127 | free(cookie); | ||
| 128 | } | ||
| 129 | /* The generation has to move before the kernel can reissue the number */ | ||
| 130 | fdstate_close(sock); | ||
| 131 | event_del((int)sock); | ||
| 132 | close((int)sock); | ||
| 133 | } | ||
| 134 | |||
| 135 | static void handle_read(const int64_t sock, struct ot_workstruct *ws) { | 118 | static void handle_read(const int64_t sock, struct ot_workstruct *ws) { |
| 136 | struct http_data *cookie = fdstate_cookie(sock); | 119 | struct http_data *cookie = fdstate_cookie(sock); |
| 137 | ssize_t byte_count = read((int)sock, ws->inbuf, G_INBUF_SIZE); | 120 | ssize_t byte_count = read((int)sock, ws->inbuf, G_INBUF_SIZE); |
| 138 | 121 | ||
| 139 | if (byte_count == 0 || (byte_count == -1 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)) { | 122 | if (byte_count == 0 || (byte_count == -1 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)) { |
| 140 | handle_dead(sock); | 123 | http_close(sock); |
| 141 | return; | 124 | return; |
| 142 | } | 125 | } |
| 143 | 126 | ||
| @@ -156,7 +139,7 @@ static void handle_read(const int64_t sock, struct ot_workstruct *ws) { | |||
| 156 | ws->request_size = byte_count; | 139 | ws->request_size = byte_count; |
| 157 | http_handle_request(sock, ws); | 140 | http_handle_request(sock, ws); |
| 158 | } else if (buf_cat(&cookie->request, ws->inbuf, (size_t)byte_count)) | 141 | } else if (buf_cat(&cookie->request, ws->inbuf, (size_t)byte_count)) |
| 159 | handle_dead(sock); | 142 | http_close(sock); |
| 160 | return; | 143 | return; |
| 161 | } | 144 | } |
| 162 | 145 | ||
| @@ -187,7 +170,7 @@ static void handle_write(const int64_t sock) { | |||
| 187 | 170 | ||
| 188 | if (cookie->batch) { | 171 | if (cookie->batch) { |
| 189 | if (batch_send(sock, cookie->batch) == BATCH_FAILED) { | 172 | if (batch_send(sock, cookie->batch) == BATCH_FAILED) { |
| 190 | handle_dead(sock); | 173 | http_close(sock); |
| 191 | return; | 174 | return; |
| 192 | } | 175 | } |
| 193 | if (batch_pending(cookie->batch)) | 176 | if (batch_pending(cookie->batch)) |
| @@ -205,7 +188,7 @@ static void handle_write(const int64_t sock) { | |||
| 205 | http_interest_add(sock, cookie, EVENT_IN); | 188 | http_interest_add(sock, cookie, EVENT_IN); |
| 206 | fdstate_set_expiry(sock, time_future_seconds(OT_CLIENT_TIMEOUT)); | 189 | fdstate_set_expiry(sock, time_future_seconds(OT_CLIENT_TIMEOUT)); |
| 207 | } else | 190 | } else |
| 208 | handle_dead(sock); | 191 | http_close(sock); |
| 209 | } | 192 | } |
| 210 | 193 | ||
| 211 | static void handle_accept(const int64_t serversocket, struct ot_workstruct *ws) { | 194 | static void handle_accept(const int64_t serversocket, struct ot_workstruct *ws) { |
| @@ -303,7 +286,7 @@ static void *server_mainloop(void *args) { | |||
| 303 | int cursor = 0; | 286 | int cursor = 0; |
| 304 | 287 | ||
| 305 | while ((sock = fdstate_next_expired(time_now_seconds(), &cursor)) != -1) | 288 | while ((sock = fdstate_next_expired(time_now_seconds(), &cursor)) != -1) |
| 306 | handle_dead(sock); | 289 | http_close(sock); |
| 307 | next_timeout_check = time_future_seconds(OT_CLIENT_TIMEOUT_CHECKINTERVAL); | 290 | next_timeout_check = time_future_seconds(OT_CLIENT_TIMEOUT_CHECKINTERVAL); |
| 308 | } | 291 | } |
| 309 | 292 | ||
| @@ -38,12 +38,34 @@ ssize_t g_stats_path_len; | |||
| 38 | 38 | ||
| 39 | enum { SUCCESS_HTTP_HEADER_LENGTH = 80, SUCCESS_HTTP_SIZE_OFF = 17 }; | 39 | enum { SUCCESS_HTTP_HEADER_LENGTH = 80, SUCCESS_HTTP_SIZE_OFF = 17 }; |
| 40 | 40 | ||
| 41 | /* Give up a connection: the one place that knows everything hanging off it. | ||
| 42 | The slot is released before anything is freed, so that a late event or the | ||
| 43 | expiry sweep finds a closed descriptor rather than a freed cookie. */ | ||
| 44 | void http_close(const int64_t sock) { | ||
| 45 | struct http_data *cookie = fdstate_cookie(sock); | ||
| 46 | |||
| 47 | fdstate_close((int)sock); | ||
| 48 | |||
| 49 | if (cookie) { | ||
| 50 | if (cookie->batch) | ||
| 51 | batch_reset(cookie->batch); | ||
| 52 | free(cookie->batch); | ||
| 53 | buf_reset(&cookie->request); | ||
| 54 | if (cookie->flag & (STRUCT_HTTP_FLAG_WAITINGFORTASK | STRUCT_HTTP_FLAG_CHUNKED_IN_TRANSFER)) | ||
| 55 | mutex_workqueue_canceltask(sock); | ||
| 56 | free(cookie); | ||
| 57 | } | ||
| 58 | |||
| 59 | event_del((int)sock); | ||
| 60 | close((int)sock); | ||
| 61 | } | ||
| 62 | |||
| 41 | static void http_senddata(const int64_t sock, struct ot_workstruct *ws) { | 63 | static void http_senddata(const int64_t sock, struct ot_workstruct *ws) { |
| 42 | struct http_data *cookie = fdstate_cookie(sock); | 64 | struct http_data *cookie = fdstate_cookie(sock); |
| 43 | ssize_t written_size; | 65 | ssize_t written_size; |
| 44 | 66 | ||
| 45 | if (!cookie) { | 67 | if (!cookie) { |
| 46 | event_del((int)sock), close((int)sock); | 68 | http_close(sock); |
| 47 | return; | 69 | return; |
| 48 | } | 70 | } |
| 49 | 71 | ||
| @@ -65,9 +87,7 @@ static void http_senddata(const int64_t sock, struct ot_workstruct *ws) { | |||
| 65 | 87 | ||
| 66 | written_size = write(sock, ws->reply, ws->reply_size); | 88 | written_size = write(sock, ws->reply, ws->reply_size); |
| 67 | if ((written_size < 0) || ((written_size == ws->reply_size) && !ws->keep_alive)) { | 89 | if ((written_size < 0) || ((written_size == ws->reply_size) && !ws->keep_alive)) { |
| 68 | buf_reset(&cookie->request); | 90 | http_close(sock); |
| 69 | free(cookie); | ||
| 70 | event_del((int)sock), close((int)sock); | ||
| 71 | return; | 91 | return; |
| 72 | } | 92 | } |
| 73 | 93 | ||
| @@ -75,9 +95,7 @@ static void http_senddata(const int64_t sock, struct ot_workstruct *ws) { | |||
| 75 | char *outbuf; | 95 | char *outbuf; |
| 76 | 96 | ||
| 77 | if (!(outbuf = malloc(ws->reply_size - written_size))) { | 97 | if (!(outbuf = malloc(ws->reply_size - written_size))) { |
| 78 | buf_reset(&cookie->request); | 98 | http_close(sock); |
| 79 | free(cookie); | ||
| 80 | event_del((int)sock), close((int)sock); | ||
| 81 | return; | 99 | return; |
| 82 | } | 100 | } |
| 83 | 101 | ||
| @@ -86,9 +104,7 @@ static void http_senddata(const int64_t sock, struct ot_workstruct *ws) { | |||
| 86 | cookie->batch = calloc(1, sizeof(ot_batch)); | 104 | cookie->batch = calloc(1, sizeof(ot_batch)); |
| 87 | if (!cookie->batch) { | 105 | if (!cookie->batch) { |
| 88 | free(outbuf); | 106 | free(outbuf); |
| 89 | buf_reset(&cookie->request); | 107 | http_close(sock); |
| 90 | free(cookie); | ||
| 91 | event_del((int)sock), close((int)sock); | ||
| 92 | return; | 108 | return; |
| 93 | } | 109 | } |
| 94 | } | 110 | } |
| @@ -37,6 +37,9 @@ struct http_data { | |||
| 37 | void http_interest_add(const int64_t s, struct http_data *cookie, int what); | 37 | void http_interest_add(const int64_t s, struct http_data *cookie, int what); |
| 38 | void http_interest_drop(const int64_t s, struct http_data *cookie, int what); | 38 | void http_interest_drop(const int64_t s, struct http_data *cookie, int what); |
| 39 | 39 | ||
| 40 | /* Close a connection and release everything that belongs to it */ | ||
| 41 | void http_close(const int64_t s); | ||
| 42 | |||
| 40 | ssize_t http_handle_request(const int64_t s, struct ot_workstruct *ws); | 43 | ssize_t http_handle_request(const int64_t s, struct ot_workstruct *ws); |
| 41 | ssize_t http_sendiovecdata(const int64_t s, struct ot_workstruct *ws, size_t iovec_entries, struct iovec *iovector, int is_partial); | 44 | ssize_t http_sendiovecdata(const int64_t s, struct ot_workstruct *ws, size_t iovec_entries, struct iovec *iovector, int is_partial); |
| 42 | ssize_t http_issue_error(const int64_t s, struct ot_workstruct *ws, int code); | 45 | ssize_t http_issue_error(const int64_t s, struct ot_workstruct *ws, int code); |
diff --git a/tests/integration/test_expiry.py b/tests/integration/test_expiry.py new file mode 100644 index 0000000..4466724 --- /dev/null +++ b/tests/integration/test_expiry.py | |||
| @@ -0,0 +1,42 @@ | |||
| 1 | """What the expiry sweep finds after a connection has already gone. | ||
| 2 | |||
| 3 | A plain request is answered and the connection closed by http_senddata, not | ||
| 4 | by the sweep. If that path leaves the descriptor's slot behind, the sweep | ||
| 5 | walks into it once the client timeout has passed and frees a cookie that is | ||
| 6 | already gone. Under the sanitizer build that is reported, and teardown turns | ||
| 7 | it into a failure. | ||
| 8 | |||
| 9 | The two numbers below mirror OT_CLIENT_TIMEOUT and | ||
| 10 | OT_CLIENT_TIMEOUT_CHECKINTERVAL in FEATURES_integration. | ||
| 11 | """ | ||
| 12 | |||
| 13 | import time | ||
| 14 | import unittest | ||
| 15 | |||
| 16 | from harness import TrackerTestCase | ||
| 17 | |||
| 18 | CLIENT_TIMEOUT = 5 | ||
| 19 | CHECK_INTERVAL = 1 | ||
| 20 | |||
| 21 | |||
| 22 | class ExpirySweep(TrackerTestCase): | ||
| 23 | def test_an_answered_request_leaves_nothing_behind(self): | ||
| 24 | info_hash = self.hash_for("expiry-sweep") | ||
| 25 | self.assertEqual(self.tracker.announce(info_hash)[b"complete"], 1) | ||
| 26 | |||
| 27 | # Long enough for the slot to expire and for a sweep to visit it | ||
| 28 | time.sleep(CLIENT_TIMEOUT + 3 * CHECK_INTERVAL) | ||
| 29 | |||
| 30 | self.assertEqual(self.tracker.torrent_count(), 1, "the tracker stopped serving while sweeping") | ||
| 31 | |||
| 32 | def test_a_connection_that_never_speaks_is_swept(self): | ||
| 33 | """The other side of it: a slot that really is live must still go.""" | ||
| 34 | import socket | ||
| 35 | |||
| 36 | idle = socket.create_connection(("127.0.0.1", self.tracker.port), timeout=5) | ||
| 37 | try: | ||
| 38 | time.sleep(CLIENT_TIMEOUT + 3 * CHECK_INTERVAL) | ||
| 39 | idle.settimeout(5) | ||
| 40 | self.assertEqual(idle.recv(64), b"", "the tracker held a connection that never sent a request") | ||
| 41 | finally: | ||
| 42 | idle.close() | ||
diff --git a/trackerlogic.h b/trackerlogic.h index 7d0693a..d269267 100644 --- a/trackerlogic.h +++ b/trackerlogic.h | |||
| @@ -32,8 +32,12 @@ typedef struct { | |||
| 32 | #define OT_TIME_SIZE 1 | 32 | #define OT_TIME_SIZE 1 |
| 33 | 33 | ||
| 34 | /* Some tracker behaviour tunable */ | 34 | /* Some tracker behaviour tunable */ |
| 35 | #ifndef OT_CLIENT_TIMEOUT | ||
| 35 | #define OT_CLIENT_TIMEOUT 30 | 36 | #define OT_CLIENT_TIMEOUT 30 |
| 37 | #endif | ||
| 38 | #ifndef OT_CLIENT_TIMEOUT_CHECKINTERVAL | ||
| 36 | #define OT_CLIENT_TIMEOUT_CHECKINTERVAL 10 | 39 | #define OT_CLIENT_TIMEOUT_CHECKINTERVAL 10 |
| 40 | #endif | ||
| 37 | #define OT_CLIENT_TIMEOUT_SEND (60 * 15) | 41 | #define OT_CLIENT_TIMEOUT_SEND (60 * 15) |
| 38 | #define OT_CLIENT_REQUEST_INTERVAL (60 * 30) | 42 | #define OT_CLIENT_REQUEST_INTERVAL (60 * 30) |
| 39 | #define OT_CLIENT_REQUEST_VARIATION (60 * 6) | 43 | #define OT_CLIENT_REQUEST_VARIATION (60 * 6) |
