Chapter 5: MPM - Multi-Processing Modules
What is an MPM?
An MPM (Multi-Processing Module) controls how Apache handles concurrent connections. It determines:
Process vs thread model
How many workers are created
How connections are distributed
When new workers are spawned/killed
Unlike other modules, only one MPM can be active at a time. The MPM is Apache’s “engine” that drives everything - it owns the main loop that accepts connections and dispatches them to the request processing pipeline.
The Three Main MPMs
The traditional Unix model: one process per connection.
graph TD
P["Parent Process<br />(manages child processes)"]
P --> C1["Child Process<br />(idle)"]
P --> C2["Child Process<br />(handling request)"]
P --> C3["Child Process<br />(idle)"]
P --> C4["Child Process<br />(idle)"]
style P fill:#e74c3c,stroke:#c0392b,color:#000
style C1 fill:#2ecc71,stroke:#27ae60,color:#000
style C2 fill:#f39c12,stroke:#e67e22,color:#000
style C3 fill:#2ecc71,stroke:#27ae60,color:#000
style C4 fill:#2ecc71,stroke:#27ae60,color:#000
Characteristics:
Each child handles one connection at a time
Process isolation (a crash in one child doesn’t affect others)
Safe for non-thread-safe modules (PHP with mod_php)
Higher memory usage (each process has its own address space)
Good for compatibility
Configuration:
<IfModule mpm_prefork_module>
StartServers 5 # Initial child processes
MinSpareServers 5 # Minimum idle processes
MaxSpareServers 10 # Maximum idle processes
MaxRequestWorkers 250 # Max concurrent connections
MaxConnectionsPerChild 0 # Requests before child respawns (0=unlimited)
</IfModule>
Hybrid model: multiple processes, each with multiple threads.
graph TD
P["Parent Process"]
P --> CP1["Child Process 1"]
P --> CP3["Child Process N"]
P --> CP2["Child Process 2"]
CP1 --> T1A["Thread 1"]
CP1 --> T1B["Thread 2"]
CP1 --> T1C["Thread 3"]
CP2 --> T2A["Thread 1"]
CP2 --> T2B["Thread 2"]
CP2 --> T2C["Thread 3"]
CP3@{ shape: processes }
style P fill:#e74c3c,stroke:#c0392b,color:#000
style CP1 fill:#3498db,stroke:#2980b9,color:#000
style CP2 fill:#3498db,stroke:#2980b9,color:#000
style CP3 fill:#3070db,stroke:#000,color:#000
Characteristics:
Each thread handles one connection
Lower memory than prefork (threads share process memory)
Requires thread-safe modules
Better scalability
Configuration:
<IfModule mpm_worker_module>
StartServers 3 # Initial child processes
MinSpareThreads 75 # Minimum idle threads (total)
MaxSpareThreads 250 # Maximum idle threads (total)
ThreadsPerChild 25 # Threads per child process
MaxRequestWorkers 400 # Max concurrent connections
MaxConnectionsPerChild 0
</IfModule>
Async I/O model: a dedicated listener thread hands connections to worker threads, and idle keep-alive connections are handled asynchronously without tying up a worker.
graph TD
P["Parent Process"]
subgraph CP1["Child Process"]
direction LR
LT["Listener Thread<br />(async I/O, epoll)"]
WT1["Worker Thread 1"]
WT2["Worker Thread 2"]
WT3["Worker Thread 3"]
end
P --> CP1
LT req1@-->|"new request"| WT1
LT req2@-->|"new request"| WT2
LT ka@-.-> KA["Keep-alive connections<br />(held by listener)"]
req1@{ animate: true }
req2@{ animate: true }
ka@{ animate: true }
style P fill:#e74c3c,stroke:#c0392b,color:#000
style LT fill:#9b59b6,stroke:#8e44ad,color:#000
style WT1 fill:#2ecc71,stroke:#27ae60,color:#000
style WT2 fill:#2ecc71,stroke:#27ae60,color:#000
style WT3 fill:#2ecc71,stroke:#27ae60,color:#000
style KA fill:#9b59b6,stroke:#8e44ad,color:#000
Characteristics:
Dedicated listener thread for async I/O
Keep-alive connections don’t tie up worker threads (this is the key innovation over Worker)
Most efficient for high-traffic sites
Requires thread-safe modules
Default MPM on modern systems
Configuration:
<IfModule mpm_event_module>
StartServers 3
MinSpareThreads 75
MaxSpareThreads 250
ThreadsPerChild 25
MaxRequestWorkers 400
MaxConnectionsPerChild 0
AsyncRequestWorkerFactor 2 # Async connections per worker
</IfModule>
Comparison
Factor |
Prefork |
Worker |
Event |
|---|---|---|---|
Memory Usage |
High |
Medium |
Medium |
Thread Safety Required |
No |
Yes |
Yes |
Keep-alive Efficiency |
Low |
Medium |
High |
PHP mod_php |
Yes |
No |
No |
PHP-FPM |
Yes |
Yes |
Yes |
Max Connections |
~256 |
~10K |
~10K+ |
Complexity |
Simple |
Medium |
Complex |
Recommendations:
Prefork: Legacy apps, mod_php, non-thread-safe modules
Worker: Balanced performance, thread-safe modules
Event: High-traffic sites, many keep-alive connections (default choice)
How the MPM Interfaces with Apache
The MPM provides a hook that Apache’s core calls to start handling connections:
// The MPM registers this hook
ap_hook_mpm(event_run, NULL, NULL, APR_HOOK_MIDDLE);
// When called, the MPM:
// 1. Creates child processes
// 2. Creates threads (for worker/event)
// 3. Accepts connections
// 4. Calls ap_process_connection() for each connection
// 5. Manages worker lifecycle
MPM Lifecycle
Startup Sequence
When Apache starts, it initializes the runtime, parses configuration, and then hands control to the MPM. From that point on, the MPM owns the main loop - creating child processes, spawning threads, and managing their lifecycle:
%%{init: {"flowchart": { "nodeSpacing": 30, "rankSpacing": 30}}}%%
graph TD
M["main()"] e0@--> I["Initialize APR"]
I e1@--> P["Parse command line"]
P e2@--> R["Read configuration"]
R e3@--> PRE["ap_run_pre_mpm()<br />Modules can hook here"]
PRE e4@--> MPM["ap_run_mpm()<br />MPM takes over"]
MPM e5@--> CC["Create child processes"]
CC e6@--> CT["Create threads<br />(worker/event only)"]
CT e7@--> AL["Enter accept loop"]
MPM e8@--> PM["Parent monitors children"]
PM e9@--> RS["Restart dead children"]
PM e10@--> SC["Scale up/down based on load"]
PM e11@--> SIG["Handle signals<br />(HUP, TERM, etc.)"]
e0@{ animate: true }
e1@{ curve: linear }
e2@{ curve: stepAfter }
e3@{ curve: linear }
e4@{ curve: stepAfter }
e5@{ curve: stepAfter }
e6@{ curve: stepAfter }
e7@{ curve: linear }
e8@{ curve: stepAfter }
e9@{ animate: true }
e10@{ animate: true }
e11@{ animate: true }
Connection Handling
When a connection arrives, the MPM creates a connection record and runs it through Apache’s hook pipeline:
// Inside the MPM accept loop:
// 1. Accept connection
apr_socket_accept(&client_sock, listen_sock, pool);
// 2. Create connection record
conn_rec *c = ap_run_create_connection(pool, server, client_sock,
conn_id, sbh, bucket_alloc);
// 3. Run pre-connection hooks (e.g., mod_ssl sets up TLS here)
ap_run_pre_connection(c, client_sock);
// 4. Process the connection (reads requests, generates responses)
ap_process_connection(c, client_sock);
// 5. Cleanup
apr_pool_destroy(c->pool);
Important
Fuzzing note: The fuzzing harness bypasses this entire flow. Instead of the MPM accepting a socket connection, the harness creates a fake conn_rec with a custom bucket allocator that reads from a memory buffer. The harness calls ap_process_connection() directly, which means everything from step 4 onward works normally - the request parsing, hook dispatch, and module handlers are all exercised. See the Harness Design guide for details.
The ap_mpm_query() API
Modules can query MPM characteristics at runtime to adapt their behavior:
int threaded, forked;
// Is this a threaded MPM?
ap_mpm_query(AP_MPMQ_IS_THREADED, &threaded);
// Is this a forked MPM?
ap_mpm_query(AP_MPMQ_IS_FORKED, &forked);
// Maximum threads per process?
int max_threads;
ap_mpm_query(AP_MPMQ_MAX_THREADS, &max_threads);
// Maximum child processes?
int max_daemons;
ap_mpm_query(AP_MPMQ_MAX_DAEMONS, &max_daemons);
Common query codes:
Query Code |
Description |
|---|---|
Highest daemon index used |
|
0=no, 1=static, 2=dynamic |
|
0=no, 1=yes |
|
Compile-time max processes |
|
Compile-time max threads |
|
Current max threads per process |
|
Max child processes |
|
Server generation number |
Thread Safety Considerations
With threaded MPMs (Worker, Event), modules must be thread-safe. This means no unprotected global mutable state:
DON’T: Global Mutable State
// WRONG: Global variable shared across threads
static int request_count = 0;
static int my_handler(request_rec *r) {
request_count++; // Race condition!
return OK;
}
DO: Use Mutexes or Atomics
// RIGHT: Protected global state
static apr_thread_mutex_t *count_mutex;
static int request_count = 0;
static int my_handler(request_rec *r) {
apr_thread_mutex_lock(count_mutex);
request_count++;
apr_thread_mutex_unlock(count_mutex);
return OK;
}
// Or use atomics for simple counters:
static apr_uint32_t request_count = 0;
static int my_handler(request_rec *r) {
apr_atomic_inc32(&request_count);
return OK;
}
DO: Use Per-Request/Connection Data
// RIGHT: Store state in request/connection (inherently thread-safe)
typedef struct {
int my_data;
} my_request_state;
static int my_handler(request_rec *r) {
my_request_state *state = apr_pcalloc(r->pool, sizeof(*state));
state->my_data = 42;
ap_set_module_config(r->request_config, &my_module, state);
return OK;
}
Each request has its own pool and its own config vector, so per-request data is naturally thread-safe.
Scoreboard
The scoreboard is shared memory used by MPMs to track worker status. The parent process uses it to monitor children, and tools like mod_status read it to display server metrics:
#include "scoreboard.h"
// Parent can read all worker statuses
for (int i = 0; i < server_limit; i++) {
for (int j = 0; j < thread_limit; j++) {
worker_score *ws = ap_get_scoreboard_worker_from_indexes(i, j);
if (ws->status == SERVER_BUSY_READ) {
// Worker is reading request
}
}
}
// Workers update their own status
ap_update_child_status_from_indexes(child_num, thread_num,
SERVER_BUSY_WRITE, r);
Worker status values:
Status |
Description |
|---|---|
Not started or dead |
|
Starting up |
|
Waiting for connection |
|
Reading request |
|
Writing response |
|
Keep-alive, waiting for request |
|
Logging |
|
DNS lookup |
|
Closing connection |
|
Gracefully finishing |
|
Marked for death |
Note
Fun fact: The scoreboard’s shared memory region was at the heart of CARPE (DIEM): CVE-2019-0211, a local root privilege escalation exploit. An attacker who could run code as an unprivileged Apache worker (e.g., via a mod_php bug) could corrupt the scoreboard’s shared memory to hijack function pointers. When the privileged parent process read the scoreboard to manage its children, it followed the corrupted pointers and executed attacker-controlled code as root.
MPM Module Structure
Here’s a simplified view of what an MPM module looks like internally:
// From server/mpm/event/event.c (simplified)
static int event_run(apr_pool_t *_pconf, apr_pool_t *plog, server_rec *s)
{
// Set up shared memory (scoreboard)
ap_scoreboard_image = ...;
// Create child processes
for (int i = 0; i < num_daemons; i++) {
make_child(s, i);
}
// Parent loop: manage children
while (!restart_pending && !shutdown_pending) {
apr_proc_wait_all_procs(&proc, &exitcode, &why, APR_WAIT, pconf);
if (child_died) {
make_child(s, slot); // Respawn
}
if (got_SIGHUP) {
// Graceful restart
}
}
return OK;
}
// Child process main function
static void child_main(int child_num)
{
// Create threads
for (int i = 0; i < threads_per_child; i++) {
apr_thread_create(&threads[i], thread_attr,
worker_thread, (void*)i, pchild);
}
// Wait for threads
apr_thread_join(&rv, threads[i]);
}
// Worker thread function
static void *worker_thread(apr_thread_t *thd, void *data)
{
while (!dying) {
// Get a connection from queue
lr = listener_pop();
// Accept connection
apr_socket_accept(&sock, lr->sd, ptrans);
// Create connection record
conn_rec *c = ap_run_create_connection(ptrans, ...);
// Process connection
ap_run_pre_connection(c, sock);
ap_process_connection(c, sock);
// Cleanup
apr_pool_clear(ptrans);
}
return NULL;
}
// Hook registration
static void event_hooks(apr_pool_t *p)
{
ap_hook_mpm(event_run, NULL, NULL, APR_HOOK_MIDDLE);
}
AP_DECLARE_MODULE(mpm_event) = {
STANDARD20_MODULE_STUFF,
NULL, NULL, NULL, NULL,
event_cmds,
event_hooks
};
Summary
MPMs are Apache’s concurrency engine:
Prefork: One process per connection, safe but heavy
Worker: Threads in processes, balanced approach
Event: Async I/O with listener thread, most efficient
Key points:
Only one MPM active at a time
MPM controls process/thread creation
MPM calls ap_process_connection() for each connection
Modules must be thread-safe for Worker/Event MPMs
Use ap_mpm_query() to check MPM characteristics
Scoreboard tracks worker status in shared memory
For fuzzing, the MPM is bypassed entirely - the harness creates a fake conn_rec and calls ap_process_connection() directly, without any process/thread management overhead. This means the fuzzer exercises the full request processing pipeline but skips the network accept and process management layers.