Skip to content

Commit 4bf6a49

Browse files
authored
[BUGFIX] Track allocated size in response and prevent double-free in cleanup (#35)
* [BUGFIX] Track allocated size in response and prevent double-free in cleanup * [TEST] Add connection pool tests for double-free bug fix (Issue #33) - Add comprehensive test suite for connection pool edge cases - Test empty body responses after multiple non-empty body requests - Test alternating request patterns and stress scenarios - Verify fix for SSL double-free crash (SIGABRT) - All 5 tests pass, confirming the bug is fixed * [TEST] Add critical double-free bug reproduction test - test_double_free_bug_reproduction() now first test, most important - Documents exact bug scenario and crash conditions - Uses real httpmorph-bin server with /status/200 empty body endpoint - Triggers Connection: close -> pool_connection_destroy -> double-free - Without fix: crashes with SIGABRT (exit code 134) - With fix: passes successfully - All 6 connection pool tests pass * [ENHANCEMENT] Add retry logic to real proxy tests for improved reliability * [BUGFIX] Enhance error handling and memory safety in various components * [ENHANCEMENT] Bump version to 0.2.7 in pyproject.toml
1 parent 2e1a55b commit 4bf6a49

File tree

11 files changed

+575
-103
lines changed

11 files changed

+575
-103
lines changed

.env.example

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Example environment variables for httpmorph testing
2+
# Copy this file to .env and fill in your actual values
3+
4+
# Proxy configuration for testing
5+
# Format: http://username:password@proxy-host:port
6+
# Or for country-specific proxy: http://username:password_country-CountryName@proxy-host:port
7+
TEST_PROXY_URL=http://your-username:your-password@proxy.example.com:31112
8+
9+
# Proxy configuration for examples (separate username/password)
10+
PROXY_URL=http://proxy.example.com:31112
11+
PROXY_USERNAME=your-username
12+
PROXY_PASSWORD=your-password
13+
14+
# HTTPBin testing host
15+
# Use httpmorph-bin.bytetunnels.com for reliable testing
16+
# (httpbin.org can be flaky and rate-limited)
17+
TEST_HTTPBIN_HOST=httpbin-of-your-own.com

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
99

1010
[project]
1111
name = "httpmorph"
12-
version = "0.2.6"
12+
version = "0.2.7"
1313
description = "A Python HTTP client focused on mimicking browser fingerprints."
1414
readme = "README.md"
1515
requires-python = ">=3.8"

src/core/async_request.c

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,6 +1339,43 @@ static int step_receiving_headers(async_request_t *req) {
13391339
}
13401340
async_request_set_error(req, -1, "SSL connection closed before complete headers");
13411341
return ASYNC_STATUS_ERROR;
1342+
} else if (err == SSL_ERROR_SYSCALL && received == 0) {
1343+
/* SSL_ERROR_SYSCALL with received==0 and errno==0 means clean connection close (EOF) */
1344+
#ifdef _WIN32
1345+
int sys_err = WSAGetLastError();
1346+
bool is_eof = (sys_err == 0);
1347+
#else
1348+
bool is_eof = (errno == 0);
1349+
#endif
1350+
1351+
if (is_eof) {
1352+
/* Connection closed cleanly - treat like SSL_ERROR_ZERO_RETURN */
1353+
if (req->recv_len >= 4) {
1354+
bool has_complete_headers = false;
1355+
for (size_t i = 0; i <= req->recv_len - 4; i++) {
1356+
if (memcmp(req->recv_buf + i, "\r\n\r\n", 4) == 0) {
1357+
has_complete_headers = true;
1358+
break;
1359+
}
1360+
}
1361+
if (has_complete_headers) {
1362+
/* Have complete headers, process them */
1363+
received = 0; /* Set to 0 to skip recv_len increment below */
1364+
goto ssl_process_headers;
1365+
}
1366+
}
1367+
async_request_set_error(req, -1, "Connection closed by peer before complete headers");
1368+
return ASYNC_STATUS_ERROR;
1369+
}
1370+
/* Fall through to regular error handling if not EOF */
1371+
#ifdef _WIN32
1372+
snprintf(req->error_msg, sizeof(req->error_msg), "SSL read failed: system error %d (WSAERR)", sys_err);
1373+
#else
1374+
snprintf(req->error_msg, sizeof(req->error_msg), "SSL read failed: system error %d (errno)", errno);
1375+
#endif
1376+
req->state = ASYNC_STATE_ERROR;
1377+
req->error_code = err;
1378+
return ASYNC_STATUS_ERROR;
13421379
} else {
13431380
/* Get detailed SSL error */
13441381
char err_buf[256];
@@ -1707,6 +1744,7 @@ static int step_receiving_body(async_request_t *req) {
17071744
if (req->response->body) {
17081745
memcpy(req->response->body, req->recv_buf + body_start, req->content_length);
17091746
req->response->body_len = req->content_length;
1747+
req->response->_body_actual_size = req->content_length; /* Track allocated size */
17101748
}
17111749
}
17121750
req->response->status_code = 200; // TODO: Parse from headers
@@ -1914,6 +1952,7 @@ static int step_receiving_body(async_request_t *req) {
19141952
if (req->response->body) {
19151953
memcpy(req->response->body, req->recv_buf + body_start, req->content_length);
19161954
req->response->body_len = req->content_length;
1955+
req->response->_body_actual_size = req->content_length; /* Track allocated size */
19171956
}
19181957
}
19191958
req->response->status_code = 200; // TODO: Parse from headers

src/core/cookies.c

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,13 @@ char* httpmorph_get_cookies_for_request(httpmorph_session_t *session,
112112
if (!session || !domain || !path) return NULL;
113113
if (session->cookie_count == 0) return NULL;
114114

115-
/* Build cookie header value */
116-
char *cookie_header = malloc(4096);
115+
/* Build cookie header value with bounds checking */
116+
const size_t buffer_size = 4096;
117+
char *cookie_header = malloc(buffer_size);
117118
if (!cookie_header) return NULL;
118119

119120
cookie_header[0] = '\0';
121+
size_t used = 0;
120122
bool first = true;
121123

122124
cookie_t *cookie = session->cookies;
@@ -128,12 +130,22 @@ char* httpmorph_get_cookies_for_request(httpmorph_session_t *session,
128130
bool secure_match = (!cookie->secure || is_secure);
129131

130132
if (domain_match && path_match && secure_match) {
133+
/* Calculate space needed: "; " + name + "=" + value */
134+
size_t needed = strlen(cookie->name) + 1 + strlen(cookie->value);
135+
if (!first) needed += 2; /* "; " prefix */
136+
137+
/* Check if we have space (leave room for null terminator) */
138+
if (used + needed >= buffer_size - 1) {
139+
/* Buffer would overflow - stop adding cookies */
140+
break;
141+
}
142+
143+
/* Safe concatenation with bounds checking */
131144
if (!first) {
132-
strcat(cookie_header, "; ");
145+
used += snprintf(cookie_header + used, buffer_size - used, "; ");
133146
}
134-
strcat(cookie_header, cookie->name);
135-
strcat(cookie_header, "=");
136-
strcat(cookie_header, cookie->value);
147+
used += snprintf(cookie_header + used, buffer_size - used, "%s=%s",
148+
cookie->name, cookie->value);
137149
first = false;
138150
}
139151

src/core/core.c

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,17 @@ httpmorph_response_t* httpmorph_request_execute(
166166
ssl = pooled_conn->ssl;
167167
use_http2 = pooled_conn->is_http2; /* Use same protocol as pooled connection */
168168

169+
/* Restore TLS info from pooled connection BEFORE potential destruction */
170+
if (sockfd >= 0) {
171+
/* Connection reused - no connect/TLS time */
172+
connect_time = 0;
173+
response->tls_time_us = 0;
174+
175+
if (pooled_conn->ja3_fingerprint) {
176+
response->ja3_fingerprint = strdup(pooled_conn->ja3_fingerprint);
177+
}
178+
}
179+
169180
/* For SSL connections, verify still valid before reuse */
170181
if (ssl) {
171182
int shutdown_state = SSL_get_shutdown(ssl);
@@ -177,17 +188,6 @@ httpmorph_response_t* httpmorph_request_execute(
177188
ssl = NULL;
178189
}
179190
}
180-
181-
if (sockfd >= 0) {
182-
/* Connection reused - no connect/TLS time */
183-
connect_time = 0;
184-
response->tls_time_us = 0;
185-
186-
/* Restore TLS info from pooled connection */
187-
if (pooled_conn->ja3_fingerprint) {
188-
response->ja3_fingerprint = strdup(pooled_conn->ja3_fingerprint);
189-
}
190-
}
191191
} else {
192192
}
193193
}
@@ -457,6 +457,9 @@ httpmorph_response_t* httpmorph_request_execute(
457457
/* Server wants to close - don't pool */
458458
if (pooled_conn) {
459459
pool_connection_destroy(pooled_conn);
460+
/* Clear local references to prevent double-free - pool_connection_destroy already freed them */
461+
sockfd = -1;
462+
ssl = NULL;
460463
pooled_conn = NULL;
461464
}
462465
/* Let normal cleanup close the connection */
@@ -473,16 +476,27 @@ httpmorph_response_t* httpmorph_request_execute(
473476
conn_to_pool = NULL;
474477
} else {
475478
conn_to_pool = pool_connection_create(host, port, sockfd, ssl, use_http2);
476-
/* Store TLS info in pooled connection for future reuse */
479+
/* Store TLS info in pooled connection for future reuse with error checking */
477480
if (conn_to_pool && ssl) {
481+
bool alloc_failed = false;
482+
478483
if (response->ja3_fingerprint) {
479484
conn_to_pool->ja3_fingerprint = strdup(response->ja3_fingerprint);
485+
if (!conn_to_pool->ja3_fingerprint) alloc_failed = true;
480486
}
481-
if (response->tls_version) {
487+
if (response->tls_version && !alloc_failed) {
482488
conn_to_pool->tls_version = strdup(response->tls_version);
489+
if (!conn_to_pool->tls_version) alloc_failed = true;
483490
}
484-
if (response->tls_cipher) {
491+
if (response->tls_cipher && !alloc_failed) {
485492
conn_to_pool->tls_cipher = strdup(response->tls_cipher);
493+
if (!conn_to_pool->tls_cipher) alloc_failed = true;
494+
}
495+
496+
/* If any allocation failed, destroy connection instead of pooling */
497+
if (alloc_failed) {
498+
pool_connection_destroy(conn_to_pool);
499+
conn_to_pool = NULL;
486500
}
487501
}
488502
/* Store proxy info for proxy connections */

src/core/http2_logic.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ static int http2_on_header_callback(nghttp2_session *session,
106106
stream_data = (http2_stream_data_t *)user_data;
107107
}
108108

109+
/* Safety check: if stream_data is still NULL, reject callback */
110+
if (!stream_data) {
111+
return NGHTTP2_ERR_CALLBACK_FAILURE;
112+
}
113+
109114
if (frame->hd.type != NGHTTP2_HEADERS || frame->headers.cat != NGHTTP2_HCAT_RESPONSE) {
110115
return 0;
111116
}
@@ -135,6 +140,11 @@ static int http2_on_data_chunk_recv_callback(nghttp2_session *session, uint8_t f
135140
stream_data = (http2_stream_data_t *)user_data;
136141
}
137142

143+
/* Safety check: if stream_data is still NULL, reject callback */
144+
if (!stream_data) {
145+
return NGHTTP2_ERR_CALLBACK_FAILURE;
146+
}
147+
138148
/* Expand buffer if needed */
139149
if (stream_data->data_len + len > stream_data->data_capacity) {
140150
/* Calculate sum first to check for overflow */

src/core/network.c

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,24 @@ static void dns_cache_add(const char *hostname, uint16_t port,
221221
return;
222222
}
223223

224+
/* Allocate hostname with error checking */
224225
entry->hostname = strdup(hostname);
225-
entry->port = port;
226+
if (!entry->hostname) {
227+
free(entry);
228+
dns_cache_unlock();
229+
return;
230+
}
231+
232+
/* Deep copy addrinfo with error checking */
226233
entry->result = addrinfo_deep_copy(result);
234+
if (!entry->result) {
235+
free(entry->hostname);
236+
free(entry);
237+
dns_cache_unlock();
238+
return;
239+
}
240+
241+
entry->port = port;
227242
entry->expires = time(NULL) + DNS_CACHE_TTL_SECONDS;
228243
entry->next = dns_cache_head;
229244

src/core/request_builder.c

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,20 @@ static int ensure_capacity(request_builder_t *builder, size_t needed) {
2121
return 0; /* Enough space */
2222
}
2323

24-
/* Calculate new capacity */
24+
/* Calculate new capacity with overflow protection */
2525
size_t new_capacity = builder->capacity;
26-
while (new_capacity < builder->len + needed) {
26+
size_t target = builder->len + needed;
27+
28+
/* Check for overflow in target calculation */
29+
if (target < builder->len) {
30+
return -1; /* Overflow detected */
31+
}
32+
33+
while (new_capacity < target) {
34+
/* Check for overflow before multiplication */
35+
if (new_capacity > SIZE_MAX / GROWTH_FACTOR) {
36+
return -1; /* Would overflow */
37+
}
2738
new_capacity *= GROWTH_FACTOR;
2839
}
2940

src/core/tls.c

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,27 @@ int httpmorph_configure_ssl_ctx(SSL_CTX *ctx, const browser_profile_t *profile)
124124
}
125125

126126
if (name) {
127+
size_t name_len = strlen(name);
127128
if (is_tls13) {
129+
/* Check bounds before adding to TLS 1.3 buffer */
130+
size_t space_needed = name_len + (p13 != tls13_ciphers ? 1 : 0); /* +1 for ':' */
131+
if ((size_t)(p13 - tls13_ciphers) + space_needed >= sizeof(tls13_ciphers)) {
132+
continue; /* Skip this cipher - would overflow */
133+
}
128134
if (p13 != tls13_ciphers) *p13++ = ':';
129-
strcpy(p13, name);
130-
p13 += strlen(name);
135+
memcpy(p13, name, name_len);
136+
p13 += name_len;
137+
*p13 = '\0'; /* Ensure null termination */
131138
} else {
139+
/* Check bounds before adding to TLS 1.2 buffer */
140+
size_t space_needed = name_len + (p12 != tls12_ciphers ? 1 : 0); /* +1 for ':' */
141+
if ((size_t)(p12 - tls12_ciphers) + space_needed >= sizeof(tls12_ciphers)) {
142+
continue; /* Skip this cipher - would overflow */
143+
}
132144
if (p12 != tls12_ciphers) *p12++ = ':';
133-
strcpy(p12, name);
134-
p12 += strlen(name);
145+
memcpy(p12, name, name_len);
146+
p12 += name_len;
147+
*p12 = '\0'; /* Ensure null termination */
135148
}
136149
}
137150
}
@@ -141,9 +154,9 @@ int httpmorph_configure_ssl_ctx(SSL_CTX *ctx, const browser_profile_t *profile)
141154
if (strlen(tls13_ciphers) > 0 && strlen(tls12_ciphers) > 0) {
142155
snprintf(combined_ciphers, sizeof(combined_ciphers), "%s:%s", tls13_ciphers, tls12_ciphers);
143156
} else if (strlen(tls13_ciphers) > 0) {
144-
strcpy(combined_ciphers, tls13_ciphers);
157+
snprintf(combined_ciphers, sizeof(combined_ciphers), "%s", tls13_ciphers);
145158
} else if (strlen(tls12_ciphers) > 0) {
146-
strcpy(combined_ciphers, tls12_ciphers);
159+
snprintf(combined_ciphers, sizeof(combined_ciphers), "%s", tls12_ciphers);
147160
}
148161

149162
/* Use strict cipher list to preserve exact order */

0 commit comments

Comments
 (0)