# 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
`````{tab-set}
````{tab-item} Prefork
The traditional Unix model: one process per connection.
```mermaid
graph TD
P["Parent Process
(manages child processes)"]
P --> C1["Child Process
(idle)"]
P --> C2["Child Process
(handling request)"]
P --> C3["Child Process
(idle)"]
P --> C4["Child Process
(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:**
```apache
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)
```
````
````{tab-item} Worker
Hybrid model: multiple processes, each with multiple threads.
```mermaid
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:**
```apache
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
```
````
````{tab-item} Event
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.
```mermaid
graph TD
P["Parent Process"]
subgraph CP1["Child Process"]
direction LR
LT["Listener Thread
(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
(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:**
```apache
StartServers 3
MinSpareThreads 75
MaxSpareThreads 250
ThreadsPerChild 25
MaxRequestWorkers 400
MaxConnectionsPerChild 0
AsyncRequestWorkerFactor 2 # Async connections per worker
```
````
`````
### 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:
```c
// 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:
```mermaid
%%{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()
Modules can hook here"]
PRE e4@--> MPM["ap_run_mpm()
MPM takes over"]
MPM e5@--> CC["Create child processes"]
CC e6@--> CT["Create threads
(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
(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:
```c
// 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 {httpd}`conn_rec` with a custom bucket allocator that reads from a memory buffer. The harness calls {httpd}`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 {httpd}`ap_mpm_query` API
Modules can query MPM characteristics at runtime to adapt their behavior:
```c
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 |
|------------|-------------|
| {httpd}`AP_MPMQ_MAX_DAEMON_USED` | Highest daemon index used |
| {httpd}`AP_MPMQ_IS_THREADED` | 0=no, 1=static, 2=dynamic |
| {httpd}`AP_MPMQ_IS_FORKED` | 0=no, 1=yes |
| {httpd}`AP_MPMQ_HARD_LIMIT_DAEMONS` | Compile-time max processes |
| {httpd}`AP_MPMQ_HARD_LIMIT_THREADS` | Compile-time max threads |
| {httpd}`AP_MPMQ_MAX_THREADS` | Current max threads per process |
| {httpd}`AP_MPMQ_MAX_DAEMONS` | Max child processes |
| {httpd}`AP_MPMQ_GENERATION` | Server generation number |
## Thread Safety Considerations
With threaded MPMs (Worker, Event), modules must be thread-safe. This means no unprotected global mutable state:
````{dropdown} DON'T: Global Mutable State
```c
// WRONG: Global variable shared across threads
static int request_count = 0;
static int my_handler(request_rec *r) {
request_count++; // Race condition!
return OK;
}
```
````
````{dropdown} DO: Use Mutexes or Atomics
```c
// 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;
}
```
````
````{dropdown} DO: Use Per-Request/Connection Data
```c
// 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:
```c
#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 |
|--------|-------------|
| {httpd}`SERVER_DEAD` | Not started or dead |
| {httpd}`SERVER_STARTING` | Starting up |
| {httpd}`SERVER_READY` | Waiting for connection |
| {httpd}`SERVER_BUSY_READ` | Reading request |
| {httpd}`SERVER_BUSY_WRITE` | Writing response |
| {httpd}`SERVER_BUSY_KEEPALIVE` | Keep-alive, waiting for request |
| {httpd}`SERVER_BUSY_LOG` | Logging |
| {httpd}`SERVER_BUSY_DNS` | DNS lookup |
| {httpd}`SERVER_CLOSING` | Closing connection |
| {httpd}`SERVER_GRACEFUL` | Gracefully finishing |
| {httpd}`SERVER_IDLE_KILL` | Marked for death |
```{note}
**Fun fact**: The scoreboard's shared memory region was at the heart of [CARPE (DIEM): CVE-2019-0211](https://cfreal.github.io/carpe-diem-cve-2019-0211-apache-local-root.html), 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:
```c
// 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 {httpd}`ap_process_connection` for each connection
- Modules must be thread-safe for Worker/Event MPMs
- Use {httpd}`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 {httpd}`conn_rec` and calls {httpd}`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.