summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDirk Engling <erdgeist@erdgeist.org>2026-10-30 01:00:52 +0100
committerDirk Engling <erdgeist@erdgeist.org>2026-10-30 01:00:52 +0100
commit79f4110365fe85f4377c50f0893134a8d5e45575 (patch)
treee69f5ceae67e4f283bede83f6649b7df0ded7076
parentd92945fe3a7960cfc7ddfd70a9a37d6db666c1e2 (diff)
Consolidate all tcp/http tear down into a single http_close function to prevent a rare UAF for expiring connections
-rw-r--r--Makefile4
-rw-r--r--opentracker.c27
-rw-r--r--ot_http.c36
-rw-r--r--ot_http.h3
-rw-r--r--tests/integration/test_expiry.py42
-rw-r--r--trackerlogic.h4
6 files changed, 83 insertions, 33 deletions
diff --git a/Makefile b/Makefile
index f79d6a6..3ce127e 100644
--- a/Makefile
+++ b/Makefile
@@ -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.
130PYTHON?=python3 130PYTHON?=python3
131FEATURES_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.
133FEATURES_integration=$(FEATURES) -DWANT_IP_FROM_QUERY_STRING -DWANT_ACCESSLIST_BLACK -DWANT_KEEPALIVE -DOT_CLIENT_TIMEOUT=5 -DOT_CLIENT_TIMEOUT_CHECKINTERVAL=1
132CFLAGS_integration=$(CFLAGS) $(OPTS_debug) $(FEATURES_integration) -fsanitize=address,undefined -fno-omit-frame-pointer 134CFLAGS_integration=$(CFLAGS) $(OPTS_debug) $(FEATURES_integration) -fsanitize=address,undefined -fno-omit-frame-pointer
133LDFLAGS_integration=$(LDFLAGS) -fsanitize=address,undefined 135LDFLAGS_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
118static 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
135static void handle_read(const int64_t sock, struct ot_workstruct *ws) { 118static 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
211static void handle_accept(const int64_t serversocket, struct ot_workstruct *ws) { 194static 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
diff --git a/ot_http.c b/ot_http.c
index 927d618..c2f7e5e 100644
--- a/ot_http.c
+++ b/ot_http.c
@@ -38,12 +38,34 @@ ssize_t g_stats_path_len;
38 38
39enum { SUCCESS_HTTP_HEADER_LENGTH = 80, SUCCESS_HTTP_SIZE_OFF = 17 }; 39enum { 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. */
44void 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
41static void http_senddata(const int64_t sock, struct ot_workstruct *ws) { 63static 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 }
diff --git a/ot_http.h b/ot_http.h
index 1ccabfd..0f351c1 100644
--- a/ot_http.h
+++ b/ot_http.h
@@ -37,6 +37,9 @@ struct http_data {
37void http_interest_add(const int64_t s, struct http_data *cookie, int what); 37void http_interest_add(const int64_t s, struct http_data *cookie, int what);
38void http_interest_drop(const int64_t s, struct http_data *cookie, int what); 38void 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 */
41void http_close(const int64_t s);
42
40ssize_t http_handle_request(const int64_t s, struct ot_workstruct *ws); 43ssize_t http_handle_request(const int64_t s, struct ot_workstruct *ws);
41ssize_t http_sendiovecdata(const int64_t s, struct ot_workstruct *ws, size_t iovec_entries, struct iovec *iovector, int is_partial); 44ssize_t http_sendiovecdata(const int64_t s, struct ot_workstruct *ws, size_t iovec_entries, struct iovec *iovector, int is_partial);
42ssize_t http_issue_error(const int64_t s, struct ot_workstruct *ws, int code); 45ssize_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
3A plain request is answered and the connection closed by http_senddata, not
4by the sweep. If that path leaves the descriptor's slot behind, the sweep
5walks into it once the client timeout has passed and frees a cookie that is
6already gone. Under the sanitizer build that is reported, and teardown turns
7it into a failure.
8
9The two numbers below mirror OT_CLIENT_TIMEOUT and
10OT_CLIENT_TIMEOUT_CHECKINTERVAL in FEATURES_integration.
11"""
12
13import time
14import unittest
15
16from harness import TrackerTestCase
17
18CLIENT_TIMEOUT = 5
19CHECK_INTERVAL = 1
20
21
22class 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)