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
239 changes: 239 additions & 0 deletions .patches/httpd-2.4.66-pr699.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
diff --git a/include/mpm_common.h b/include/mpm_common.h
index 6b3d1f5..154df9a 100644
--- a/include/mpm_common.h
+++ b/include/mpm_common.h
@@ -40,6 +40,7 @@
#include "ap_config.h"
#include "ap_mpm.h"
#include "scoreboard.h"
+#include "apr_optional.h"

#if APR_HAVE_NETINET_TCP_H
#include <netinet/tcp.h> /* for TCP_NODELAY */
@@ -479,6 +480,20 @@ AP_DECLARE_HOOK(void, child_stopping,
*/
void mpm_common_pre_config(apr_pool_t *pconf);

+/**
+ * Hooks for modules to report connections the MPM did not accept itself.
+ *
+ * MPMs that wait for their connection count to drain before stopping a child
+ * need this so externally accepted connections keep the child alive until
+ * they finish.
+ *
+ * Call ap_mpm_note_extra_connection_added() when such a connection starts,
+ * and ap_mpm_note_extra_connection_removed() when it ends. These functions
+ * may be NULL if the active MPM does not implement them.
+ */
+APR_DECLARE_OPTIONAL_FN(void, ap_mpm_note_extra_connection_added, (void));
+APR_DECLARE_OPTIONAL_FN(void, ap_mpm_note_extra_connection_removed, (void));
+
#ifdef __cplusplus
}
#endif
diff --git a/server/mpm/event/event.c b/server/mpm/event/event.c
index 050d823..48119ca 100644
--- a/server/mpm/event/event.c
+++ b/server/mpm/event/event.c
@@ -828,6 +828,21 @@ static apr_status_t decrement_connection_count(void *cs_)
return APR_SUCCESS;
}

