Merge #79: durable HTTP response-truncation fix, both runtimes (via stage)

This commit is contained in:
2026-07-22 15:46:02 -05:00
2 changed files with 92 additions and 20 deletions
+45 -9
View File
@@ -82,8 +82,14 @@ static _Thread_local ElArena _tl_arena = {NULL, 0, 0};
static _Thread_local int _tl_arena_active = 0; static _Thread_local int _tl_arena_active = 0;
/* Binary-safe fs_read length — set by fs_read, consumed by http_send_response. /* Binary-safe fs_read length — set by fs_read, consumed by http_send_response.
* Allows serving PNGs and other binary files without strlen truncation. */ * Allows serving PNGs and other binary files without strlen truncation.
* PAIRED with the buffer pointer it describes: the length may only be applied
* to the exact buffer fs_read returned. Without the pairing, any handler that
* fs_read a file and then WRAPPED it into a larger response had that response
* truncated to the file's length (Content-Length lied AND the send stopped
* short) the safety-contact onboarding trap, 2026-07-17. */
static _Thread_local size_t _tl_fs_read_len = 0; static _Thread_local size_t _tl_fs_read_len = 0;
static _Thread_local const char* _tl_fs_read_buf = NULL;
static void el_arena_track(char* p) { static void el_arena_track(char* p) {
if (!_tl_arena_active || !p) return; if (!_tl_arena_active || !p) return;
@@ -101,6 +107,8 @@ static void el_arena_track(char* p) {
void el_request_start(void) { void el_request_start(void) {
_tl_arena.count = 0; _tl_arena.count = 0;
_tl_arena_active = 1; _tl_arena_active = 1;
_tl_fs_read_len = 0; /* never let a previous request's file length */
_tl_fs_read_buf = NULL; /* leak into this response's byte accounting */
} }
/* Called by http_worker after the El handler returns and the response is sent. /* Called by http_worker after the El handler returns and the response is sent.
@@ -1484,11 +1492,14 @@ static void http_send_response(int fd, const char* body) {
} }
const char* eff_body = is_envelope ? env_body : body; const char* eff_body = is_envelope ? env_body : body;
/* Use the real byte count from fs_read if available (handles binary files /* Use the real byte count from fs_read ONLY when this body IS the exact
* with embedded null bytes PNG, WOFF2, etc.). Fall back to strlen for * buffer fs_read returned (binary files with embedded null bytes PNG,
* normal text/JSON responses where _tl_fs_read_len is 0. */ * WOFF2, etc.). Any other body wrapped, enveloped, or derived must be
size_t blen = (_tl_fs_read_len > 0) ? _tl_fs_read_len : strlen(eff_body); * measured with strlen, or it is truncated/over-read to the file's size. */
size_t blen = (_tl_fs_read_len > 0 && eff_body == _tl_fs_read_buf)
? _tl_fs_read_len : strlen(eff_body);
_tl_fs_read_len = 0; /* consume — one-shot per response */ _tl_fs_read_len = 0; /* consume — one-shot per response */
_tl_fs_read_buf = NULL;
int head_only = _tl_http_head_only; int head_only = _tl_http_head_only;
JsonBuf hdrs; jb_init(&hdrs); JsonBuf hdrs; jb_init(&hdrs);
@@ -1568,11 +1579,22 @@ static void* http_worker(void* arg) {
const char* rs = EL_CSTR(r); const char* rs = EL_CSTR(r);
/* Copy response out BEFORE arena teardown. /* Copy response out BEFORE arena teardown.
* For binary files, _tl_fs_read_len holds the real byte count * For binary files, _tl_fs_read_len holds the real byte count
* use memcpy instead of strdup so null bytes are preserved. */ * use memcpy instead of strdup so null bytes are preserved.
size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0); * The stored length applies ONLY when the response IS the exact
* fs_read buffer; a wrapped/derived response must use strlen or
* it gets truncated (or over-read) to the file's length. */
size_t rlen;
if (_tl_fs_read_len > 0 && rs && rs == _tl_fs_read_buf) {
rlen = _tl_fs_read_len; /* raw file bytes — binary-safe */
} else {
rlen = rs ? strlen(rs) : 0;
_tl_fs_read_len = 0; /* hint doesn't describe this body */
_tl_fs_read_buf = NULL;
}
response = malloc(rlen + 1); response = malloc(rlen + 1);
if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; } if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; }
else if (response) { response[0] = '\0'; } else if (response) { response[0] = '\0'; }
if (_tl_fs_read_len > 0) _tl_fs_read_buf = response; /* hint follows the copy */
} else { } else {
response = el_strdup_persist("el-runtime: no http handler registered"); response = el_strdup_persist("el-runtime: no http handler registered");
} }
@@ -1822,10 +1844,20 @@ static void* http_worker_v2(void* arg) {
el_val_t hmap = http_build_headers_map(hdr_block ? hdr_block : ""); el_val_t hmap = http_build_headers_map(hdr_block ? hdr_block : "");
el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), hmap, EL_STR(body)); el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), hmap, EL_STR(body));
const char* rs = EL_CSTR(r); const char* rs = EL_CSTR(r);
size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0); /* Same pairing rule as the v1 worker: the fs_read length is only
* trustworthy for the exact buffer fs_read returned. */
size_t rlen;
if (_tl_fs_read_len > 0 && rs && rs == _tl_fs_read_buf) {
rlen = _tl_fs_read_len; /* raw file bytes — binary-safe */
} else {
rlen = rs ? strlen(rs) : 0;
_tl_fs_read_len = 0; /* hint doesn't describe this body */
_tl_fs_read_buf = NULL;
}
response = malloc(rlen + 1); response = malloc(rlen + 1);
if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; } if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; }
else if (response) { response[0] = '\0'; } else if (response) { response[0] = '\0'; }
if (_tl_fs_read_len > 0) _tl_fs_read_buf = response; /* hint follows the copy */
el_release(hmap); el_release(hmap);
} else { } else {
response = el_strdup_persist( response = el_strdup_persist(
@@ -2024,6 +2056,7 @@ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) {
el_val_t fs_read(el_val_t pathv) { el_val_t fs_read(el_val_t pathv) {
const char* path = EL_CSTR(pathv); const char* path = EL_CSTR(pathv);
_tl_fs_read_len = 0; _tl_fs_read_len = 0;
_tl_fs_read_buf = NULL;
if (!path) return el_wrap_str(el_strdup("")); if (!path) return el_wrap_str(el_strdup(""));
FILE* f = fopen(path, "rb"); FILE* f = fopen(path, "rb");
if (!f) return el_wrap_str(el_strdup("")); if (!f) return el_wrap_str(el_strdup(""));
@@ -2035,6 +2068,7 @@ el_val_t fs_read(el_val_t pathv) {
size_t got = fread(buf, 1, (size_t)sz, f); size_t got = fread(buf, 1, (size_t)sz, f);
buf[got] = '\0'; buf[got] = '\0';
_tl_fs_read_len = got; /* store real byte count for binary-safe send */ _tl_fs_read_len = got; /* store real byte count for binary-safe send */
_tl_fs_read_buf = buf; /* ...valid ONLY for this exact buffer */
fclose(f); fclose(f);
return el_wrap_str(buf); return el_wrap_str(buf);
} }
@@ -3577,8 +3611,10 @@ el_val_t json_get_raw(el_val_t json_str, el_val_t key) {
const char* k = EL_CSTR(key); const char* k = EL_CSTR(key);
const char* p = json_find_key(json, k); const char* p = json_find_key(json, k);
/* Clear fs_read binary-length hint — result is a fresh null-terminated /* Clear fs_read binary-length hint — result is a fresh null-terminated
* string, not the raw file bytes, so Content-Length must use strlen. */ * string, not the raw file bytes, so Content-Length must use strlen.
* (Kept although the pointer pairing now makes this redundant.) */
_tl_fs_read_len = 0; _tl_fs_read_len = 0;
_tl_fs_read_buf = NULL;
if (!p) return el_wrap_str(el_strdup("")); if (!p) return el_wrap_str(el_strdup(""));
const char* end = json_skip_value(p); const char* end = json_skip_value(p);
size_t n = (size_t)(end - p); size_t n = (size_t)(end - p);
+45 -9
View File
@@ -71,8 +71,14 @@ static _Thread_local ElArena _tl_arena = {NULL, 0, 0};
static _Thread_local int _tl_arena_active = 0; static _Thread_local int _tl_arena_active = 0;
/* Binary-safe fs_read length — set by fs_read, consumed by http_send_response. /* Binary-safe fs_read length — set by fs_read, consumed by http_send_response.
* Allows serving PNGs and other binary files without strlen truncation. */ * Allows serving PNGs and other binary files without strlen truncation.
* PAIRED with the buffer pointer it describes: the length may only be applied
* to the exact buffer fs_read returned. Without the pairing, any handler that
* fs_read a file and then WRAPPED it into a larger response had that response
* truncated to the file's length (Content-Length lied AND the send stopped
* short) the safety-contact onboarding trap, 2026-07-17. */
static _Thread_local size_t _tl_fs_read_len = 0; static _Thread_local size_t _tl_fs_read_len = 0;
static _Thread_local const char* _tl_fs_read_buf = NULL;
static void el_arena_track(char* p) { static void el_arena_track(char* p) {
if (!_tl_arena_active || !p) return; if (!_tl_arena_active || !p) return;
@@ -90,6 +96,8 @@ static void el_arena_track(char* p) {
void el_request_start(void) { void el_request_start(void) {
_tl_arena.count = 0; _tl_arena.count = 0;
_tl_arena_active = 1; _tl_arena_active = 1;
_tl_fs_read_len = 0; /* never let a previous request's file length */
_tl_fs_read_buf = NULL; /* leak into this response's byte accounting */
} }
/* Called by http_worker after the El handler returns and the response is sent. /* Called by http_worker after the El handler returns and the response is sent.
@@ -1403,11 +1411,14 @@ static void http_send_response(int fd, const char* body) {
} }
const char* eff_body = is_envelope ? env_body : body; const char* eff_body = is_envelope ? env_body : body;
/* Use the real byte count from fs_read if available (handles binary files /* Use the real byte count from fs_read ONLY when this body IS the exact
* with embedded null bytes PNG, WOFF2, etc.). Fall back to strlen for * buffer fs_read returned (binary files with embedded null bytes PNG,
* normal text/JSON responses where _tl_fs_read_len is 0. */ * WOFF2, etc.). Any other body wrapped, enveloped, or derived must be
size_t blen = (_tl_fs_read_len > 0) ? _tl_fs_read_len : strlen(eff_body); * measured with strlen, or it is truncated/over-read to the file's size. */
size_t blen = (_tl_fs_read_len > 0 && eff_body == _tl_fs_read_buf)
? _tl_fs_read_len : strlen(eff_body);
_tl_fs_read_len = 0; /* consume — one-shot per response */ _tl_fs_read_len = 0; /* consume — one-shot per response */
_tl_fs_read_buf = NULL;
int head_only = _tl_http_head_only; int head_only = _tl_http_head_only;
JsonBuf hdrs; jb_init(&hdrs); JsonBuf hdrs; jb_init(&hdrs);
@@ -1479,11 +1490,22 @@ static void* http_worker(void* arg) {
const char* rs = EL_CSTR(r); const char* rs = EL_CSTR(r);
/* Copy response out BEFORE arena teardown. /* Copy response out BEFORE arena teardown.
* For binary files, _tl_fs_read_len holds the real byte count * For binary files, _tl_fs_read_len holds the real byte count
* use memcpy instead of strdup so null bytes are preserved. */ * use memcpy instead of strdup so null bytes are preserved.
size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0); * The stored length applies ONLY when the response IS the exact
* fs_read buffer; a wrapped/derived response must use strlen or
* it gets truncated (or over-read) to the file's length. */
size_t rlen;
if (_tl_fs_read_len > 0 && rs && rs == _tl_fs_read_buf) {
rlen = _tl_fs_read_len; /* raw file bytes — binary-safe */
} else {
rlen = rs ? strlen(rs) : 0;
_tl_fs_read_len = 0; /* hint doesn't describe this body */
_tl_fs_read_buf = NULL;
}
response = malloc(rlen + 1); response = malloc(rlen + 1);
if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; } if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; }
else if (response) { response[0] = '\0'; } else if (response) { response[0] = '\0'; }
if (_tl_fs_read_len > 0) _tl_fs_read_buf = response; /* hint follows the copy */
} else { } else {
response = el_strdup_persist("el-runtime: no http handler registered"); response = el_strdup_persist("el-runtime: no http handler registered");
} }
@@ -1805,10 +1827,20 @@ static void* http_worker_v2(void* arg) {
el_val_t hmap = http_build_headers_map(hdr_block ? hdr_block : ""); el_val_t hmap = http_build_headers_map(hdr_block ? hdr_block : "");
el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), hmap, EL_STR(body)); el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), hmap, EL_STR(body));
const char* rs = EL_CSTR(r); const char* rs = EL_CSTR(r);
size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0); /* Same pairing rule as the v1 worker: the fs_read length is only
* trustworthy for the exact buffer fs_read returned. */
size_t rlen;
if (_tl_fs_read_len > 0 && rs && rs == _tl_fs_read_buf) {
rlen = _tl_fs_read_len; /* raw file bytes — binary-safe */
} else {
rlen = rs ? strlen(rs) : 0;
_tl_fs_read_len = 0; /* hint doesn't describe this body */
_tl_fs_read_buf = NULL;
}
response = malloc(rlen + 1); response = malloc(rlen + 1);
if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; } if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; }
else if (response) { response[0] = '\0'; } else if (response) { response[0] = '\0'; }
if (_tl_fs_read_len > 0) _tl_fs_read_buf = response; /* hint follows the copy */
el_release(hmap); el_release(hmap);
} else { } else {
response = el_strdup_persist( response = el_strdup_persist(
@@ -1924,6 +1956,7 @@ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) {
el_val_t fs_read(el_val_t pathv) { el_val_t fs_read(el_val_t pathv) {
const char* path = EL_CSTR(pathv); const char* path = EL_CSTR(pathv);
_tl_fs_read_len = 0; _tl_fs_read_len = 0;
_tl_fs_read_buf = NULL;
if (!path) return el_wrap_str(el_strdup("")); if (!path) return el_wrap_str(el_strdup(""));
FILE* f = fopen(path, "rb"); FILE* f = fopen(path, "rb");
if (!f) return el_wrap_str(el_strdup("")); if (!f) return el_wrap_str(el_strdup(""));
@@ -1935,6 +1968,7 @@ el_val_t fs_read(el_val_t pathv) {
size_t got = fread(buf, 1, (size_t)sz, f); size_t got = fread(buf, 1, (size_t)sz, f);
buf[got] = '\0'; buf[got] = '\0';
_tl_fs_read_len = got; /* store real byte count for binary-safe send */ _tl_fs_read_len = got; /* store real byte count for binary-safe send */
_tl_fs_read_buf = buf; /* ...valid ONLY for this exact buffer */
fclose(f); fclose(f);
return el_wrap_str(buf); return el_wrap_str(buf);
} }
@@ -3305,8 +3339,10 @@ el_val_t json_get_raw(el_val_t json_str, el_val_t key) {
const char* k = EL_CSTR(key); const char* k = EL_CSTR(key);
const char* p = json_find_key(json, k); const char* p = json_find_key(json, k);
/* Clear fs_read binary-length hint — result is a fresh null-terminated /* Clear fs_read binary-length hint — result is a fresh null-terminated
* string, not the raw file bytes, so Content-Length must use strlen. */ * string, not the raw file bytes, so Content-Length must use strlen.
* (Kept although the pointer pairing now makes this redundant.) */
_tl_fs_read_len = 0; _tl_fs_read_len = 0;
_tl_fs_read_buf = NULL;
if (!p) return el_wrap_str(el_strdup("")); if (!p) return el_wrap_str(el_strdup(""));
const char* end = json_skip_value(p); const char* end = json_skip_value(p);
size_t n = (size_t)(end - p); size_t n = (size_t)(end - p);