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:

  1. ap_hook_translate_name() - the registration function modules call

  2. ap_run_translate_name() - the dispatch function the core calls to invoke all registered callbacks

  3. 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 _hooks struct: All hooks for a module share a static struct (_hooks) that holds their callback arrays. This is generated by the macro.

  • DECLINED is -1: The RUN_FIRST dispatch loop checks rv != -1 (which is DECLINED) to decide whether to continue. Any other return value - OK (0), DONE, or an HTTP_* error code - stops iteration.

There are two dispatch variants:

  • RUN_FIRST: Calls callbacks until one returns something other than DECLINED. 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:

  1. Numeric sort (qsort by nOrder) - groups callbacks by their priority constant

  2. Topological 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:

  1. The APR_HOOK_* constant (coarse ordering)

  2. The predecessor/successor lists (fine-grained ordering within the same level)

  3. 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) and ap_run_* (dispatch) functions from macros

  • Modules register callbacks in register_hooks() with an ordering constant (APR_HOOK_FIRST / MIDDLE / LAST) or predecessor/successor lists

  • RUN_FIRST hooks stop on the first non-DECLINED return; RUN_ALL hooks call every callback

  • ap_setup_prelinked_modules() loads all modules, calls their register_hooks, and apr_hook_sort_all resolves the final ordering

  • Return DECLINED to pass, OK when handled, HTTP_* to abort