+static void ap_mpm_note_extra_connection_added(void)
+{
+ apr_atomic_inc32(&connection_count);
+}
+
+static void ap_mpm_note_extra_connection_removed(void)
+{
+ int is_last_connection = !apr_atomic_dec32(&connection_count);
+
+ /* Wake a listener blocked waiting for connection_count to drain. */
+ if (listener_is_wakeable && is_last_connection && listener_may_exit) {
+ apr_pollset_wakeup(event_pollset);
+ }
+}
+
static void notify_suspend(event_conn_state_t *cs)
{
ap_run_suspend_connection(cs->c, cs->r);
@@ -3466,6 +3481,10 @@ static void setup_slave_conn(conn_rec *c, void *csd)
event_conn_state_t *cs;

mcs = ap_get_module_config(c->master->conn_config, &mpm_event_module);
+ if (!mcs) {
+ /* Master connection is not managed by this MPM; nothing to inherit. */
+ return;
+ }

cs = apr_pcalloc(c->pool, sizeof(*cs));
cs->c = c;
@@ -3607,6 +3626,9 @@ static int event_pre_config(apr_pool_t * pconf, apr_pool_t * plog,
const char *userdata_key = "mpm_event_module";
int test_atomics = 0;

+ APR_REGISTER_OPTIONAL_FN(ap_mpm_note_extra_connection_added);
+ APR_REGISTER_OPTIONAL_FN(ap_mpm_note_extra_connection_removed);
+
debug = ap_exists_config_define("DEBUG");

if (debug) {
diff --git a/server/mpm/prefork/prefork.c b/server/mpm/prefork/prefork.c
index b5adb57..2640972 100644
--- a/server/mpm/prefork/prefork.c
+++ b/server/mpm/prefork/prefork.c
@@ -18,6 +18,7 @@
#include "apr_portable.h"
#include "apr_strings.h"
#include "apr_thread_proc.h"
+#include "apr_atomic.h"
#include "apr_signal.h"

#define APR_WANT_STDIO
@@ -88,6 +89,7 @@

/* config globals */

+static apr_uint32_t connection_count = 0; /* Number of open connections */
static int ap_daemons_to_start=0;
static int ap_daemons_min_free=0;
static int ap_daemons_max_free=0;
@@ -215,19 +217,24 @@ static void prefork_note_child_started(int slot, pid_t pid)
}

/* a clean exit from a child with proper cleanup */
-static void clean_child_exit(int code) __attribute__ ((noreturn));
-static void clean_child_exit(int code)
+static void clean_child_exit_ex(int code, int from_signal)
+ __attribute__ ((noreturn));
+static void clean_child_exit_ex(int code, int from_signal)
{
retained->mpm->mpm_state = AP_MPMQ_STOPPING;

apr_signal(SIGHUP, SIG_IGN);
apr_signal(SIGTERM, SIG_IGN);

- if (code == 0) {
- ap_run_child_stopping(pchild, 0);
- }
-
if (pchild) {
+ if (!code && !from_signal) {
+ ap_run_child_stopping(pchild, !retained->mpm->is_ungraceful);
+ if (!retained->mpm->is_ungraceful) {
+ while (apr_atomic_read32(&connection_count) > 0) {
+ apr_sleep(apr_time_from_msec(100));
+ }
+ }
+ }
apr_pool_destroy(pchild);
}

@@ -240,6 +247,13 @@ static void clean_child_exit(int code)
exit(code);
}

+/* a clean exit from a child with proper cleanup */
+static void clean_child_exit(int code) __attribute__ ((noreturn));
+static void clean_child_exit(int code)
+{
+ clean_child_exit_ex(code, 0);
+}
+
static apr_status_t accept_mutex_on(void)
{
apr_status_t rv = apr_proc_mutex_lock(my_bucket->mutex);
@@ -356,12 +370,22 @@ static const char *prefork_get_name(void)

static void just_die(int sig)
{
- clean_child_exit(0);
+ clean_child_exit_ex(0, 1);
}

/* volatile because it's updated from a signal handler */
static int volatile die_now = 0;

+static void ap_mpm_note_extra_connection_added(void)
+{
+ apr_atomic_inc32(&connection_count);
+}
+
+static void ap_mpm_note_extra_connection_removed(void)
+{
+ apr_atomic_dec32(&connection_count);
+}
+
static void stop_listening(int sig)
{
retained->mpm->mpm_state = AP_MPMQ_STOPPING;
@@ -1286,6 +1310,9 @@ static int prefork_pre_config(apr_pool_t *p, apr_pool_t *plog, apr_pool_t *ptemp
apr_status_t rv;
const char *userdata_key = "mpm_prefork_module";

+ APR_REGISTER_OPTIONAL_FN(ap_mpm_note_extra_connection_added);
+ APR_REGISTER_OPTIONAL_FN(ap_mpm_note_extra_connection_removed);
+
debug = ap_exists_config_define("DEBUG");

if (debug) {
diff --git a/server/mpm/worker/worker.c b/server/mpm/worker/worker.c
index 315371d..0d0a3d0 100644
--- a/server/mpm/worker/worker.c
+++ b/server/mpm/worker/worker.c
@@ -30,6 +30,7 @@
#include "apr_thread_mutex.h"
#include "apr_proc_mutex.h"
#include "apr_poll.h"
+#include "apr_atomic.h"

#include <stdlib.h>

@@ -116,6 +117,7 @@
* Actual definitions of config globals
*/

+static apr_uint32_t connection_count = 0; /* Number of open connections */
static int threads_per_child = 0; /* Worker threads per child */
static int ap_daemons_to_start = 0;
static int min_spare_threads = 0;
@@ -506,6 +508,16 @@ static void check_infinite_requests(void)
}
}

+static void ap_mpm_note_extra_connection_added(void)
+{
+ apr_atomic_inc32(&connection_count);
+}
+
+static void ap_mpm_note_extra_connection_removed(void)
+{
+ apr_atomic_dec32(&connection_count);
+}
+
static void unblock_signal(int sig)
{
sigset_t sig_mask;
@@ -1301,6 +1313,12 @@ static void child_main(int child_num_arg, int child_bucket)
rv == AP_MPM_PODX_GRACEFUL ? ST_GRACEFUL : ST_UNGRACEFUL);
}

+ if (terminate_mode == ST_GRACEFUL) {
+ while (apr_atomic_read32(&connection_count) > 0) {
+ apr_sleep(apr_time_from_msec(100));
+ }
+ }
+
free(threads);

clean_child_exit(resource_shortage ? APEXIT_CHILDSICK : 0);
@@ -2059,6 +2077,9 @@ static int worker_pre_config(apr_pool_t *pconf, apr_pool_t *plog,
apr_status_t rv;
const char *userdata_key = "mpm_worker_module";

+ APR_REGISTER_OPTIONAL_FN(ap_mpm_note_extra_connection_added);
+ APR_REGISTER_OPTIONAL_FN(ap_mpm_note_extra_connection_removed);
+
debug = ap_exists_config_define("DEBUG");

if (debug) {
98 changes: 98 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,104 @@ mod_http3 changes
Changes are listed most recent first.
Security-related entries always appear at the top of their release block.

v0.0.60 (2026-08-14)
--------------------
*) Added H3StreamTimeout, bounding how long a response may make no progress.
A client that opened a stream and then stopped reading left its worker
thread blocked on the response queue with nothing to release it: the
transport stays alive on keepalives and the idle reaper skips a session
that still has a task running, so a handful of such clients could occupy
every worker in the pool. Defaults to the server's Timeout, as
mod_http2's H2StreamTimeout does, and the window is measured per chunk of
progress so a slow but advancing transfer is unaffected.
[Alexander Gerasimov <codeguard gmail.com>]

*) Added H3MaxStreamErrors, closing a connection whose client has caused more
than that many stream errors (default 8). Answering a malformed request
per stream keeps the connection serving, which also let a client send
malformed requests indefinitely; the connection is now closed with
H3_EXCESSIVE_LOAD.
[Alexander Gerasimov <codeguard gmail.com>]

*) Applied the core LimitRequestFields and LimitRequestFieldSize limits to
HTTP/3 requests, and advertised the resulting bound as the HTTP/3
SETTINGS_MAX_FIELD_SECTION_SIZE. httpd enforces these while parsing an
HTTP/1 message, so nothing applied them to fields that arrive already
decoded: the server advertised nghttp3's default of (1<<62)-1 and header
fields accumulated in the stream pool without any bound. An over-limit
request is now answered with 431 and the connection keeps serving.
[Alexander Gerasimov <codeguard gmail.com>]

*) Advertised a QPACK dynamic table, with H3QpackTableCapacity (default 4096)
and H3QpackBlockedStreams (default 16). nghttp3 defaults the decoder
capacity to 0, which tells a client it may not use the dynamic table at
all, so every request re-sent its cookies and user-agent literally -- worse
header compression than the same server gives over HTTP/2, whose HPACK
table is 4096 by default. Setting the capacity to 0 keeps the old
behaviour.
[Alexander Gerasimov <codeguard gmail.com>]

*) Added H3MinWorkers, H3MaxWorkers and H3MaxWorkerIdleSeconds. The request
worker pool was fixed at 16 threads growing to 64 with no way to tune it,
so a large machine could not use more and a small one could not use fewer.
The defaults are the previous fixed values, so nothing changes unless
configured.
[Alexander Gerasimov <codeguard gmail.com>]

