diff --git a/CHANGES b/CHANGES index 8ae835854..a20c043e3 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,19 @@ Changes with FreeUnit 1.35.4 xx xxx 2026 + *) Bugfix: bounds-check app-supplied arguments at the language-binding + trust boundary. PHP: bound the response-header isspace() skip to + the header buffer; free the realpath() temp on script-resolution + failure; document that PATH_INFO is length-only (not NUL-terminated). + Python: surface failed environ-template refresh in the WSGI worker; + fix two ASGI lifespan NULL-checks that referenced the wrong handle. + Perl: scrub ERRSV on PSGI interpreter init failure so a stale + exception cannot propagate to the next interpreter. Java: + validate (off, len) in InputStream.readLine() against the byte + array's length. WASM: bound guest-supplied offsets in + send_headers/send_response and the per-request memcpy chain + against NXT_WASM_MEM_SIZE. + *) Bugfix: fix router process CPU spin and connection hang under port scanning load; CLOSE-WAIT sockets are now cleaned up properly on client FIN, idle connection queue iteration fixed, systemd file diff --git a/src/java/nxt_jni_InputStream.c b/src/java/nxt_jni_InputStream.c index fabff685e..614ddc2c7 100644 --- a/src/java/nxt_jni_InputStream.c +++ b/src/java/nxt_jni_InputStream.c @@ -90,12 +90,27 @@ static jint JNICALL nxt_java_InputStream_readLine(JNIEnv *env, jclass cls, jlong req_info_ptr, jarray out, jint off, jint len) { + jsize array_len; uint8_t *data; ssize_t res; nxt_unit_request_info_t *req; req = nxt_jlong2ptr(req_info_ptr); + /* + * Validate (off, len) against the array bounds before handing + * GetPrimitiveArrayCritical's pointer + an attacker-controlled + * offset to nxt_unit_request_read(). Without this, a malicious + * caller can drive an OOB write of up to len bytes past the + * array's heap allocation. + */ + array_len = (*env)->GetArrayLength(env, out); + if (off < 0 || len < 0 || off > array_len || len > array_len - off) { + nxt_java_throw_IllegalStateException(env, + "InputStream.readLine: off/len out of bounds"); + return -1; + } + data = (*env)->GetPrimitiveArrayCritical(env, out, NULL); res = nxt_unit_request_readline_size(req, len); diff --git a/src/nxt_php_sapi.c b/src/nxt_php_sapi.c index 240541540..a88fa4f50 100644 --- a/src/nxt_php_sapi.c +++ b/src/nxt_php_sapi.c @@ -883,6 +883,7 @@ nxt_php_set_target(nxt_task_t *task, nxt_php_target_t *target, p = nxt_realpath(tmp); if (nxt_slow_path(p == NULL)) { nxt_alert(task, "script realpath(%s) failed %E", tmp, nxt_errno); + nxt_free(tmp); return NXT_ERROR; } @@ -1471,6 +1472,11 @@ nxt_php_dynamic_request(nxt_php_run_ctx_t *ctx, nxt_unit_request_t *r) path.length = ctx->path_info.start - path.start; ctx->path_info.length = r->path_length - path.length; + /* + * ctx->path_info points into the shmem-mapped request buffer + * and is not NUL-terminated. All consumers below use the + * length field; do not pass path_info.start to C-string APIs. + */ } else if (path.start[path.length - 1] == '/') { script_name = *ctx->index; @@ -1792,7 +1798,9 @@ nxt_php_send_headers(sapi_headers_struct *sapi_headers TSRMLS_DC) } value = colon + 1; - while(isspace(*value)) { + while (value < h->header + h->header_len + && isspace((unsigned char) *value)) + { value++; } diff --git a/src/perl/nxt_perl_psgi.c b/src/perl/nxt_perl_psgi.c index c16b21737..05eee2372 100644 --- a/src/perl/nxt_perl_psgi.c +++ b/src/perl/nxt_perl_psgi.c @@ -572,6 +572,12 @@ nxt_perl_psgi_ctx_init(const char *script, nxt_perl_psgi_ctx_t *pctx) fail: + /* + * Scrub ERRSV so a stale exception from eval_pv() / io_init() does + * not propagate to the next interpreter created on this pctx slot. + */ + sv_setsv(ERRSV, &PL_sv_undef); + nxt_perl_psgi_io_release(my_perl, &pctx->arg_input); nxt_perl_psgi_io_release(my_perl, &pctx->arg_error); diff --git a/src/python/nxt_python_asgi_lifespan.c b/src/python/nxt_python_asgi_lifespan.c index 8ef783779..47a06317c 100644 --- a/src/python/nxt_python_asgi_lifespan.c +++ b/src/python/nxt_python_asgi_lifespan.c @@ -150,6 +150,24 @@ nxt_py_asgi_lifespan_target_startup(nxt_py_asgi_ctx_data_t *ctx_data, return NULL; } + /* + * Initialize every pointer field the deallocator may touch BEFORE + * any path can goto release_lifespan / release_receive / release_send, + * since PyObject_New returns uninitialized memory and the dealloc + * runs Py_CLEAR on these unconditionally. + */ + lifespan->ctx_data = ctx_data; + lifespan->disabled = 0; + lifespan->startup_received = 0; + lifespan->startup_sent = 0; + lifespan->shutdown_received = 0; + lifespan->shutdown_sent = 0; + lifespan->shutdown_called = 0; + lifespan->startup_future = NULL; + lifespan->shutdown_future = NULL; + lifespan->receive_future = NULL; + lifespan->state = NULL; + ret = NULL; receive = PyObject_GetAttrString((PyObject *) lifespan, "receive"); @@ -159,13 +177,13 @@ nxt_py_asgi_lifespan_target_startup(nxt_py_asgi_ctx_data_t *ctx_data, } send = PyObject_GetAttrString((PyObject *) lifespan, "send"); - if (nxt_slow_path(receive == NULL)) { + if (nxt_slow_path(send == NULL)) { nxt_unit_alert(NULL, "Python failed to get 'send' method"); goto release_receive; } done = PyObject_GetAttrString((PyObject *) lifespan, "_done"); - if (nxt_slow_path(receive == NULL)) { + if (nxt_slow_path(done == NULL)) { nxt_unit_alert(NULL, "Python failed to get '_done' method"); goto release_send; } @@ -179,17 +197,6 @@ nxt_py_asgi_lifespan_target_startup(nxt_py_asgi_ctx_data_t *ctx_data, goto release_done; } - lifespan->ctx_data = ctx_data; - lifespan->disabled = 0; - lifespan->startup_received = 0; - lifespan->startup_sent = 0; - lifespan->shutdown_received = 0; - lifespan->shutdown_sent = 0; - lifespan->shutdown_called = 0; - lifespan->shutdown_future = NULL; - lifespan->receive_future = NULL; - lifespan->state = NULL; - scope = nxt_py_asgi_new_scope(NULL, nxt_py_lifespan_str, nxt_py_2_0_str); if (nxt_slow_path(scope == NULL)) { goto release_future; diff --git a/src/python/nxt_python_wsgi.c b/src/python/nxt_python_wsgi.c index 6bbf9e397..ecbc30fb0 100644 --- a/src/python/nxt_python_wsgi.c +++ b/src/python/nxt_python_wsgi.c @@ -453,6 +453,17 @@ nxt_python_request_handler(nxt_unit_request_info_t *req) PyEval_RestoreThread(pctx->thread_state); pctx->environ = nxt_python_copy_environ(NULL); + if (nxt_slow_path(pctx->environ == NULL)) { + /* + * Refresh failed; surface the error. The next request's + * NULL-check (above) will retry via copy_environ(req) and + * fail it cleanly with NXT_UNIT_ERROR if the retry also + * fails — no NULL dereference downstream. + */ + nxt_unit_alert(NULL, + "Python failed to refresh the \"environ\" " + "template"); + } pctx->thread_state = PyEval_SaveThread(); } diff --git a/src/wasm/nxt_wasm.c b/src/wasm/nxt_wasm.c index db79d6aee..7a7689efa 100644 --- a/src/wasm/nxt_wasm.c +++ b/src/wasm/nxt_wasm.c @@ -42,15 +42,95 @@ nxt_wasm_do_response_end(nxt_wasm_ctx_t *ctx) void nxt_wasm_do_send_headers(nxt_wasm_ctx_t *ctx, uint32_t offset) { - size_t fields_len; + size_t fields_len, fields_table_end; unsigned int i; nxt_wasm_response_fields_t *rh; + /* + * `offset`, `nfields`, and every (name_off, name_len, value_off, + * value_len) tuple arrive from the guest WASM module. Bound them + * against the linear-memory size before any dereference. + */ + if (offset > NXT_WASM_MEM_SIZE - sizeof(nxt_wasm_response_fields_t)) { + nxt_unit_req_alert(ctx->req, + "WASM send_headers offset %u out of range", offset); + return; + } + rh = (nxt_wasm_response_fields_t *)(ctx->baddr + offset); + /* + * Bound the fields[] table. Each entry is nxt_wasm_http_field_t; + * compute end-offset with overflow-safe arithmetic. + */ + if (rh->nfields > (NXT_WASM_MEM_SIZE - offset + - sizeof(nxt_wasm_response_fields_t)) + / sizeof(nxt_wasm_http_field_t)) + { + nxt_unit_req_alert(ctx->req, + "WASM send_headers nfields=%u out of range", + rh->nfields); + return; + } + fields_table_end = offset + sizeof(nxt_wasm_response_fields_t) + + (size_t) rh->nfields * sizeof(nxt_wasm_http_field_t); + + /* + * Bound each (name, value) range in the guest's memory. Field + * offsets are guest-relative to `rh`, so the absolute offset in + * linear memory is `offset + field_off`. + * + * Two additional caps: + * - `name_len` is passed to nxt_unit_response_add_field() whose + * `name_length` parameter is uint8_t — any name_len > 255 would + * silently truncate, splicing the next bytes of guest memory + * into the emitted header. Reject up front. + * - `fields_len` is the aggregate sum of every name+value length; + * nothing stops a guest from pointing many fields at the same + * in-bounds region and inflating the sum (or overflowing it). + * Accumulate with overflow checks and cap to the linear-memory + * size: no legitimate response header table can exceed that. + */ fields_len = 0; for (i = 0; i < rh->nfields; i++) { - fields_len += rh->fields[i].name_len + rh->fields[i].value_len; + uint32_t name_off = rh->fields[i].name_off; + uint32_t name_len = rh->fields[i].name_len; + uint32_t val_off = rh->fields[i].value_off; + uint32_t val_len = rh->fields[i].value_len; + size_t abs_name = (size_t) offset + name_off; + size_t abs_val = (size_t) offset + val_off; + + if (abs_name < fields_table_end + || abs_name > NXT_WASM_MEM_SIZE + || name_len > NXT_WASM_MEM_SIZE - abs_name + || abs_val < fields_table_end + || abs_val > NXT_WASM_MEM_SIZE + || val_len > NXT_WASM_MEM_SIZE - abs_val) + { + nxt_unit_req_alert(ctx->req, + "WASM send_headers field[%u] out of range " + "(name_off=%u name_len=%u val_off=%u val_len=%u)", + i, name_off, name_len, val_off, val_len); + return; + } + + if (name_len > UINT8_MAX) { + nxt_unit_req_alert(ctx->req, + "WASM send_headers field[%u] name_len=%u exceeds 255", + i, name_len); + return; + } + + if (val_len > NXT_WASM_MEM_SIZE - fields_len + || name_len > NXT_WASM_MEM_SIZE - fields_len - val_len) + { + nxt_unit_req_alert(ctx->req, + "WASM send_headers aggregate header size exceeds %zu " + "at field[%u]", (size_t) NXT_WASM_MEM_SIZE, i); + return; + } + + fields_len += name_len + val_len; } nxt_unit_response_init(ctx->req, ctx->status, rh->nfields, fields_len); @@ -80,8 +160,26 @@ nxt_wasm_do_send_response(nxt_wasm_ctx_t *ctx, uint32_t offset) nxt_unit_response_init(req, ctx->status, 0, 0); } + /* + * `offset` arrives from the guest WASM module; bound it against + * the linear-memory size before dereferencing. + */ + if (offset > NXT_WASM_MEM_SIZE - sizeof(nxt_wasm_response_t)) { + nxt_unit_req_alert(req, + "WASM send_response offset %u out of range", offset); + return; + } + resp = (nxt_wasm_response_t *)(nxt_wasm_ctx.baddr + offset); + if (resp->size > NXT_WASM_MEM_SIZE + - offset - offsetof(nxt_wasm_response_t, data)) + { + nxt_unit_req_alert(req, "WASM send_response size %u out of range", + resp->size); + return; + } + nxt_unit_response_write(req, (const char *)resp->data, resp->size); } @@ -97,23 +195,65 @@ nxt_wasm_request_handler(nxt_unit_request_info_t *req) nxt_wasm_request_t *wr; nxt_wasm_http_field_t *df; + /* + * Publish the in-flight request on the shared ctx *before* any + * hook or guarded jump so REQUEST_INIT / REQUEST_END always see + * the right request — including the bounds-failure paths that + * goto request_done before reaching the body-read block below. + * Without this, REQUEST_END would fire with a stale (or NULL) + * ctx->req on the first guarded exit of any worker. + */ + nxt_wasm_ctx.req = req; + NXT_WASM_DO_HOOK(NXT_WASM_FH_REQUEST_INIT); wr = (nxt_wasm_request_t *)nxt_wasm_ctx.baddr; + /* + * Each request field is copied into the WASM linear memory at the + * running `offset`. Without bounds checks, an unusually large + * request (many headers, oversized header values) can drive the + * memcpy past the end of the 32 MB sandbox region. + */ + /* + * Route bounds failures through the request_done label so the + * REQUEST_END hook still fires — modules tracking per-request + * state in request_init_handler need their matching cleanup. + */ +#define WASM_OFFSET_GUARD(need) \ + do { \ + if (offset > NXT_WASM_MEM_SIZE - 1 \ + || (size_t)(need) > NXT_WASM_MEM_SIZE - 1 - offset) \ + { \ + nxt_unit_req_alert(req, \ + "WASM request buffer overflow at offset %zu", offset); \ + nxt_unit_request_done(req, NXT_UNIT_ERROR); \ + goto request_done; \ + } \ + } while (0) + #define SET_REQ_MEMBER(dmember, smember) \ do { \ const char *str = nxt_unit_sptr_get(&r->smember); \ + size_t slen = strlen(str); \ + WASM_OFFSET_GUARD(slen); \ wr->dmember##_off = offset; \ - wr->dmember##_len = strlen(str); \ - memcpy((uint8_t *)wr + offset, str, wr->dmember##_len + 1); \ - offset += wr->dmember##_len + 1; \ + wr->dmember##_len = slen; \ + memcpy((uint8_t *)wr + offset, str, slen + 1); \ + offset += slen + 1; \ } while (0) r = req->request; offset = sizeof(nxt_wasm_request_t) + (r->fields_count * sizeof(nxt_wasm_http_field_t)); + if (offset > NXT_WASM_MEM_SIZE) { + nxt_unit_req_alert(req, "WASM request header table exceeds linear " + "memory: fields_count=%u", r->fields_count); + nxt_unit_request_done(req, NXT_UNIT_ERROR); + goto request_done; + } + SET_REQ_MEMBER(path, path); SET_REQ_MEMBER(method, method); SET_REQ_MEMBER(version, version); @@ -129,19 +269,24 @@ nxt_wasm_request_handler(nxt_unit_request_info_t *req) for (sf = r->fields; sf < sf_end; sf++) { const char *name = nxt_unit_sptr_get(&sf->name); const char *value = nxt_unit_sptr_get(&sf->value); + size_t nlen = strlen(name); + size_t vlen = strlen(value); + WASM_OFFSET_GUARD(nlen); df->name_off = offset; - df->name_len = strlen(name); - memcpy((uint8_t *)wr + offset, name, df->name_len + 1); - offset += df->name_len + 1; + df->name_len = nlen; + memcpy((uint8_t *)wr + offset, name, nlen + 1); + offset += nlen + 1; + WASM_OFFSET_GUARD(vlen); df->value_off = offset; - df->value_len = strlen(value); - memcpy((uint8_t *)wr + offset, value, df->value_len + 1); - offset += df->value_len + 1; + df->value_len = vlen; + memcpy((uint8_t *)wr + offset, value, vlen + 1); + offset += vlen + 1; df++; } +#undef WASM_OFFSET_GUARD wr->tls = r->tls; wr->nfields = r->fields_count; @@ -156,7 +301,6 @@ nxt_wasm_request_handler(nxt_unit_request_info_t *req) wr->request_size = offset + bytes_read; nxt_wasm_ctx.status = NXT_WASM_HTTP_OK; - nxt_wasm_ctx.req = req; err = nxt_wops->exec_request(&nxt_wasm_ctx); if (err) { goto out_err_500;