Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -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
Expand Down
15 changes: 15 additions & 0 deletions src/java/nxt_jni_InputStream.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
10 changes: 9 additions & 1 deletion src/nxt_php_sapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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++;
}

Expand Down
6 changes: 6 additions & 0 deletions src/perl/nxt_perl_psgi.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
33 changes: 20 additions & 13 deletions src/python/nxt_python_asgi_lifespan.c
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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;
Comment on lines 179 to 182
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Initialize lifespan fields before new cleanup paths

When creating the bound send method fails, this new early cleanup path decrefs lifespan before fields such as lifespan->state are initialized below. The deallocator unconditionally runs Py_CLEAR(lifespan->state), so an allocation failure in PyObject_GetAttrString(..., "send") or the analogous _done path can dereference an uninitialized pointer instead of failing cleanly. Initialize the object fields immediately after PyObject_New() or avoid the deallocator until they are initialized.

Useful? React with 👍 / 👎.

}

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;
}
Expand All @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions src/python/nxt_python_wsgi.c
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Loading
Loading