*) Resolved mod_logio once at post_config instead of walking the loaded module
list, comparing names, on every connection and again on every request.
[Alexander Gerasimov <codeguard gmail.com>]

*) Published the mod_ssl TLS environment for HTTP/3 requests: SSL_PROTOCOL,
SSL_CIPHER, SSL_CIPHER_USEKEYSIZE, SSL_CIPHER_ALGKEYSIZE,
SSL_CIPHER_EXPORT and SSL_SESSION_RESUMED, under the names mod_ssl uses.
mod_ssl does not manage these connections, so scripts and rewrite
conditions that read them saw nothing but HTTPS=on over HTTP/3. The values
are read through the new h3q_conn_tls_info() and formatted once per
connection, not per request.
[Alexander Gerasimov <codeguard gmail.com>]

*) Added H3SessionTickets, which controls whether TLS 1.3 session tickets are
issued so a returning client can resume instead of running a full
handshake. Tickets stay on by default, as before, and each worker process
keeps its own ticket keys, so a client resumes only when it returns to the
process that issued its ticket.
[Alexander Gerasimov <codeguard gmail.com>]

*) Added H3EarlyData, off by default. The OpenSSL QUIC stack has no
server-side 0-RTT, so turning early data on now logs a warning saying so
instead of silently doing nothing.
[Alexander Gerasimov <codeguard gmail.com>]

