Chapter 6: The Hook System
What Are Hooks?
Hooks are Apache’s primary extension mechanism. They allow modules to register callback functions that are called at specific points during request processing.
Think of hooks as event listeners. When Apache reaches a certain phase, it “runs” the hook - calling all registered callbacks in order.
%%{init: {"flowchart": {"curve": "basis", "nodeSpacing": 30, "rankSpacing": 20}}}%%
flowchart TD
subgraph A[ap_run_post_read_request]
SSL[mod_ssl<br>callback] --> LOG[mod_log<br>callback] --> XYZ[mod_xyz<br>callback]
end
A --> B
subgraph B[ap_run_translate_name]
ALIAS[mod_alias] --> PROXY[mod_proxy]
end
B --> C[...]
How Hooks Work
Every hook in Apache is generated by a pair of macros: one declares the hook’s API, and the other implements the dispatch logic.
The Hook Macros
AP_DECLARE_HOOK (from include/ap_hooks.h) declares a hook’s registration and run functions:
// In a header file (e.g., http_request.h)
AP_DECLARE_HOOK(int, translate_name, (request_rec *r))
This expands (via APR_DECLARE_EXTERNAL_HOOK in srclib/apr-util/include/apr_hooks.h) into three things:
ap_hook_translate_name() - the registration function modules call
ap_run_translate_name() - the dispatch function the core calls to invoke all registered callbacks
A global apr_array_header_t that stores the list of registered callbacks for this hook
AP_IMPLEMENT_HOOK_RUN_FIRST (from include/ap_hooks.h) provides the implementation. It wraps the APR-Util macro:
// include/ap_hooks.h
#define AP_IMPLEMENT_HOOK_RUN_FIRST(ret, name, args_decl, args_use, decline) \
APR_IMPLEMENT_EXTERNAL_HOOK_RUN_FIRST(ap, AP, ret, name, args_decl, \
args_use, decline)
Full macro expansion for translate_name
For translate_name, this expands into three generated functions:
// The registration function - called by modules in register_hooks()
void ap_hook_translate_name(ap_HOOK_translate_name_t *pf,
const char *const *aszPre,
const char *const *aszSucc, int nOrder) {
ap_LINK_translate_name_t *pHook;
if (!_hooks.link_translate_name) {
_hooks.link_translate_name = apr_array_make(
apr_hook_global_pool, 1, sizeof(ap_LINK_translate_name_t));
apr_hook_sort_register("translate_name", &_hooks.link_translate_name);
}
pHook = apr_array_push(_hooks.link_translate_name);
pHook->pFunc = pf;
pHook->aszPredecessors = aszPre;
pHook->aszSuccessors = aszSucc;
pHook->nOrder = nOrder;
pHook->szName = apr_hook_debug_current;
if (apr_hook_debug_enabled)
apr_hook_debug_show("translate_name", aszPre, aszSucc);
}
// Accessor for the hook's callback array
apr_array_header_t *ap_hook_get_translate_name(void) {
return _hooks.link_translate_name;
}
// The dispatch function - called by the core to run all callbacks
int ap_run_translate_name(request_rec *r) {
ap_LINK_translate_name_t *pHook;
int n;
int rv = -1;
if (_hooks.link_translate_name) {
pHook = (ap_LINK_translate_name_t *)_hooks.link_translate_name->elts;
for (n = 0; n < _hooks.link_translate_name->nelts; ++n) {
rv = pHook[n].pFunc(r);
if (rv != -1) // -1 is DECLINED - keep going
break; // Any other value stops the chain
}
}
return rv;
}
A few things to note:
Lazy initialization: The hook’s callback array is only created when the first module registers for it (apr_array_make inside ap_hook_translate_name()).
apr_hook_sort_register: Each hook registers itself with the global sort system so apr_hook_sort_all can find it later.
The
_hooksstruct: All hooks for a module share a static struct (_hooks) that holds their callback arrays. This is generated by the macro.DECLINEDis -1: TheRUN_FIRSTdispatch loop checksrv != -1(which isDECLINED) to decide whether to continue. Any other return value -OK (0),DONE, or anHTTP_*error code - stops iteration.
There are two dispatch variants:
RUN_FIRST: Calls callbacks until one returns something other thanDECLINED. Used by most hooks (translate_name, handler, etc.)RUN_ALL: Calls every callback and only stops on error. Used by hooks where all modules should participate (log_transaction, etc.)
Module Loading and Hook Registration
When Apache starts, it discovers all compiled-in modules, calls their register_hooks functions, and sorts the resulting callbacks. This is driven by ap_setup_prelinked_modules() in server/config.c.
When you build Apache with --enable-mods-static=all (as the fuzzer does), the build system generates a file called modules.c that lists every statically linked module in the ap_prelinked_modules array:
// Generated modules.c
module *ap_prelinked_modules[] = {
&core_module,
&so_module,
&http_module,
&mod_session,
&mod_session_cookie,
&mod_session_crypto,
// ... every statically compiled module
NULL // sentinel
};
ap_setup_prelinked_modules() walks this array and initializes each module:
// server/config.c (simplified)
void ap_setup_prelinked_modules(process_rec *process)
{
// 1. Walk the prelinked module array
for (module **m = ap_prelinked_modules; *m != NULL; m++) {
// Assign each module a unique index (module_index)
// and call its register_hooks function
ap_add_module(*m, process->pconf, NULL);
}
// 2. After ALL modules have registered their hooks,
// sort every hook's callback list
apr_hook_sort_all();
}
ap_add_module() does the critical work for each module:
Assigns a unique
module_index(used for per-module config vectors)Calls the module’s
register_hooks()function, which populates the global hook arrays
Hook Sorting
After every module has registered, apr_hook_sort_all resolves the final ordering. It iterates over every registered hook and performs a two-phase sort:
Numeric sort (
qsortbynOrder) - groups callbacks by their priority constantTopological sort (
tsort()) - within the same priority level, resolves predecessor/successor constraints into a valid ordering
flowchart TD
A["ap_setup_prelinked_modules()"] --> B["for each module in<br />ap_prelinked_modules[]"]
B --> C["ap_add_module(module)"]
C --> D["Assign module_index"]
D --> E["Call module→register_hooks()"]
E --> F["Module calls ap_hook_*() to<br />register callbacks into<br />global hook arrays"]
F --> B
B --> G["apr_hook_sort_all()"]
G --> H["for each hook"]
H --> I["sort_hook()"]
I --> J["1. qsort by nOrder"]
J --> K["2. tsort() for predecessor/<br />successor constraints"]
K --> H
style A fill:#e74c3c,stroke:#c0392b,color:#000
style G fill:#3498db,stroke:#2980b9,color:#000
The final callback order for any hook is determined by:
The
APR_HOOK_*constant (coarse ordering)The predecessor/successor lists (fine-grained ordering within the same level)
Registration order (as a tiebreaker when everything else is equal)
Note
Security note: Hook phase and ordering bugs are a real attack surface. The order of LoadModule directives in the config affects hook execution order - changing it can introduce silent inconsistencies that yield useful exploit primitives like header manipulation, auth bypass, or unexpected state reaching downstream handlers.
Using Hooks
Registering for a Hook
Modules register callbacks in their register_hooks function:
static int my_translate_name(request_rec *r)
{
if (should_handle(r)) {
r->filename = apr_pstrdup(r->pool, "/my/path");
return OK;
}
return DECLINED;
}
static void register_hooks(apr_pool_t *p)
{
ap_hook_translate_name(my_translate_name, NULL, NULL, APR_HOOK_MIDDLE);
}
The fourth parameter controls callback ordering:
APR_HOOK_REALLY_FIRST (-10)
│ mod_ssl pre_connection (needs to wrap socket early)
▼
APR_HOOK_FIRST (0)
│ Core handlers, security modules
▼
APR_HOOK_MIDDLE (10)
│ Most modules register here
│ mod_rewrite, mod_alias, etc.
▼
APR_HOOK_LAST (20)
│ Fallback handlers
│ mod_autoindex, mod_dir
▼
APR_HOOK_REALLY_LAST (30)
│ Final cleanup, logging
│ mod_log_config
For fine-grained control, specify modules that must run before/after:
static const char *predecessors[] = { "mod_alias.c", NULL };
static const char *successors[] = { "mod_proxy.c", NULL };
static void register_hooks(apr_pool_t *p)
{
// Run after mod_alias, before mod_proxy
ap_hook_translate_name(my_translate_name,
predecessors, // Must run after these
successors, // Must run before these
APR_HOOK_MIDDLE);
}
Return Values
Return values control how hook execution proceeds:
OK // Success - continue processing
DECLINED // Not handled - let others try
DONE // Request complete - skip remaining phases
HTTP_* // HTTP error code - abort with error
For RUN_FIRST hooks (most hooks), the chain works like this:
%%{init: {"flowchart": {"curve": "basis", "nodeSpacing": 80, "rankSpacing": 30}}}%%
flowchart LR
A["Module A"] --> DA{"Result?"}
DA e1@-->|DECLINED| B["Module B"] --> DB{"Result?"}
DA e2@-->|OK| H["Request handled"]
DA e3@--x|HTTP_*| E["Abort with error"]
DB e4@-->|DECLINED| C["Module C"] --> DC{"Result?"}
DB e5@-->|OK| H
DB e6@--x|HTTP_*| E
DC e7@-->|OK| H
DC e8@-->|DECLINED| N@{ shape: processes, label: "... next module ..." }
DC e9@--x|HTTP_*| E
e1@{ curve: linear }
e2@{ curve: stepBefore }
e3@{ curve: stepAfter }
e4@{ curve: linear }
e5@{ curve: stepBefore }
e6@{ curve: stepAfter }
e7@{ curve: stepBefore }
e8@{ curve: linear }
e9@{ curve: stepAfter }
style H fill:#2ecc71,stroke:#27ae60,color:#000
style E fill:#e74c3c,stroke:#c0392b,color:#000
style DA fill:#f39c12,stroke:#e67e22,color:#000
style DB fill:#f39c12,stroke:#e67e22,color:#000
style DC fill:#f39c12,stroke:#e67e22,color:#000
Creating Custom Hooks
Modules can define their own hooks for other modules to use:
// In my_module.h - declare the hook
AP_DECLARE_HOOK(int, my_custom_hook, (request_rec *r, const char *data))
// In my_module.c - implement hook infrastructure
APR_IMPLEMENT_EXTERNAL_HOOK_RUN_ALL(ap, MY_MODULE, int, my_custom_hook,
(request_rec *r, const char *data),
(r, data), OK, DECLINED)
// Call the hook somewhere in your module
int rv = ap_run_my_custom_hook(r, "some data");
// Other modules can now hook:
ap_hook_my_custom_hook(their_callback, NULL, NULL, APR_HOOK_MIDDLE);
Hook Reference
Request Processing Hooks
These hooks run in order for each HTTP request. All have the signature int (*)(request_rec *r):
1. Post-Read-Request
First chance to examine a request after headers are read.
ap_hook_post_read_request(my_post_read, NULL, NULL, APR_HOOK_MIDDLE);
static int my_post_read(request_rec *r)
{
// Log initial request info
// Set up per-request state
return DECLINED; // Let others run too
}
2. Translate Name
Map URI to filename or handler.
ap_hook_translate_name(my_translate, NULL, NULL, APR_HOOK_MIDDLE);
static int my_translate(request_rec *r)
{
if (strncmp(r->uri, "/special/", 9) == 0) {
r->filename = apr_pstrcat(r->pool, "/var/special",
r->uri + 8, NULL);
return OK; // We handled it
}
return DECLINED;
}
3. Map to Storage
Called after translate_name, before access checking.
ap_hook_map_to_storage(my_map, NULL, NULL, APR_HOOK_MIDDLE);
4. Header Parser
Parse request headers.
ap_hook_header_parser(my_header_parser, NULL, NULL, APR_HOOK_MIDDLE);
5. Access Checker
IP/host-based access control (before authentication).
ap_hook_access_checker(my_access_checker, NULL, NULL, APR_HOOK_MIDDLE);
static int my_access_checker(request_rec *r)
{
if (is_banned_ip(r->useragent_ip)) {
return HTTP_FORBIDDEN;
}
return DECLINED;
}
6. Check User ID (Authentication)
Authenticate the user.
ap_hook_check_user_id(my_authn, NULL, NULL, APR_HOOK_MIDDLE);
static int my_authn(request_rec *r)
{
const char *auth_header = apr_table_get(r->headers_in, "Authorization");
if (!auth_header) {
return DECLINED;
}
if (validate_auth(auth_header)) {
r->user = apr_pstrdup(r->pool, username);
return OK;
}
return HTTP_UNAUTHORIZED;
}
7. Auth Checker (Authorization)
Check if authenticated user is authorized.
ap_hook_auth_checker(my_authz, NULL, NULL, APR_HOOK_MIDDLE);
static int my_authz(request_rec *r)
{
if (!r->user) {
return DECLINED;
}
if (user_has_access(r->user, r->uri)) {
return OK;
}
return HTTP_FORBIDDEN;
}
8. Type Checker
Determine content type and handler.
ap_hook_type_checker(my_type_checker, NULL, NULL, APR_HOOK_MIDDLE);
static int my_type_checker(request_rec *r)
{
if (r->filename && ends_with(r->filename, ".xyz")) {
ap_set_content_type(r, "application/x-xyz");
r->handler = "xyz-handler";
return OK;
}
return DECLINED;
}
9. Fixups
Last chance to modify request before handler.
ap_hook_fixups(my_fixup, NULL, NULL, APR_HOOK_MIDDLE);
static int my_fixup(request_rec *r)
{
apr_table_set(r->headers_out, "X-Processed-By", "MyModule");
return DECLINED;
}
10. Handler
Generate the response content.
ap_hook_handler(my_handler, NULL, NULL, APR_HOOK_MIDDLE);
static int my_handler(request_rec *r)
{
if (!r->handler || strcmp(r->handler, "my-handler") != 0) {
return DECLINED;
}
ap_set_content_type(r, "text/plain");
ap_rputs("Hello from my handler!\n", r);
return OK;
}
11. Log Transaction
Log the completed request.
ap_hook_log_transaction(my_logger, NULL, NULL, APR_HOOK_MIDDLE);
static int my_logger(request_rec *r)
{
log_request(r->uri, r->status, r->bytes_sent);
return OK;
}
Connection Hooks
These hooks operate at the connection level, before HTTP parsing:
Pre-Connection
Set up connection state, filters, etc.
// Signature: int (*)(conn_rec *c, void *csd)
ap_hook_pre_connection(my_pre_conn, NULL, NULL, APR_HOOK_MIDDLE);
static int my_pre_conn(conn_rec *c, void *csd)
{
// csd is the socket descriptor
ap_add_input_filter("MY_INPUT", NULL, NULL, c);
return OK;
}
Process Connection
Handle the entire connection (used by protocol modules).
// Signature: int (*)(conn_rec *c)
ap_hook_process_connection(my_process_conn, NULL, NULL, APR_HOOK_MIDDLE);
static int my_process_conn(conn_rec *c)
{
// Custom protocol handler
// Return OK to claim the connection
return DECLINED; // Let HTTP handle it
}
Create Connection
Create the conn_rec structure.
// Signature: conn_rec* (*)(apr_pool_t *p, server_rec *s, ...)
ap_hook_create_connection(my_create_conn, NULL, NULL, APR_HOOK_MIDDLE);
Server Lifecycle Hooks
These hooks run during server startup and shutdown:
Pre Config
Called before configuration is loaded.
// Signature: int (*)(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp)
ap_hook_pre_config(my_pre_config, NULL, NULL, APR_HOOK_MIDDLE);
Post Config
Called after configuration is loaded.
// Signature: int (*)(apr_pool_t *pconf, apr_pool_t *plog,
// apr_pool_t *ptemp, server_rec *s)
ap_hook_post_config(my_post_config, NULL, NULL, APR_HOOK_MIDDLE);
static int my_post_config(apr_pool_t *pconf, apr_pool_t *plog,
apr_pool_t *ptemp, server_rec *s)
{
// Validate configuration
// Allocate shared resources
return OK;
}
Open Logs
Called when log files should be opened.
// Signature: int (*)(apr_pool_t *pconf, apr_pool_t *plog,
// apr_pool_t *ptemp, server_rec *s)
ap_hook_open_logs(my_open_logs, NULL, NULL, APR_HOOK_MIDDLE);
Child Init
Called when a child process starts.
// Signature: void (*)(apr_pool_t *p, server_rec *s)
ap_hook_child_init(my_child_init, NULL, NULL, APR_HOOK_MIDDLE);
static void my_child_init(apr_pool_t *p, server_rec *s)
{
// Initialize per-child resources
// Open database connections, etc.
}
Complete Example
Here’s a module using multiple hooks to track request timing:
#include "httpd.h"
#include "http_config.h"
#include "http_protocol.h"
#include "http_request.h"
#include "ap_config.h"
module AP_MODULE_DECLARE_DATA example_module;
typedef struct {
apr_time_t start_time;
} example_request_state;
// Post-read: record start time
static int example_post_read(request_rec *r)
{
example_request_state *state = apr_pcalloc(r->pool, sizeof(*state));
state->start_time = apr_time_now();
ap_set_module_config(r->request_config, &example_module, state);
return DECLINED;
}
// Fixup: add custom header
static int example_fixup(request_rec *r)
{
apr_table_set(r->headers_out, "X-Example-Module", "active");
return DECLINED;
}
// Handler: respond to /example
static int example_handler(request_rec *r)
{
if (!r->handler || strcmp(r->handler, "example-handler") != 0) {
return DECLINED;
}
example_request_state *state = ap_get_module_config(
r->request_config, &example_module);
ap_set_content_type(r, "text/plain");
ap_rputs("Hello from Example Module!\n", r);
if (state) {
apr_time_t elapsed = apr_time_now() - state->start_time;
ap_rprintf(r, "Processing time: %" APR_TIME_T_FMT " microseconds\n",
elapsed);
}
return OK;
}
// Log: record timing
static int example_log(request_rec *r)
{
example_request_state *state = ap_get_module_config(
r->request_config, &example_module);
if (state) {
apr_time_t elapsed = apr_time_now() - state->start_time;
ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
"Request to %s took %" APR_TIME_T_FMT " us",
r->uri, elapsed);
}
return OK;
}
// Register hooks
static void register_hooks(apr_pool_t *p)
{
ap_hook_post_read_request(example_post_read, NULL, NULL,
APR_HOOK_FIRST);
ap_hook_fixups(example_fixup, NULL, NULL,
APR_HOOK_MIDDLE);
ap_hook_handler(example_handler, NULL, NULL,
APR_HOOK_MIDDLE);
ap_hook_log_transaction(example_log, NULL, NULL,
APR_HOOK_LAST);
}
AP_DECLARE_MODULE(example) = {
STANDARD20_MODULE_STUFF,
NULL, NULL, NULL, NULL,
NULL, // No directives
register_hooks
};
See also: mod_example_hooks.c (Apache’s own hook tracing module) and the official module development guide.
Fuzzing Implications
Important
Understanding hook infrastructure matters for the fuzzing harness:
Static linking means ap_prelinked_modules contains every module we want to fuzz. The harness calls ap_setup_prelinked_modules() during initialization, which registers all hooks exactly as a real Apache would.
Hook ordering is deterministic for a given set of compiled modules. This means fuzzing results are reproducible - the same input always hits the same callback chain in the same order.
The harness can selectively disable modules by manipulating the module list before ap_setup_prelinked_modules() runs, which is useful for isolating specific code paths during targeted fuzzing.
Summary
Hooks are Apache’s plugin system:
AP_DECLARE_HOOK generates
ap_hook_*(register) andap_run_*(dispatch) functions from macrosModules register callbacks in
register_hooks()with an ordering constant (APR_HOOK_FIRST /MIDDLE/LAST) or predecessor/successor listsRUN_FIRSThooks stop on the first non-DECLINEDreturn;RUN_ALLhooks call every callbackap_setup_prelinked_modules() loads all modules, calls their
register_hooks, and apr_hook_sort_all resolves the final orderingReturn
DECLINEDto pass,OKwhen handled,HTTP_*to abort