diff options
| author | Dirk Engling <erdgeist@erdgeist.org> | 2026-11-06 21:17:54 +0100 |
|---|---|---|
| committer | Dirk Engling <erdgeist@erdgeist.org> | 2026-11-06 21:17:54 +0100 |
| commit | a29e606b6f97c00d0b7e80cb0592bca54f92f2c9 (patch) | |
| tree | 3907ef58492c459bd64e1381117f16083c4968cf | |
| parent | 1d7ed6b295a5a9509d8194ce2a03255f0fb5a5fc (diff) | |
Fix a theoretical bug where a short request packet yields an oob read
| -rw-r--r-- | src/ot_http.c | 6 | ||||
| -rw-r--r-- | tests/integration/test_keepalive.py | 16 |
2 files changed, 19 insertions, 3 deletions
diff --git a/src/ot_http.c b/src/ot_http.c index fc49333..1b5281c 100644 --- a/src/ot_http.c +++ b/src/ot_http.c | |||
| @@ -762,8 +762,8 @@ ssize_t http_handle_request(const int64_t sock, struct ot_workstruct *ws) { | |||
| 762 | } | 762 | } |
| 763 | } | 763 | } |
| 764 | 764 | ||
| 765 | /* This one implicitely tests strlen < 5, too -- remember, it is \n terminated */ | 765 | /* The request need not be NUL terminated and can be shorter than the literal */ |
| 766 | if (memcmp(read_ptr, "GET /", 5)) | 766 | if (ws->request_size < 5 || memcmp(read_ptr, "GET /", 5)) |
| 767 | HTTPERROR_400; | 767 | HTTPERROR_400; |
| 768 | 768 | ||
| 769 | /* Skip leading '/' */ | 769 | /* Skip leading '/' */ |
| @@ -786,7 +786,7 @@ ssize_t http_handle_request(const int64_t sock, struct ot_workstruct *ws) { | |||
| 786 | if ((*write_ptr == 'a') || (*write_ptr == '?')) | 786 | if ((*write_ptr == 'a') || (*write_ptr == '?')) |
| 787 | http_handle_announce(sock, ws, read_ptr); | 787 | http_handle_announce(sock, ws, read_ptr); |
| 788 | #ifdef WANT_FULLSCRAPE | 788 | #ifdef WANT_FULLSCRAPE |
| 789 | else if (!memcmp(write_ptr, "scrape HTTP/", 12)) | 789 | else if (ws->request + ws->request_size - write_ptr >= 12 && !memcmp(write_ptr, "scrape HTTP/", 12)) |
| 790 | http_handle_fullscrape(sock, ws); | 790 | http_handle_fullscrape(sock, ws); |
| 791 | #endif | 791 | #endif |
| 792 | /* This is the hardcore match for scrape */ | 792 | /* This is the hardcore match for scrape */ |
diff --git a/tests/integration/test_keepalive.py b/tests/integration/test_keepalive.py index 2c724fc..f9aa468 100644 --- a/tests/integration/test_keepalive.py +++ b/tests/integration/test_keepalive.py | |||
| @@ -107,6 +107,22 @@ class KeepAlive(TrackerTestCase): | |||
| 107 | finally: | 107 | finally: |
| 108 | c.close() | 108 | c.close() |
| 109 | 109 | ||
| 110 | def test_a_tiny_split_request_does_not_overread(self): | ||
| 111 | """A request that arrives in pieces and completes as a buffer shorter | ||
| 112 | than the dispatch literals (\"GET /\" etc.) used to read past the | ||
| 113 | exactly sized buffer. It must be rejected without a sanitizer hit.""" | ||
| 114 | c = self.tracker.connect() | ||
| 115 | try: | ||
| 116 | c.sock.sendall(b"\r\n") | ||
| 117 | time.sleep(0.05) | ||
| 118 | c.sock.sendall(b"\r\n") # completes as a 4-byte buffer | ||
| 119 | time.sleep(0.1) | ||
| 120 | finally: | ||
| 121 | c.close() | ||
| 122 | # the tracker must still answer; tearDown's stop() surfaces any sanitizer abort | ||
| 123 | answer = self.tracker.announce(self.hash_for("after-tiny"), port=6881, left=0) | ||
| 124 | self.assertNotIn(b"failure reason", answer, "the tracker stopped serving after a tiny request") | ||
| 125 | |||
| 110 | def test_a_split_request_answered_by_a_worker_does_not_spin(self): | 126 | def test_a_split_request_answered_by_a_worker_does_not_spin(self): |
| 111 | """A reply that a worker produces does not go through the send path, | 127 | """A reply that a worker produces does not go through the send path, |
| 112 | so the request stays in the buffer the read loop is reading. The loop | 128 | so the request stays in the buffer the read loop is reading. The loop |