*) Added H3SocketBufferSize, which asks for the QUIC socket send and receive
buffer size. A receive buffer left at the OS default overflows once a
single connection runs at speed, and each dropped datagram costs a
retransmit; the OS still caps what it grants, and a capped grant is
logged rather than fatal.
[Alexander Gerasimov <codeguard gmail.com>]

*) Rejected malformed HTTP/3 requests (missing or duplicate pseudo-header
fields, connection-specific fields, content-length mismatch) with a
stream error of type H3_MESSAGE_ERROR per RFC 9114 4.1.2. Previously
one malformed request closed the whole QUIC connection, ending every
other request in flight on it.
[Alexander Gerasimov <codeguard gmail.com>]

*) Added support for building against httpd 2.4.52+. Without response
buckets the module removes the core HTTP_HEADER filter from its
requests and snapshots status and headers itself, mirroring
mod_http2's !AP_HAS_RESPONSE_BUCKETS path. This replaces the
"#error Not supported for the moment." guard on AP_HAS_RESPONSE_BUCKETS,
so trunk and 2.4.x now build from the same source.
[Alexander Gerasimov <codeguard gmail.com>]

*) Made the MPM connection-count notifications optional. Stock 2.4.x
MPMs do not provide them; the module then runs in a degraded mode
where a graceful child stop does not wait for active QUIC
connections to drain. The patch in .patches/httpd-2.4.66-pr699.patch
adds the notifications to httpd 2.4.x.
[Alexander Gerasimov <codeguard gmail.com>]

v0.0.59 (2026-08-12)
--------------------
*) Disabled vcpkg applocal deployment in the Windows sub-builds; parallel
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.26)

project(mod_http3 VERSION 0.0.59)
project(mod_http3 VERSION 0.0.60)

# -- Compiler and Build Type Checks --
if(NOT CMAKE_C_COMPILER_ID MATCHES "^(GNU|MSVC)$")
Expand Down
11 changes: 8 additions & 3 deletions cmake/modules/httpd.cmake
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
# -- Apache httpd v2.4.x (20211221) --
# -- Apache httpd (trunk MMN 20211221+ preferred; 2.4.52+ via compat layer) --

if(TARGET httpd)
return()
endif()

set(HTTPD_VERSION_MIN "2.4.x")
set(HTTPD_MMN_MIN "20211221")
# 2.4.52 is the floor: ap_create_request (2.4.49), the child_stopping hook
# (2.4.49) and ap_thread_current (2.4.52) must exist. Against a 2.4.x server
# mod_http3 uses its response compat path (see mod_http3/include/h3_compat.h).
# The MMN floor stays on the 2.4.x major (20120211); trunk reports 20211221 and
# compares greater, so both satisfy it.
set(HTTPD_VERSION_MIN "2.4.52")
set(HTTPD_MMN_MIN "20120211")

if(WIN32)
include(windows/httpd)
Expand Down
6 changes: 4 additions & 2 deletions docs/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ clones OpenSSL's external test submodules, which the build never uses.
| Dependency | Minimum |
| --- | --- |
| OpenSSL | 3.5.0 with QUIC support |
| Apache httpd | MMN 20211221 |
| Apache httpd | trunk (MMN 20211221) or 2.4.52+ |
| APR | 1.7.0 |
| APR-util | 1.6.0 |
| nghttp3 | 1.18.0 |

The submodule build produces these. Supply your own with the `WITH_*` options
only if they meet the minimums; a distribution httpd is usually rejected on MMN.
only if they meet the minimums; a distribution httpd is accepted from 2.4.52 on.

Against httpd trunk the module consumes response buckets directly; against 2.4.x it uses a built-in compatibility path (see `mod_http3/include/h3_compat.h`) that captures the response the way the core `HTTP_HEADER` filter would. On stock MPMs, which lack the optional `ap_mpm_note_extra_connection_added`/`_removed` functions, the module runs in a degraded mode where a graceful child stop does not wait for active QUIC connections to drain; the MPM patch in `.patches/httpd-2.4.66-pr699.patch` restores that.

## Custom Prefixes

Expand Down
Loading