Group
Extension

Hyperman/include/hyperman/hm_core.h

#ifndef HM_CORE_H
#define HM_CORE_H

/* Hyperman loop core: connections, HTTP parse and
 * serialize, PSGI dispatch (sync, streaming, Future), watchers, deferred
 * queue, timeouts/fairness, listener, workers, and the prefork supervisor.
 * Requires hm_future.h (futures + C closures) and hyperman.h (backends). */

#define HM_POOL_MAX     1024   /* pooled hm_conn structs kept per loop     */
#define HM_IOV_MAX      64     /* writev: header + up to 63 body chunks    */
#define HM_SWEEP_SECS   5.0    /* timeout sweep interval                    */
#define HM_HIDX_STACK   64     /* header-line index on the stack before
                                  falling back to the loop's growable one  */
#define HM_WB_HIWAT     (1 << 20)  /* queued response bytes a connection may
                                      hold before hm_process stops serving
                                      its pipeline until the client reads  */
#define HM_WB_KEEP      (1 << 20)  /* a pooled connection keeps a write
                                      buffer up to this; larger ones are
                                      freed, so one big response does not
                                      pin its buffer for the pool's life  */

#include "hm_win.h"     /* the socket/IO shim: hm_os_*, hm_iovec, hm_memmem.
                           First, so every later header sees the platform
                           headers it pulls; a passthrough on POSIX. */
#include "hm_parse.h"   /* hm_hline, hm_parse_index, hm_chunked_* */
#include "hm_proxy.h"   /* the PROXY protocol header, v1 and v2  */
#ifndef _WIN32
#include <pwd.h>        /* user => 'name' (hm_resolve_drop)        */
#include <grp.h>        /* group => 'name'                          */
#include <netdb.h>      /* getaddrinfo (hm_resolve)                 */
#include <sys/un.h>     /* path => listeners                        */
#include <sys/stat.h>
#endif
#include "hm_abi.h"     /* public C ABI: hm_abi table + callback typedefs */

#if defined(HM_HAVE_SENDFILE_LINUX)
#include <sys/sendfile.h>
#elif defined(HM_HAVE_SENDFILE_FREEBSD) || defined(HM_HAVE_SENDFILE_MACOS)
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/uio.h>
#endif

typedef struct hm_loop hm_loop;

/* A streaming response body (filehandle / IO-object PSGI bodies): instead
 * of slurping the whole file into one SV, the connection drains its write
 * buffer and refills it a chunk at a time - or hands the file straight to
 * sendfile(2) on a plaintext socket. `sv` keeps the body (and so its fd)
 * alive; kind 1 reads the fd at an explicit offset (pread), kind 2 pulls
 * a getline object. At most HM_BSRC_CHUNK response-body bytes are ever
 * buffered per connection. */
#define HM_BSRC_CHUNK    65536
#define HM_SENDFILE_MAX  (1 << 20)   /* bytes per sendfile(2) call */
typedef struct {
    int           kind;        /* 0 none, 1 regular-file fd, 2 getline    */
    int           fd;          /* kind 1: borrowed from the body's handle */
    off_t         off;         /* kind 1: next byte to read               */
    UV            remaining;   /* bytes still owed to the client          */
    SV           *sv;          /* the body, owned: keeps the fd alive     */
    unsigned char nosf;        /* sendfile(2) refused; use the read path  */
} hm_bsrc;
typedef struct hm_conn hm_conn;

/* A bound listening socket. The loop holds an array of these; a connection
 * remembers the one it was accepted on so TLS-wrap, SERVER_PORT, and the
 * built-in https redirect are all per-listener. */
typedef struct {
    int   fd;             /* bound listener fd, -1 until bound        */
    char *host;           /* bind host (owned)                        */
    int   port;
    void *tls_ctx;        /* SSL_CTX*; NULL = plain                   */
    int   http2;          /* per-listener h2 (h2c / ALPN)             */
    int   http3;          /* per-listener h3: a QUIC listener on the
                           * same host and port, over UDP. Intrinsically
                           * the same origin as the TLS listener it sits
                           * on - same certificate, same SNI registry -
                           * which is why it is a flag here rather than
                           * a listener of its own (hm_quic.h)        */
    int   redirect_https; /* 0 = off; else target https port for 301  */

    /* What the context above was built from, kept so a worker can build
     * another one. The spec these are copied from lives in the parent's
     * cfg, which a worker does not hold - and without them tls_reload
     * would have nothing to rebuild with. Owned copies: the spec's
     * strings belong to the parent's config and outlive nothing here. */
    char *tls_cert;       /* NULL on a plain listener                 */
    char *tls_key;
    char *tls_ca;         /* NULL unless client certs are verified    */
    int   tls_verify;
    SV   *tls_sni;        /* the SNI map run() was given, or NULL: a
                           * tls_reload with no map rebuilds from it   */

    /* The QUIC half, when http3 is on. It rides on this listener rather
     * than being one of its own because h3 is intrinsically the same origin
     * as the TLS listener it advertises from: same host, same port number,
     * same certificate, same SNI registry - only the transport differs. */
    int   udp_fd;         /* bound SOCK_DGRAM fd, or -1               */
    void *quic;           /* hm_quic_srv *, or NULL                   */
    void *quic_ctx;       /* the QUIC-side SSL_CTX                    */
    char *altsvc;         /* precomputed Alt-Svc value, or NULL       */
    int   starved;        /* accept hit EMFILE: read interest is off
                           * until a short timer puts it back        */
    int   is_unix;        /* a path listener: no peer address, no
                           * TCP options, SERVER_PORT 0              */
    int   proxy_protocol; /* a connection starts with a PROXY header
                           * (hm_proxy_readable), before any TLS     */
} hm_listener;

struct hm_conn {
    int      fd;
    char    *rbuf;          /* owned; grows for large bodies, capacity kept */
    size_t   rlen, rcap;
    char    *wbuf;          /* owned; capacity kept across requests */
    size_t   wlen, woff, wcap;
    int      writing;       /* write watcher armed?                 */
    int      keepalive;
    int      awaiting;      /* parked on a pending Future?          */
    UV       id;            /* generation id, guards fd reuse       */
    UV       nreqs;         /* requests served on this connection   */
    SV      *io_sv;         /* psgix.io: dup'd filehandle, per conn */
    SV      *cid_sv;        /* psgix.hyperman.conn: [fd, id], per conn */
    SV      *peer_sv;       /* cached env values, constant for the  */
    SV      *peer_host_sv;  /* life of the connection: REMOTE_ADDR, */
    SV      *peer_port_sv;  /* REMOTE_HOST, REMOTE_PORT,            */
    SV      *sname_sv;      /* SERVER_NAME,                         */
    SV      *sport_sv;      /* SERVER_PORT                          */
    SV      *resp_f;        /* pending response Future (awaiting)   */
    SV      *env_sv;        /* held while parked, for access_log    */
    void    *h2;            /* hm_h2_sess* when in HTTP/2 mode       */
    void    *ssl;           /* SSL* when this is a TLS connection    */
    const char *tls_proto;  /* SSL_PROTOCOL / SSL_CIPHER for $env.   */
    const char *tls_cipher; /* OpenSSL-owned, valid for the conn     */
    void    *tls_peer;      /* hm_tls_peer*, only if a client cert   */
    unsigned char tls_hs;         /* TLS handshake in progress       */
    unsigned char tls_r_wants_w;  /* SSL_read blocked wanting write  */
    unsigned char tls_failed;     /* SSL_new failed at accept: closed  */
    unsigned char stream_open;    /* a streamed response has begun and
                                   * not closed: one per request       */
    UV            body_rx;        /* body bytes since body_phase began */
    time_t        body_t0;        /* when body_phase began             */
    unsigned char tls_w_wants_r;  /* SSL_write blocked wanting read  */
    unsigned char ktls_tx;        /* TX offloaded to the kernel (reporting
                                   * only - the write paths ask OpenSSL
                                   * itself, see hm_ktls_tx)          */
    unsigned char recv_fallback;  /* this connection reads off readiness for
                                   * good: completion was unavailable or
                                   * refused once, and a persistent watcher
                                   * is armed                    */
    unsigned char recv_inflight;  /* a completion-mode recv is outstanding
                                   * into c->rbuf; the kernel may write to
                                   * it at any moment, so it must not be
                                   * grown, freed or reused until the
                                   * completion lands            */
    unsigned char recv_lent;      /* reading off a lent readiness watcher
                                   * while requests wait unconsumed in
                                   * rbuf (hm_recv_unsafe)        */
    unsigned char again_queued;   /* hm_again_push has it for the top of
                                   * the next turn (hm_run_again) */
    unsigned char detached;       /* app took the socket (see hm_detach) */
    unsigned char tunnel;         /* a 101 stream handle owns the bytes both
                                   * ways (hm_stream_open): what the peer
                                   * sends goes to the handle's read half,
                                   * never to the request parser         */
    unsigned char accepts_gzip;   /* this request's Accept-Encoding    */
    unsigned char cont_sent;      /* a 100 Continue has gone out for the
                                   * request being read. Per request, and
                                   * on the connection rather than on the
                                   * stack, because the headers stay in rbuf
                                   * and hm_process re-reads them on every
                                   * readable event until the body is whole */
    unsigned char is_head;        /* the request being answered is a HEAD:
                                   * the headers go out, the body does not */
    unsigned char in_app;         /* a synchronous handler for this
                                   * connection is on the C stack. A ->get
                                   * inside it pumps this same loop, so a
                                   * close asked for meanwhile is deferred
                                   * to the frame that called the app   */
    unsigned char close_deferred; /* hm_close was asked for while in_app */
    unsigned char read_paused;    /* read interest dropped while in_app with
                                   * a full buffer (hm_read_pause)       */
    unsigned char pp_wait;        /* awaiting the PROXY header: nothing
                                   * else reads this socket until it is
                                   * parsed (hm_proxy_readable)         */
    char     proxy_addr[INET6_ADDRSTRLEN]; /* the proxy that connected, once
                                   * the header named the real client */
    int      proxy_port;
    unsigned char backlogged;     /* hm_process stopped on a full write
                                   * buffer; the drain re-queues it     */
    unsigned char req_11;         /* the request on the plate is HTTP/1.1 */
    unsigned char stream_mode;    /* a streamed body's framing (HM_SM_*) */
    unsigned char stream_done;    /* a streamed body ended with keep-alive
                                   * on: the drain serves the pipeline   */
    UV       stream_serial; /* streamed responses begun on this conn; a
                             * Writer carries the one it belongs to, so a
                             * writer kept past its response cannot end
                             * the next one                              */
    UV       stream_left;   /* HM_SM_LENGTH: body octets still owed      */
    size_t   chunk_pos;     /* chunked body: octets validated so far, so
                             * a re-scan starts at the last chunk boundary
                             * rather than at the first chunk           */
    size_t   chunk_dec;     /* decoded octets up to chunk_pos           */
    hm_bsrc  bsrc;          /* streaming body being dripped, if any */
    int      last_status;   /* last response status/bytes (logging) */
    size_t   last_blen;
    char     peer[INET6_ADDRSTRLEN];  /* REMOTE_ADDR, filled at accept    */
    int      peer_port;               /* REMOTE_PORT, filled at accept    */
    time_t   last_active;
    time_t   req_start;     /* first byte of an incomplete request  */
    time_t   last_rx;       /* last read that delivered bytes: the body
                             * phase is timed from here, not from the
                             * request's first byte                  */
    unsigned char body_phase; /* headers complete, body still arriving:
                             * hm_sweep applies body_timeout        */
    /* A body being drained to a temp file rather than accumulated in rbuf.
     * -1 when not spilling. The fd becomes psgi.input and the handle owns it
     * from that moment; the file is unlinked at creation, so nothing is left
     * behind however the request ends. */
    int      spill_fd;
    long     spill_got;     /* body octets already written to it    */
    hm_conn *lru_prev, *lru_next;
    hm_conn *pool_next;
    hm_loop *loop;
    hm_listener *lst;       /* listener this conn was accepted on   */
};

/* QUIC's expiry min-heap. hm_quic.h owns every operation on it; the type is
 * here because hm_loop carries one BY VALUE, so the "has QUIC anything to do"
 * test on the loop's hot path is a load and a compare rather than a
 * dereference. ngtcp2 rearms a connection's expiry after essentially every
 * packet, and none of the backend timer vtables can carry that - see the
 * header comment in hm_quic.h for the costing. */
typedef struct hm_qconn hm_qconn;
typedef struct { hm_qconn **a; int n, cap; } hm_qheap;

/* app-owned fd watcher: a one-shot Future, a persistent Perl callback, or a
 * persistent C callback (the ABI path - dispatched with no Perl frame) */
typedef struct {
    SV            *sv;      /* future or Perl cb; NULL for a C watcher */
    unsigned char  is_cb;
    hm_abi_io_cb   c_cb;    /* C watcher; checked before sv            */
    void          *c_ud;
} hm_iow;

/* timer watcher */
#define HM_TW_FUTURE 0
#define HM_TW_CB     1
#define HM_TW_SWEEP  2
#define HM_TW_HARDSTOP 3
#define HM_TW_C      4      /* ABI: C callback, cancellable via handle */
typedef struct hm_tw_s {
    int kind;
    SV *sv;
    hm_loop *loop;
    hm_abi_timer_cb c_cb;   /* HM_TW_C only */
    void           *c_ud;
    double          due;    /* oneshot deadline, monotonic seconds; 0 = none */
    /* Cancelled, and waiting on the loop's graveyard to be freed. The
     * backends fill a whole event array before any of it is dispatched, so
     * a timer cancelled from an earlier event in the batch may still be
     * named by a later one; freeing it at once was a use after free. */
    unsigned char   dead;
    struct hm_tw_s *gy_next;
} hm_tw;

/* The deadline a oneshot is armed for, on the clock hm_dispatch checks it
 * against. The backend's own timer is a wake-up hint, not the deadline:
 * kqueue's EVFILT_TIMER on NetBSD is a callout in scheduler ticks, and in a
 * VM those ticks run ahead of CLOCK_MONOTONIC. */
#define hm_tw_due(secs) ((double)hm_now_ns() / 1e9 + (secs))

typedef struct { int fd; UV id; } hm_again;

/* One piece of an access-log line. The format string is compiled to these
 * once, in the parent, so writing a line is a walk over an array and never a
 * parse: a literal run, an $env value (the key SV carries its hash), or one
 * of the computed fields. */
#define HM_LT_LIT      0   /* literal bytes                                 */
#define HM_LT_ENV      1   /* $env->{key}, escaped; "-" when absent         */
#define HM_LT_DASH     2   /* %l: always "-"                                */
#define HM_LT_STATUS   3   /* %s, %>s                                       */
#define HM_LT_BYTES    4   /* %b: body bytes, "-" when there were none      */
#define HM_LT_BYTES0   5   /* %B: body bytes, "0" when there were none      */
#define HM_LT_TIME     6   /* %t: [dd/Mon/YYYY:HH:MM:SS +ZZZZ]              */
#define HM_LT_TIME_ISO 7   /* json: YYYY-MM-DDTHH:MM:SS+ZZZZ                */
#define HM_LT_REQLINE  8   /* %r: METHOD URI PROTO                          */
#define HM_LT_QUERY    9   /* %q: ?query, or nothing                        */
#define HM_LT_PID     10   /* %P                                            */
#define HM_LT_DUR_US  11   /* %D: microseconds since the request was built  */
#define HM_LT_DUR_S   12   /* %T: whole seconds of the same                 */
typedef struct {
    int     kind;
    char   *lit;        /* HM_LT_LIT: owned                                */
    size_t  litlen;
    SV     *key;        /* HM_LT_ENV: a shared-string SV, hash precomputed */
} hm_logtok;

struct hm_loop {
    int         pid;                /* the process that created this loop
                                     (hm_os_getpid: MSVC has no pid_t) */
    hm_backend *be;
    hm_conn   **conns;              /* [HM_MAXFD] */
    hm_iow     *io_r, *io_w;        /* [HM_MAXFD] app io watchers */
    AV         *deferred;           /* run-soon callbacks          */
    hm_again   *again;              /* conns with buffered requests */
    int         again_n, again_cap;
    hm_hline   *hidx;               /* header-line index overflow buffer */
    int         hidx_cap;
    hm_conn    *lru_head, *lru_tail;
    hm_conn    *pool; int pool_n;
    /* Completion-mode receive (io_uring only, opt-in). `completion` is set
     * once at loop start when the backend can do it and the operator asked.
     * `quar` holds read buffers belonging to connections that closed with a
     * recv still outstanding - see hm_quar_push. */
    int         completion;
    struct hm_quar *quar;
    hm_tw      *tw_gy;              /* cancelled timers, freed between batches */
    int         starved_retry;      /* a listener waits for hm_accept_unstarve */
    hm_tw      *sweep_tw;
    SV         *app;                /* server: PSGI app coderef     */
    hm_listener *listeners;         /* server: bound listeners      */
    int         nlisteners;
    int         attached;           /* server attached (>=1 listener) */
    int         nconns;
    int         stop;
    int         stopping;           /* graceful shutdown in progress */
    int         running;
    int         perl_owned;         /* created via Hyperman::Loop->new */
    time_t      now;
    double      idle_timeout;
    double      header_timeout;
    double      body_timeout;       /* a body stalled this long is closed */
    double      request_timeout;    /* whole request ceiling; 0 = none    */
    UV          body_min_rate;      /* bytes/s floor over a body; 0 = none */
    int         max_pipeline;       /* fairness: requests per conn per wakeup */
    size_t      max_read;           /* request size ceiling (headers + body) */
    int         compress;          /* gzip responses (0 = off, the default) */
    size_t      compress_min;      /* below this, gzip buys nothing         */
    int         compress_level;    /* gzip level; HZ_LEVEL by default       */
    SV         *compress_types;    /* AV of extra compressible media types  */
    SV         *self_sv;            /* cached psgix.loop wrapper */
    SV         *log_cb;             /* access_log coderef, or NULL */
    int         log_fd;             /* fast access-log fd (C writer), or -1 */
    char       *log_buf;            /* pending Combined-format log lines    */
    size_t      log_len, log_cap;
    time_t      log_ts_sec;         /* second the cached stamp below is for  */
    char        log_ts[32];         /* "dd/Mon/YYYY:HH:MM:SS +ZZZZ", 1Hz     */
    time_t      date_sec;           /* second the Date header below is for   */
    char        date_hdr[40];       /* "Date: <IMF-fixdate>\r\n", 1Hz        */
    hm_logtok  *logtok;             /* the compiled access_log_format        */
    int         nlogtok;
    int         log_json;           /* values escaped for JSON, not CLF      */
    int         log_wants_t0;       /* the format reads %D/%T: stamp requests */
    time_t      log_iso_sec;        /* second the ISO stamp below is for     */
    char        log_iso[40];        /* "YYYY-MM-DDTHH:MM:SS+ZZZZ", 1Hz        */
    UV          requests;           /* requests dispatched (stats)  */
    UV          accepts;            /* connections accepted (stats) */
    UV          denied;             /* connections dropped at accept, denylist */
    UV          bytes_out;          /* response bytes queued (stats) */
    UV          datagrams;          /* QUIC datagrams taken off the wire */
    UV          h3_conns;           /* QUIC handshakes completed         */
    UV          h3_live;            /* QUIC connections open now         */
    UV          h3_requests;        /* requests served over HTTP/3       */
    UV          datagrams_out;      /* QUIC datagrams sent (one sendto
                                     * each: no GSO anywhere yet)       */
    UV          http3_max_conns;    /* ceiling on h3_live, 0 = unlimited */
    hm_qheap    qheap;              /* QUIC expiry deadlines (hm_quic.h) */
    UV          max_requests;       /* recycle worker after N (0=off) */
    int         recycle_pending;    /* max_requests hit; drain at loop top */
    double      shutdown_grace;     /* graceful-drain bound (secs)   */
    hm_tw      *hardstop_tw;
};

/* One served request, counted, and max_requests tested - for every protocol
 * that finishes one. The bound belongs to the worker and not to HTTP/1: a
 * site whose browsers all negotiate h2 would otherwise never reach it, and
 * the worker it was meant to retire grows for as long as the box lets it.
 *
 * Not on the 101 paths (h2c upgrade, detach, tunnel), which count their
 * request and deliberately do not test: the socket there has just left this
 * loop for an application that means to hold it open, and a drain decided by
 * the request that handed it over is the one moment it must not happen.
 *
 * A function and not a macro because the argument is read five times. */
static void hm_count_request(hm_loop *loop) {
    loop->requests++;
    if (loop->max_requests && loop->requests >= loop->max_requests
        && !loop->stopping && loop->attached)
        loop->recycle_pending = 1;  /* drain at the loop iteration boundary */
}

/* Does a Hyperman::Writer's tag slot hold this string? The four-element
 * writer shapes (h2 and h3) have the same arity, so they are told apart by
 * the tag and never by length - a length test routes an HTTP/3 body into an
 * nghttp2 session. Used from xs/writer.xs. */
static int hm_writer_tag_is(pTHX_ AV *w, const char *want, STRLEN wantlen) {
    SV **t = av_fetch(w, 3, 0);
    STRLEN l;
    const char *p;
    if (!t || !*t || !SvOK(*t)) return 0;
    p = SvPV(*t, l);
    return l == wantlen && memcmp(p, want, l) == 0;
}

/* Find the listener owning a ready fd (listener count is tiny). */
static hm_listener *hm_listener_for_fd(hm_loop *loop, int fd) {
    int i;
    for (i = 0; i < loop->nlisteners; i++)
        if (loop->listeners[i].fd == fd) return &loop->listeners[i];
    return NULL;
}

/* The same, for the QUIC side. Separate rather than a flag on the above,
 * because a UDP listener fd is never in loop->conns[] and the caller wants
 * to know which of the two it has. */
static hm_listener *hm_listener_for_udp(hm_loop *loop, int fd) {
    int i;
    for (i = 0; i < loop->nlisteners; i++)
        if (loop->listeners[i].udp_fd >= 0 && loop->listeners[i].udp_fd == fd)
            return &loop->listeners[i];
    return NULL;
}

static SV      *hm_empty_input;     /* shared empty psgi.input */
static SV      *hm_empty_input_sv(pTHX);   /* ... checked still open */
#ifndef _WIN32
static pid_t    hm_children[1024];  /* the supervisor's pool (POSIX only) */
static time_t   hm_child_start[1024];
/* The WORKER INDEX each of those was forked with, which is not its position
 * here: this array is kept packed by swapping the last entry down over a
 * departed one, so a position means nothing beyond "somebody lives here". The
 * index is an identity - it picks the worker's waker slot and its CPU - so it
 * has to travel with the child rather than be recomputed from where the child
 * happens to sit. */
static int      hm_child_widx[1024];
static int      hm_nchildren = 0;
#endif
static UV       hm_id_counter = 0;

static SV *hm_loop_to_sv(pTHX_ hm_loop *loop);
static void hm_on_signal(pTHX_ hm_loop *loop);
static void hm_deliver(pTHX_ int fd, UV id, SV *resp);
static int hm_flush(pTHX_ hm_conn *c);
static void hm_process(pTHX_ hm_conn *c);
static int hm_queue_response(pTHX_ hm_conn *c, SV *resp);
static void hm_close(pTHX_ hm_loop *loop, hm_conn *c);
static void hm_wb_reserve(hm_conn *c, size_t need);
static void hm_wb_put(hm_conn *c, const char *p, size_t n);
static void hm_again_push(hm_loop *loop, hm_conn *c);
static void hm_tw_gy_sweep(hm_loop *loop);
static void hm_h2_free(pTHX_ void *h2);   /* defined in hm_http2.h */
static void hm_readable(pTHX_ hm_conn *c);
static void hm_read_resume(hm_loop *loop, hm_conn *c);

/* ABI v6 stream handles, defined in hm_stream.h (included below, after the
 * HTTP/1 and HTTP/2 halves it sits on top of). The two counters are read on
 * the close and flush paths rather than called into, because they are zero
 * for every connection that never opened a handle - which is nearly all of
 * them - and a list walk per flush is not free. */
static int  hm_stream_live_n   = 0;
static int  hm_stream_paused_n = 0;
static void hm_quic_readable(pTHX_ hm_loop *loop, hm_listener *lst); /* hm_quic.h */
static void hm_quic_expire(pTHX_ hm_loop *loop);                     /* hm_quic.h */
static void hm_quic_srv_attach(pTHX_ hm_loop *loop, hm_listener *lst, int widx);
static void hm_quic_srv_free(pTHX_ hm_listener *lst);
static void hm_quic_shutdown(pTHX_ hm_loop *loop);
static void hm_quic_ctx_swap(pTHX_ hm_listener *lst, void *fresh);
static uint64_t hm_quic_next_deadline(hm_loop *loop);
static void hm_quic_secret_init(void);
static void hm_stream_conn_gone(pTHX_ int fd, UV id);
static int  hm_stream_deliver(pTHX_ int fd, UV cid, int64_t sid,
                              const char *buf, STRLEN len, int fin);
static void hm_stream_h2_gone(pTHX_ int fd, UV id, int64_t sid);
static void hm_stream_drained(pTHX_ hm_conn *c);
static int  hm_h2_reentrant(hm_conn *c);  /* hm_http2.h */

#include "hm_tls.h"                        /* SSL_CTX, wrap, hm_cread/hm_cwrite */

/* ---- small utilities ----------------------------------------------------- */

static const char *hm_reason(int s) {
    switch (s) {
        case 100: return "Continue";      case 101: return "Switching Protocols";
        case 102: return "Processing";    case 103: return "Early Hints";
        case 200: return "OK";            case 201: return "Created";
        case 204: return "No Content";    case 301: return "Moved Permanently";
        case 302: return "Found";         case 304: return "Not Modified";
        case 400: return "Bad Request";   case 401: return "Unauthorized";
        case 403: return "Forbidden";     case 404: return "Not Found";
        case 405: return "Method Not Allowed";
        case 426: return "Upgrade Required";
        case 500: return "Internal Server Error";
        default:  return "OK";
    }
}

/* Tiny decimal formatters: snprintf's format parsing and locale machinery
 * are measurable at hot-path request rates. Return bytes written. */
static size_t hm_utoa(char *out, unsigned long v) {
    char tmp[20];
    size_t n = 0, i;
    do { tmp[n++] = (char)('0' + v % 10); v /= 10; } while (v);
    for (i = 0; i < n; i++) out[i] = tmp[n - 1 - i];
    return n;
}
static size_t hm_itoa(char *out, long v) {
    if (v < 0) { out[0] = '-'; return 1 + hm_utoa(out + 1, 0UL - (unsigned long)v); }
    return hm_utoa(out, (unsigned long)v);
}

/* Status line for the response: precomputed for the common statuses, built
 * into buf (>= 64 bytes) otherwise. Sets *len, returns the line. */
static const char *hm_status_line(int s, size_t *len, char *buf) {
#define HM_SL(str) do { *len = sizeof(str) - 1; return str; } while (0)
    switch (s) {
        case 200: HM_SL("HTTP/1.1 200 OK\r\n");
        case 201: HM_SL("HTTP/1.1 201 Created\r\n");
        case 204: HM_SL("HTTP/1.1 204 No Content\r\n");
        case 301: HM_SL("HTTP/1.1 301 Moved Permanently\r\n");
        case 302: HM_SL("HTTP/1.1 302 Found\r\n");
        case 304: HM_SL("HTTP/1.1 304 Not Modified\r\n");
        case 400: HM_SL("HTTP/1.1 400 Bad Request\r\n");
        case 401: HM_SL("HTTP/1.1 401 Unauthorized\r\n");
        case 403: HM_SL("HTTP/1.1 403 Forbidden\r\n");
        case 404: HM_SL("HTTP/1.1 404 Not Found\r\n");
        case 405: HM_SL("HTTP/1.1 405 Method Not Allowed\r\n");
        case 500: HM_SL("HTTP/1.1 500 Internal Server Error\r\n");
    }
#undef HM_SL
    {
        const char *r = hm_reason(s);
        size_t rl = strlen(r), n = 9;
        memcpy(buf, "HTTP/1.1 ", 9);
        n += hm_itoa(buf + n, (long)s);
        buf[n++] = ' ';
        memcpy(buf + n, r, rl); n += rl;
        buf[n++] = '\r'; buf[n++] = '\n';
        *len = n;
        return buf;
    }
}

/* Format a peer sockaddr into a printable REMOTE_ADDR string and its port ("" /
 * 0 on failure or for address families we don't serve). */
static void hm_fmt_peer(char *out, size_t outlen, int *port, const struct sockaddr *sa) {
    out[0] = '\0';
    if (port) *port = 0;
    if (!sa) return;
    if (sa->sa_family == AF_INET) {
        const struct sockaddr_in *s4 = (const struct sockaddr_in *)sa;
        inet_ntop(AF_INET, &s4->sin_addr, out, (socklen_t)outlen);
        if (port) *port = ntohs(s4->sin_port);
    } else if (sa->sa_family == AF_INET6) {
        const struct sockaddr_in6 *s6 = (const struct sockaddr_in6 *)sa;
        /* An IPv4 client of a dual-stack listener arrives as ::ffff:a.b.c.d.
         * Formatted as the IPv4 it is: the denylist and the rate limiter
         * key on the string, and "1.2.3.4" must be one client whichever
         * listener it came through. */
        if (IN6_IS_ADDR_V4MAPPED(&s6->sin6_addr))
            inet_ntop(AF_INET, ((const unsigned char *)&s6->sin6_addr) + 12,
                      out, (socklen_t)outlen);
        else
            inet_ntop(AF_INET6, &s6->sin6_addr, out, (socklen_t)outlen);
        if (port) *port = ntohs(s6->sin6_port);
    }
}

static void hm_set_nonblock(int fd) { hm_os_set_nonblock(fd); }

/* fd of a filehandle / IO ref, or a plain fd number */
static int hm_fd_of(pTHX_ SV *sv) {
    if (SvROK(sv)) {
        IO *io = sv_2io(sv);
        if (io && IoIFP(io)) return PerlIO_fileno(IoIFP(io));
        croak("Hyperman: not an open filehandle");
    }
    return (int)SvIV(sv);
}

/* a fresh anonymous glob with an IO slot (standard idiom) */
static GV *hm_anon_glob(pTHX) {
    GV *gv = newGVgen("Hyperman");
    (void)GvIOn(gv);
    SvREFCNT_inc((SV *)gv);
    (void)hv_delete(GvSTASH(gv), GvNAME(gv), GvNAMELEN(gv), G_DISCARD);
    return gv;
}

/* A read handle over an already-open descriptor, rewound. The handle owns the
 * descriptor from this point. */
static SV *hm_input_from_fd(pTHX_ int fd) {
    PerlIO *fp;
    GV *gv;
    IO *io;
    if (PerlLIO_lseek(fd, 0, SEEK_SET) < 0) { PerlLIO_close(fd); return NULL; }
    fp = PerlIO_fdopen(fd, "rb");
    if (!fp) { PerlLIO_close(fd); return NULL; }
    gv = hm_anon_glob(aTHX);
    if (!gv) { PerlIO_close(fp); return NULL; }
    io = GvIOn(gv);
    IoIFP(io) = fp;
    IoTYPE(io) = IoTYPE_RDONLY;
    return newRV_noinc((SV *)gv);
}

/* A temp file for a body being drained out of the read buffer.
 *
 * Unlinked the instant it exists, so the descriptor is its only reference: it
 * goes away when the request ends, when the handler dies, or when the worker
 * is killed, and there is no cleanup path anywhere to forget. Nothing is
 * inherited across a fork either, which is the failure this workspace keeps
 * meeting from the other direction. */
/* Where a body temp file goes. TMPDIR is the POSIX spelling of the question
 * and TEMP the Windows one - asking only the first left Windows falling back
 * to a "/tmp" that is not there, which is a failed mkstemp on every request
 * carrying a body. */
static const char *hm_tmpdir(void) {
    const char *d = getenv("TMPDIR");
    if (d && *d) return d;
#ifdef _WIN32
    if ((d = getenv("TEMP")) && *d) return d;
    if ((d = getenv("TMP"))  && *d) return d;
    return ".";
#else
    return "/tmp";
#endif
}

/* dir + "/hm-body-XXXXXX" into buf, or 0 if the directory will not fit. */
static int hm_body_tmppath(char *buf, size_t cap) {
    const char *dir = hm_tmpdir();
    size_t dl = strlen(dir);
    if (dl + sizeof("/hm-body-XXXXXX") > cap) return 0;
    my_strlcpy(buf, dir, cap);
    if (dl && dir[dl - 1] != '/' && dir[dl - 1] != '\\')
        my_strlcat(buf, "/", cap);
    my_strlcat(buf, "hm-body-XXXXXX", cap);
    return 1;
}

static int hm_body_tmpfile(void) {
    char path[512];
    int fd;
    if (!hm_body_tmppath(path, sizeof path)) return -1;
    fd = mkstemp(path);
    if (fd < 0) return -1;
    (void)unlink(path);
    return fd;
}

/* Above this, the body goes to a temp file instead of a second copy in
 * memory. Below it, the copy is cheaper than an inode and three syscalls. */
#ifndef HM_SPILL_BODY
#  define HM_SPILL_BODY (1024 * 1024)
#endif

/* psgi.input over a temp file, for a body large enough that a second copy in
 * memory is the wrong trade.
 *
 * The file is UNLINKED as soon as it is open, and the handle keeps it alive:
 * it vanishes when the handle is closed, whether that is a normal response, a
 * handler that died, or the worker being killed mid-request. There is no
 * cleanup path to forget and nothing on disk for another process to find.
 *
 * Returns NULL to fall back to the in-memory handle, because a server that
 * cannot make a temp file should still serve the request.
 */
static SV *hm_new_input_file(pTHX_ const char *body, STRLEN len) {
    char path[512];
    int fd;
    STRLEN off = 0;

    if (!hm_body_tmppath(path, sizeof path)) return NULL;

    fd = mkstemp(path);
    if (fd < 0) return NULL;
    (void)PerlLIO_unlink(path);          /* the handle is the only reference */

    while (off < len) {
        SSize_t n = PerlLIO_write(fd, body + off, len - off);
        if (n <= 0) { PerlLIO_close(fd); return NULL; }
        off += (STRLEN)n;
    }
    if (PerlLIO_lseek(fd, 0, SEEK_SET) < 0) { PerlLIO_close(fd); return NULL; }

    return hm_input_from_fd(aTHX_ fd);
}

/* psgi.input: an in-memory read handle over the request body, via the
 * PerlIO :scalar layer - the C equivalent of open($fh,'<',\$body).
 *
 * WINDOWS TAKES THE FILE AT EVERY SIZE. PerlIO_openn is declared in perlio.h
 * but is not among the symbols perl5xx.dll exports, so the :scalar layer
 * cannot be reached from XS there at all - it is a link error, which is what
 * the first Windows build of this core hit. The temp file needs nothing but
 * PerlIO_fdopen, which IS exported, and it is the path every platform
 * already runs for a body over HM_SPILL_BODY rather than a second one
 * written for this. The trade is an inode and three syscalls on a Windows
 * request that carries a body. */
static SV *hm_new_input(pTHX_ const char *body, STRLEN len) {
#ifdef _WIN32
    SV *f = hm_new_input_file(aTHX_ body, len);
    return f ? f : &PL_sv_undef;
#else
    static int layer_loaded = 0;
    SV *bufsv;
    if (len > HM_SPILL_BODY) {
        SV *f = hm_new_input_file(aTHX_ body, len);
        if (f) return f;
    }
    bufsv = newSVpvn(body, len);
    SV *rv = sv_2mortal(newRV_noinc(bufsv));
    PerlIO *fp;
    GV *gv;
    IO *io;
    if (!layer_loaded) {
        load_module(PERL_LOADMOD_NOIMPORT, newSVpvs("PerlIO::scalar"), NULL);
        layer_loaded = 1;
    }
    fp = PerlIO_openn(aTHX_ ":scalar", "r", -1, 0, 0, NULL, 1, &rv);
    if (!fp) return &PL_sv_undef;
    gv = hm_anon_glob(aTHX);
    io = GvIO(gv);
    IoIFP(io) = fp;
    IoTYPE(io) = IoTYPE_RDONLY;
    return newRV_noinc((SV *)gv);
#endif
}

/* The shared empty psgi.input, as every bodyless request gets it: one glob
 * for the whole worker, because a fresh :scalar handle per request is a
 * glob, an IO and a layer for a body that is not there. One check keeps the
 * sharing honest: an application that closed it (or re-opened it over
 * something else) left a handle with no input stream, and the next request
 * would read a closed filehandle. A new one replaces it then. */
static SV *hm_empty_input_sv(pTHX) {
    if (hm_empty_input && SvROK(hm_empty_input)) {
        SV *gv = SvRV(hm_empty_input);
        IO *io = SvTYPE(gv) == SVt_PVGV ? GvIO((GV *)gv) : NULL;
        if (io && IoIFP(io)) return SvREFCNT_inc(hm_empty_input);
        SvREFCNT_dec(hm_empty_input);
        hm_empty_input = NULL;
    }
    hm_empty_input = hm_new_input(aTHX_ "", 0);
    return SvREFCNT_inc(hm_empty_input);
}

/* psgix.io: a dup of the client socket, so an app that hijacks it keeps
 * the socket alive after the server side closes. Absent on Windows:
 * duplicating a socket there is WSADuplicateSocket, not dup(2), and the
 * result would have to be re-wrapped for perl's IO layer - unproven, and
 * psgix.io is optional in PSGI, so it is simply not offered. */
static SV *hm_fh_for_fd(pTHX_ int fd) {
#ifdef _WIN32
    (void)fd;
    return &PL_sv_undef;
#else
    int nfd = dup(fd);
    PerlIO *fp;
    GV *gv;
    IO *io;
    if (nfd < 0) return &PL_sv_undef;
    fp = PerlIO_fdopen(nfd, "r+");
    if (!fp) { hm_os_close(nfd); return &PL_sv_undef; }
    gv = hm_anon_glob(aTHX);
    io = GvIO(gv);
    IoIFP(io) = fp;
    IoOFP(io) = fp;
    IoTYPE(io) = IoTYPE_RDWR;
    return newRV_noinc((SV *)gv);
#endif
}

/* Read out a filehandle / IO::Handle-like response body, honouring
 * ->getline/->close on objects (PSGI body protocol). Returns a PV SV. */
static SV *hm_slurp_body(pTHX_ SV *body) {
    SV *out = newSVpvs("");
    if (sv_isobject(body)
        && gv_fetchmethod_autoload(SvSTASH(SvRV(body)), "getline", 0)) {
        for (;;) {
            dSP;
            SV *chunk;
            int n;
            ENTER; SAVETMPS; PUSHMARK(SP);
            XPUSHs(body);
            PUTBACK;
            n = call_method("getline", G_SCALAR | G_EVAL);
            SPAGAIN;
            chunk = n ? POPs : NULL;
            if (SvTRUE(ERRSV) || !chunk || !SvOK(chunk)) {
                PUTBACK; FREETMPS; LEAVE;
                break;
            }
            sv_catsv(out, chunk);
            PUTBACK; FREETMPS; LEAVE;
        }
        if (gv_fetchmethod_autoload(SvSTASH(SvRV(body)), "close", 0)) {
            dSP;
            ENTER; SAVETMPS; PUSHMARK(SP);
            XPUSHs(body);
            PUTBACK;
            call_method("close", G_DISCARD | G_EVAL);
            FREETMPS; LEAVE;
        }
    } else if (SvROK(body) && (SvTYPE(SvRV(body)) == SVt_PVGV
                               || SvTYPE(SvRV(body)) == SVt_PVIO)) {
        /* sv_2io croaks on anything that is not a handle, and this is not
         * under an eval: a body of [200, [], {}] would unwind the loop */
        IO *io = sv_2io(body);
        if (io && IoIFP(io)) {
            char buf[8192];
            SSize_t got;
            while ((got = PerlIO_read(IoIFP(io), buf, sizeof(buf))) > 0)
                sv_catpvn(out, buf, got);
            PerlIO_close(IoIFP(io));
            IoIFP(io) = NULL;
            IoOFP(io) = NULL;
        }
    }
    return out;
}

/* ---- streaming body sources ---------------------------------------------- *
 * The lift: can this filehandle-ish body be streamed rather than slurped?
 *
 *   - a glob / IO ref over a regular file lifts to (fd, tell, remaining):
 *     remaining is the Content-Length header when the response carries one,
 *     else file-size minus position (which is then also what the auto
 *     Content-Length announces).
 *   - a blessed object lifts when it has BOTH getline and fileno over a
 *     regular file AND the response promises a Content-Length - the range
 *     contract Punk::SendFile::Reader satisfies; the raw fd position is
 *     taken as the read point, so hand over a handle that has not been
 *     buffer-read from. Without a usable fd, a getline object with a
 *     Content-Length still streams, one chunk per getline (kind 2).
 *
 * Anything else returns 0 and the caller slurps, exactly as before. */

static UV hm_bsrc_method_uv(pTHX_ SV *obj, const char *meth, int *ok) {
    dSP;
    UV out = 0;
    int n;
    *ok = 0;
    ENTER; SAVETMPS; PUSHMARK(SP);
    XPUSHs(obj);
    PUTBACK;
    n = call_method(meth, G_SCALAR | G_EVAL);
    SPAGAIN;
    if (n > 0) {
        SV *r = POPs;
        if (!SvTRUE(ERRSV) && SvOK(r) && SvIV(r) >= 0) {
            out = (UV)SvUV(r);
            *ok = 1;
        }
    }
    PUTBACK; FREETMPS; LEAVE;
    return out;
}

static int hm_bsrc_lift(pTHX_ SV *body, hm_bsrc *bs, int has_len, UV clen) {
    if (!SvROK(body)) return 0;

    if (sv_isobject(body)) {
        HV *stash = SvSTASH(SvRV(body));
        int fd = -1, ok = 0;
        if (!gv_fetchmethod_autoload(stash, "getline", 0)) return 0;
        if (!has_len) return 0;      /* no promised length, keep the slurp */
        if (gv_fetchmethod_autoload(stash, "fileno", 0)) {
            UV v = hm_bsrc_method_uv(aTHX_ body, "fileno", &ok);
            if (ok) fd = (int)v;
        }
        if (fd >= 0) {
            struct stat st;
            off_t pos;
            if (fstat(fd, &st) == 0 && S_ISREG(st.st_mode)
                && (pos = lseek(fd, 0, SEEK_CUR)) >= 0) {
                bs->kind = 1;
                bs->fd   = fd;
                bs->off  = pos;
                bs->remaining = clen;
                bs->sv = SvREFCNT_inc(body);
                return 1;
            }
        }
        bs->kind = 2;
        bs->remaining = clen;
        bs->sv = SvREFCNT_inc(body);
        return 1;
    }

    if (SvTYPE(SvRV(body)) == SVt_PVGV || SvTYPE(SvRV(body)) == SVt_PVIO) {
        IO *io = sv_2io(body);
        PerlIO *fp = io ? IoIFP(io) : NULL;
        struct stat st;
        int fd;
        off_t pos;
        if (!fp) return 0;
        fd = PerlIO_fileno(fp);
        if (fd < 0 || fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) return 0;
        pos = (off_t)PerlIO_tell(fp);
        if (pos < 0 || pos > st.st_size) return 0;
        bs->kind = 1;
        bs->fd   = fd;
        bs->off  = pos;
        bs->remaining = has_len ? clen : (UV)(st.st_size - pos);
        bs->sv = SvREFCNT_inc(body);
        return 1;
    }
    return 0;
}

/* Close out a source: the PSGI body contract says the server closes the
 * handle it was given (hm_slurp_body has always done so). Objects get
 * their close method; a glob's PerlIO is closed directly. */
static void hm_bsrc_release(pTHX_ hm_bsrc *bs) {
    if (!bs->kind) return;
    if (bs->sv) {
        if (sv_isobject(bs->sv)) {
            if (gv_fetchmethod_autoload(SvSTASH(SvRV(bs->sv)), "close", 0)) {
                dSP;
                ENTER; SAVETMPS; PUSHMARK(SP);
                XPUSHs(bs->sv);
                PUTBACK;
                call_method("close", G_DISCARD | G_EVAL);
                FREETMPS; LEAVE;
            }
        }
        else {
            IO *io = sv_2io(bs->sv);
            if (io && IoIFP(io)) {
                PerlIO_close(IoIFP(io));
                IoIFP(io) = NULL;
                IoOFP(io) = NULL;
            }
        }
        SvREFCNT_dec(bs->sv);
    }
    memset(bs, 0, sizeof(*bs));
}

/* Refill the write buffer with the next chunk of the source. Returns the
 * bytes added; 0 when the source is spent; -1 on a source that ended
 * early (file truncated under us, getline died or dried up) - the client
 * was promised more bytes than it will get, so the connection must not
 * be reused. */
static ssize_t hm_bsrc_fill(pTHX_ hm_conn *c) {
    hm_bsrc *bs = &c->bsrc;
    if (!bs->remaining) return 0;
    if (bs->kind == 1) {
        size_t want = bs->remaining < HM_BSRC_CHUNK
                          ? (size_t)bs->remaining : HM_BSRC_CHUNK;
        ssize_t n;
        hm_wb_reserve(c, want);
        n = hm_os_pread(bs->fd, c->wbuf + c->wlen, want, bs->off);
        if (n <= 0) { c->keepalive = 0; return -1; }
        c->wlen += (size_t)n;
        bs->off += n;
        bs->remaining -= (UV)n;
        return n;
    }
    for (;;) {                                   /* kind 2: one getline */
        dSP;
        SV *chunk;
        STRLEN l = 0;
        const char *p = NULL;
        int n;
        ENTER; SAVETMPS; PUSHMARK(SP);
        XPUSHs(bs->sv);
        PUTBACK;
        n = call_method("getline", G_SCALAR | G_EVAL);
        SPAGAIN;
        chunk = n > 0 ? POPs : NULL;
        if (SvTRUE(ERRSV) || !chunk || !SvOK(chunk)) {
            PUTBACK; FREETMPS; LEAVE;
            c->keepalive = 0;                    /* remaining > 0 here */
            return -1;
        }
        p = SvPV(chunk, l);
        if (l > bs->remaining) l = (STRLEN)bs->remaining;  /* hold the framing */
        if (l) hm_wb_put(c, p, l);
        bs->remaining -= (UV)l;
        PUTBACK; FREETMPS; LEAVE;
        if (l) return (ssize_t)l;
        if (!bs->remaining) return 0;
        /* an empty chunk with bytes still owed: ask again */
    }
}

/* Scan a header AV for Content-Length; fills *out and returns 1 if found. */
static int hm_hav_clen(pTHX_ AV *hav, UV *out) {
    SSize_t i, n;
    if (!hav) return 0;
    n = av_len(hav) + 1;
    for (i = 0; i + 1 < n; i += 2) {
        SV **k = av_fetch(hav, i, 0);
        STRLEN kl;
        const char *ks;
        if (!k || !*k) continue;
        ks = SvPV(*k, kl);
        if (kl == 14 && hm_strncasecmp(ks, "Content-Length", 14) == 0) {
            SV **v = av_fetch(hav, i + 1, 0);
            if (v && *v && SvOK(*v)) {
                /* Digits only. SvUV of "abc" is 0, which selected the
                 * counted mode with a length of nothing and cut the body
                 * at its first write; a value that is not a length is
                 * treated as absent, and the framing chosen without it. */
                STRLEN vl, j;
                const char *vs = SvPV(*v, vl);
                if (vl == 0 || vl > 19) return 0;
                for (j = 0; j < vl; j++)
                    if (vs[j] < '0' || vs[j] > '9') return 0;
                *out = (UV)SvUV(*v);
                return 1;
            }
        }
    }
    return 0;
}

/* a [500, [...], [$msg]] PSGI triple (mortal) */
static SV *hm_500_resp(pTHX_ const char *msg) {
    AV *resp = newAV();
    AV *hdrs = newAV();
    AV *body = newAV();
    av_push(hdrs, newSVpvs("Content-Type"));
    av_push(hdrs, newSVpvs("text/plain"));
    av_push(body, newSVpv(msg, 0));
    av_push(resp, newSViv(500));
    av_push(resp, newRV_noinc((SV *)hdrs));
    av_push(resp, newRV_noinc((SV *)body));
    return sv_2mortal(newRV_noinc((SV *)resp));
}

/* ---- connections --------------------------------------------------------- */

/* Return a connection struct to the pool, or free it when the pool is full.
 * The buffers ride along so the steady state allocates nothing - but only
 * at their ordinary sizes. A read buffer grown for a 15MB body, or a write
 * buffer grown for one slow client, would otherwise stay that size for as
 * long as the pool held the struct, and a thousand of them is the whole
 * ceiling resident for nothing. */
static void hm_pool_conn(hm_loop *loop, hm_conn *c) {
    if (c->rbuf && c->rcap > HM_RBUF)    { free(c->rbuf); c->rbuf = NULL; c->rcap = 0; }
    if (c->wbuf && c->wcap > HM_WB_KEEP) { free(c->wbuf); c->wbuf = NULL; c->wcap = 0; }
    if (loop->pool_n < HM_POOL_MAX) {
        c->pool_next = loop->pool;
        loop->pool = c;
        loop->pool_n++;
    } else {
        if (c->wbuf) free(c->wbuf);
        if (c->rbuf) free(c->rbuf);
        free(c);
    }
}

static void hm_lru_unlink(hm_loop *loop, hm_conn *c) {
    if (c->lru_prev) c->lru_prev->lru_next = c->lru_next;
    else if (loop->lru_head == c) loop->lru_head = c->lru_next;
    if (c->lru_next) c->lru_next->lru_prev = c->lru_prev;
    else if (loop->lru_tail == c) loop->lru_tail = c->lru_prev;
    c->lru_prev = c->lru_next = NULL;
}

static void hm_lru_touch(hm_loop *loop, hm_conn *c) {
    c->last_active = loop->now;
    if (loop->lru_tail == c) return;
    hm_lru_unlink(loop, c);
    c->lru_prev = loop->lru_tail;
    if (loop->lru_tail) loop->lru_tail->lru_next = c;
    loop->lru_tail = c;
    if (!loop->lru_head) loop->lru_head = c;
}

/* ---- completion-mode receive ---------------------------------------------
 *
 * A readiness backend says "this fd has bytes" and the core reads them. A
 * completion backend is asked for the read UP FRONT and hands back bytes that
 * are already in the buffer - which is how the read syscall stops being a
 * syscall per request and becomes part of one batched submission per turn of
 * the loop.
 *
 * What that costs is that the kernel holds a pointer into c->rbuf for a while,
 * and the two things the core does casually with that buffer - GROW it, and
 * POOL it for the next connection - both become memory corruption if they
 * happen while a read is outstanding. Growth is handled by only ever growing
 * before arming (hm_arm_recv). Pooling is handled here.
 *
 * The identity problem is the other half. A completion can outlive the
 * connection that asked for it: hm_close closes the fd, the number is reused
 * by a new connection moments later, and the old read lands looking exactly
 * like the new one's. So every read carries a token of fd plus the
 * connection's generation id, and a completion whose token does not match the
 * connection now holding that fd is somebody else's. */

#define HM_RECV_TOKEN(c) \
    (((uint64_t)(c)->id << 17) | (uint64_t)(unsigned)(c)->fd)

/* A read buffer whose connection is gone but whose kernel read is not. Parked
 * under the token the kernel will hand back, and freed when it does. */
typedef struct hm_quar {
    uint64_t        token;
    char           *buf;
    struct hm_quar *next;
} hm_quar;

static void hm_quar_push(hm_loop *loop, uint64_t token, char *buf) {
    hm_quar *q;
    if (!buf) return;
    q = (hm_quar *)malloc(sizeof(hm_quar));
    /* Out of memory with a read outstanding: the buffer CANNOT be freed, so
     * it is leaked deliberately. One leaked read buffer beats handing the
     * kernel a pointer into the heap it no longer owns. */
    if (!q) return;
    q->token = token;
    q->buf   = buf;
    q->next  = loop->quar;
    loop->quar = q;
}

static void hm_quar_release(hm_loop *loop, uint64_t token) {
    hm_quar **pp = &loop->quar, *q;
    while ((q = *pp)) {
        if (q->token == token) { *pp = q->next; free(q->buf); free(q); return; }
        pp = &q->next;
    }
}

static void hm_quar_free_all(hm_loop *loop) {
    hm_quar *q = loop->quar, *n;
    while (q) { n = q->next; free(q->buf); free(q); q = n; }
    loop->quar = NULL;
}

/* Ask for this connection's next read. 1 if one is now outstanding, 0 if the
 * caller must fall back to a readiness watcher - which is always correct, so
 * every refusal here is a performance decision and never a correctness one. */
static int hm_arm_recv(hm_loop *loop, hm_conn *c) {
    size_t want;
    if (!loop->completion || !loop->be->submit_recv) return 0;
    /* TLS reads belong to OpenSSL: the bytes on the socket are ciphertext and
     * the engine may already hold plaintext this would never see. */
    if (c->ssl || c->recv_inflight || c->detached || c->pp_wait) return 0;
    if (c->rlen == c->rcap) {
        /* Grow BEFORE the kernel is given the pointer. Never while a read is
         * outstanding, which is why this cannot live in the completion. */
        size_t ncap;
        if (c->rcap >= loop->max_read) return 0;   /* the readiness path owns
                                                    * the oversize/413 answer */
        ncap = c->rcap * 2;
        if (ncap > loop->max_read) ncap = loop->max_read;
        c->rbuf = (char *)hm_xrealloc(c->rbuf, ncap);
        c->rcap = ncap;
    }
    want = c->rcap - c->rlen;
    if (loop->be->submit_recv(loop->be, c->fd, c->rbuf + c->rlen, want,
                              HM_RECV_TOKEN(c)) != 0)
        return 0;
    c->recv_inflight = 1;
    return 1;
}

static int  hm_read_lend(hm_loop *loop, hm_conn *c);
static void hm_read_unlend(hm_loop *loop, hm_conn *c);

/* Requests are waiting in rbuf behind a park, a streamed body or a full write
 * buffer, and whoever serves them will memmove the buffer down. A completion
 * armed now holds rbuf + rlen, and after the move it lands past the new rlen
 * while rlen counts it from the front: the parser re-reads the tail of the
 * request it already served and waits for the rest of it until the idle
 * sweep. (t/27 on every Linux smoker with liburing: the request after a
 * pipelined one behind a sendfile body was never read.) */
static int hm_recv_unsafe(hm_conn *c) {
    return c->rlen > 0
        && (c->again_queued || c->bsrc.kind || c->awaiting
            || c->backlogged || c->stream_open);
}

/* Arm the next read however this connection reads: a completion if it can,
 * a readiness watcher otherwise. */
static void hm_rearm_read(hm_loop *loop, hm_conn *c) {
    /* A readiness read happens at rlen as it stands at the time, so it is
     * safe across the move; given back once the buffer has been served. */
    if (c->recv_lent) {
        if (hm_recv_unsafe(c)) return;
        hm_read_unlend(loop, c);
        c->recv_lent = 0;
    }
    /* Already reading off readiness: the watcher is persistent and
     * level-triggered, so re-adding it would be a syscall per response on
     * epoll for no effect at all. */
    if (c->recv_fallback) return;
    /* A read is already outstanding: the kernel holds rbuf + rlen and will
     * complete into it. A readiness watcher added now would read the same
     * bytes into the same offset first. hm_arm_recv refuses for the same
     * reason, but its refusal is not a request to fall back. */
    if (c->recv_inflight) return;
    if (loop->completion && hm_recv_unsafe(c)) {
        if (hm_read_lend(loop, c)) c->recv_lent = 1;
        return;
    }
    if (hm_arm_recv(loop, c)) return;
    c->recv_fallback = 1;
    loop->be->add_io(loop->be, c->fd, HM_EV_READ, 0);
}

/* Lend a completion-mode connection a readiness watcher for as long as an
 * application call that may wait on it is on the stack: an HTTP/2 handler
 * waiting for its body to drain is waiting for window updates on this very
 * socket. No completion is outstanding then - HM_EV_RECV re-arms only after
 * hm_process returns, and arming one under it would let the kernel write into
 * a buffer nghttp2 is consuming - so without the watcher the handler pumps a
 * loop that never reads its connection. 1 if lent. */
static int hm_read_lend(hm_loop *loop, hm_conn *c) {
    if (!loop->completion || c->recv_fallback || c->recv_inflight) return 0;
    c->recv_fallback = 1;
    loop->be->add_io(loop->be, c->fd, HM_EV_READ, 0);
    return 1;
}

/* And take it back: the caller's hm_rearm_read returns it to completion. */
static void hm_read_unlend(hm_loop *loop, hm_conn *c) {
    if (!c->read_paused) loop->be->remove_io(loop->be, c->fd, HM_EV_READ);
    c->read_paused = 0;
    c->recv_fallback = 0;
}

static void hm_close(pTHX_ hm_loop *loop, hm_conn *c) {
    /* A synchronous handler for this connection is on the stack: its ->get
     * pumped this loop and the loop found the peer gone, or the sweep found
     * it idle. The frame that called the app still holds c and will finish
     * with it (hm_process's consumed/memmove, hm_queue_response), so the
     * struct cannot be pooled - the next accept would hand it to a new
     * client and the first client's response would land on the second's
     * socket. Stop reading, mark it, and let that frame close it. */
    if (c->in_app) {
        if (!c->close_deferred) {
            c->close_deferred = 1;
            loop->be->remove_io(loop->be, c->fd,
                                HM_EV_READ | (c->writing ? HM_EV_WRITE : 0));
            c->writing = 0;
        }
        return;
    }
    /* Stream handles open on this connection die with it. They are told
     * first, while the connection is still consistent, and they are only
     * marked dead - the handle itself belongs to whoever opened it until
     * it calls stream_close. */
    if (hm_stream_live_n) hm_stream_conn_gone(aTHX_ c->fd, c->id);
    /* Before anything else touches the buffers: if the kernel is still
     * holding a pointer into this connection's read buffer, take the buffer
     * away from the connection so the pooling below cannot hand it to the
     * next one. The connection is pooled with a NULL rbuf and hm_new_conn
     * allocates a fresh one. */
    /* c is never NULL here - every caller passes a live connection, and this
     * function dereferences it unconditionally a few lines down. The `c &&`
     * these two guards used to carry was dead, and it told a static analyser
     * the opposite of the truth: cppcheck reported four null dereferences
     * from it, all of them the guard's fault rather than the code's. */
    if (c->recv_inflight) {
        if (loop->be->cancel_recv) loop->be->cancel_recv(loop->be, c->fd);
        hm_quar_push(loop, HM_RECV_TOKEN(c), c->rbuf);
        c->rbuf = NULL;
        c->rcap = 0;
        c->rlen = 0;
        c->recv_inflight = 0;
    }
    if (c->spill_fd >= 0) {          /* a body was still draining */
        PerlLIO_close(c->spill_fd);
        c->spill_fd = -1;
        c->spill_got = 0;
    }
    int fd = c->fd;
    SV *pending = c->resp_f;      /* cancelled below, once state is consistent */
    c->resp_f = NULL;
    loop->be->remove_io(loop->be, fd, HM_EV_READ | (c->writing ? HM_EV_WRITE : 0));
    if (c->bsrc.kind) hm_bsrc_release(aTHX_ &c->bsrc);   /* mid-stream abort */
    if (c->ssl) hm_tls_conn_free(c);
    hm_os_close(fd);
    hm_lru_unlink(loop, c);
    loop->conns[fd] = NULL;
    loop->nconns--;
    if (c->io_sv)  { SvREFCNT_dec(c->io_sv);  c->io_sv = NULL; }
    if (c->cid_sv) { SvREFCNT_dec(c->cid_sv); c->cid_sv = NULL; }
    if (c->env_sv) { SvREFCNT_dec(c->env_sv); c->env_sv = NULL; }
    if (c->peer_sv)      { SvREFCNT_dec(c->peer_sv);      c->peer_sv = NULL; }
    if (c->peer_host_sv) { SvREFCNT_dec(c->peer_host_sv); c->peer_host_sv = NULL; }
    if (c->peer_port_sv) { SvREFCNT_dec(c->peer_port_sv); c->peer_port_sv = NULL; }
    if (c->sname_sv)     { SvREFCNT_dec(c->sname_sv);     c->sname_sv = NULL; }
    if (c->sport_sv)     { SvREFCNT_dec(c->sport_sv);     c->sport_sv = NULL; }
    /* Detached from the connection BEFORE it is torn down: freeing the
     * session cancels every stream's parked Future, and a continuation
     * that fires on that cancel looks the stream up through c->h2. */
    if (c->h2)     { void *h2 = c->h2; c->h2 = NULL; hm_h2_free(aTHX_ h2); }
    hm_pool_conn(loop, c);
    if (loop->stopping && loop->nconns == 0) loop->stop = 1;
    /* client went away while awaiting: cancel the response Future so
     * in-flight async work can abort (propagates up the then-chain) */
    if (pending) {
        dSP;
        ENTER; SAVETMPS; PUSHMARK(SP);
        XPUSHs(pending);
        PUTBACK;
        call_method("cancel", G_DISCARD | G_EVAL);
        FREETMPS; LEAVE;
        SvREFCNT_dec(pending);
    }
}

/* ---- detach: hand a live HTTP/1 connection to the application ------------ *
 * The seam a protocol upgrade (WebSocket) needs: the socket stays open and
 * becomes the app's, while the server forgets it entirely so the app's own
 * io watchers on that fd fire (hm_dispatch routes to loop->io_r/io_w only
 * when conns[fd] is NULL).
 *
 * Two phases, because the callers of the app frame keep using the conn after
 * it returns (hm_process's consumed/memmove, hm_deliver's flush):
 *
 *   hm_detach        - immediate: validate, disarm the server's watchers so
 *                      no further bytes are pulled in, flag the conn.
 *   hm_conn_release  - deferred: hm_close minus the close(2) and minus the
 *                      watcher removal, run once the caller is done with the
 *                      conn. After it, conns[fd] is NULL and the fd is the
 *                      application's to own and to close.
 *
 * The app must not have produced any response bytes for this request, and
 * returns a sentinel (conventionally [101, [], []]) which is discarded. */
static int hm_detach(pTHX_ hm_loop *loop, int fd, UV id) {
    hm_conn *c;
#ifdef _WIN32
    /* The fd is a CRT wrapper around a SOCKET (hm_win.h). Handing that to
     * perl's IO layer to own, after this server stops watching it, has not
     * been proven on a real Windows box - so it is refused rather than
     * half-supported. A WebSocket tier over this server falls back to
     * psgi.streaming, which is its documented second transport. */
    (void)loop; (void)fd; (void)id; (void)c;
    return -6;
#else
    if (fd < 0 || fd >= HM_MAXFD) return -1;
    c = loop->conns[fd];
    if (!c || c->id != id)       return -1;   /* gone, or a stale ticket    */
    if (c->h2)                   return -2;   /* HTTP/2 streams share a conn */
    if (c->ssl)                  return -3;   /* TLS state cannot be handed on */
    if (c->wlen != c->woff
        || c->bsrc.kind)         return -4;   /* output still draining      */
    if (c->detached)             return -5;   /* already handed over        */
    loop->be->remove_io(loop->be, fd,
                        HM_EV_READ | (c->writing ? HM_EV_WRITE : 0));
    c->writing  = 0;
    c->detached = 1;
    return 0;
#endif
}

/* Deferred half of hm_detach: forget the connection without closing its fd. */
static void hm_conn_release(pTHX_ hm_loop *loop, hm_conn *c) {
    int fd = c->fd;
    SV *pending = c->resp_f;
    /* The fd survives, the connection object does not: a handle naming it
     * is as dead as it would be after a close. */
    if (hm_stream_live_n) hm_stream_conn_gone(aTHX_ c->fd, c->id);
    c->resp_f = NULL;
    if (c->bsrc.kind) hm_bsrc_release(aTHX_ &c->bsrc);
    hm_lru_unlink(loop, c);
    loop->conns[fd] = NULL;
    loop->nconns--;
    if (c->io_sv)  { SvREFCNT_dec(c->io_sv);  c->io_sv = NULL; }
    if (c->cid_sv) { SvREFCNT_dec(c->cid_sv); c->cid_sv = NULL; }
    if (c->env_sv) { SvREFCNT_dec(c->env_sv); c->env_sv = NULL; }
    if (c->peer_sv)      { SvREFCNT_dec(c->peer_sv);      c->peer_sv = NULL; }
    if (c->peer_host_sv) { SvREFCNT_dec(c->peer_host_sv); c->peer_host_sv = NULL; }
    if (c->peer_port_sv) { SvREFCNT_dec(c->peer_port_sv); c->peer_port_sv = NULL; }
    if (c->sname_sv)     { SvREFCNT_dec(c->sname_sv);     c->sname_sv = NULL; }
    if (c->sport_sv)     { SvREFCNT_dec(c->sport_sv);     c->sport_sv = NULL; }
    hm_pool_conn(loop, c);
    if (loop->stopping && loop->nconns == 0) loop->stop = 1;
    /* the request's own Future, if any, is simply dropped - the app owns
     * the socket now, so there is nothing left to cancel it for */
    if (pending) SvREFCNT_dec(pending);
}

static hm_conn *hm_new_conn(hm_loop *loop, int fd, hm_listener *lst) {
    hm_conn *c;
    if (loop->pool) {
        char  *wbuf, *rbuf; size_t wcap, rcap;
        c = loop->pool;
        loop->pool = c->pool_next;
        loop->pool_n--;
        wbuf = c->wbuf; wcap = c->wcap;      /* keep the buffers */
        rbuf = c->rbuf; rcap = c->rcap;
        memset(c, 0, sizeof(*c));
        c->wbuf = wbuf; c->wcap = wcap;
        c->rbuf = rbuf; c->rcap = rcap;
    } else {
        c = (hm_conn *)hm_xcalloc(1, sizeof(hm_conn));
    }
    if (!c->rbuf) {
        c->rbuf = (char *)hm_xmalloc(HM_RBUF);
        c->rcap = HM_RBUF;
    }
    c->fd = fd;
    c->keepalive = 1;
    c->spill_fd = -1;          /* 0 is a real fd; the zeroed struct is not
                                * "not spilling" without this */
    c->spill_got = 0;
    c->id = ++hm_id_counter;
    c->loop = loop;
    c->lst = lst;
    c->last_active = loop->now;
    loop->conns[fd] = c;
    loop->nconns++;
#ifndef HM_HAVE_ACCEPT4
    hm_set_nonblock(fd);               /* accept4 did it where there is one */
#endif
    if (!(lst && lst->is_unix)) {
        int one = 1;
        hm_os_setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
    }
    if (lst && lst->proxy_protocol) {
        /* The PROXY header comes first, on the raw socket, before TLS can
         * wrap it and before any request byte is read. Readiness reads for
         * life (recv_fallback), the header-phase timeout from now, and no
         * TLS until hm_proxy_readable has the client's address. */
        c->pp_wait = 1;
        c->recv_fallback = 1;
        c->req_start = loop->now;
        loop->be->add_io(loop->be, fd, HM_EV_READ, 0);
    }
    else if (lst && lst->tls_ctx) {
        /* TLS wraps first: the handshake is driven off readiness, and
         * hm_arm_recv would decline a TLS connection anyway. The handshake
         * is timed as a request header is (req_start): a peer that opens a
         * socket and never finishes its ClientHello was holding a slot for
         * idle_timeout. A wrap that fails (SSL_new under memory pressure)
         * closes rather than serving plaintext on a TLS listener. */
        c->req_start = loop->now;
        if (hm_tls_wrap(c, lst->tls_ctx) < 0) {
            hm_errlog("Hyperman: TLS setup failed for a new connection; closed\n");
            c->tls_failed = 1;           /* hm_accept closes it, with aTHX */
        } else {
            loop->be->add_io(loop->be, fd, HM_EV_READ, 0);
        }
    }
    else hm_rearm_read(loop, c);
    /* append to LRU tail */
    c->lru_prev = loop->lru_tail;
    if (loop->lru_tail) loop->lru_tail->lru_next = c;
    loop->lru_tail = c;
    if (!loop->lru_head) loop->lru_head = c;
    return c;
}

/* ---- HTTP parse ---------------------------------------------------------- */

/* Find a request header's value in the header block (case-insensitive name).
 * Returns a pointer to the (trimmed) value and sets *vlen, or NULL. */
static const char *hm_find_header(const char *head, size_t headlen,
                                  const char *name, size_t nlen, size_t *vlen) {
    const char *p = (const char *)memchr(head, '\n', headlen);   /* skip req line */
    const char *hend = head + headlen;
    if (!p) return NULL;
    p++;
    while (p < hend) {
        const char *eol = (const char *)memchr(p, '\r', hend - p);
        size_t len = eol ? (size_t)(eol - p) : (size_t)(hend - p);
        const char *colon = (const char *)memchr(p, ':', len);
        if (colon && (size_t)(colon - p) == nlen
            && hm_strncasecmp(p, name, nlen) == 0) {
            const char *v = colon + 1;
            size_t vl = (size_t)((p + len) - v);
            while (vl && (*v == ' ' || *v == '\t')) { v++; vl--; }
            while (vl && (v[vl-1] == ' ' || v[vl-1] == '\t')) vl--;
            *vlen = vl;
            return v;
        }
        p = eol ? eol + 2 : hend;
    }
    return NULL;
}

/* case-insensitive substring search */
static int hm_ci_contains(const char *hay, size_t hl, const char *needle) {
    size_t nl = strlen(needle), i;
    if (nl > hl) return 0;
    for (i = 0; i + nl <= hl; i++)
        if (hm_strncasecmp(hay + i, needle, nl) == 0) return 1;
    return 0;
}

/* ---- Expect: 100-continue (RFC 9110 10.1.1) -------------------------------
 * A client that expects to send a large body may ask permission first and
 * wait for an interim 100 before spending the upload. A server that never
 * answers does not break such a client - it stalls it, for however long the
 * client is prepared to wait (a second, in curl and in most of the Java
 * clients) on every request. So the answer is worth having for its own sake,
 * and the refusals are worth having more: a body over max_body is refused
 * here, on the headers, rather than after the client has sent it all.
 */
#define HM_EXPECT_NONE     0
#define HM_EXPECT_CONTINUE 1
#define HM_EXPECT_OTHER    2

/* The request line's HTTP version. Only "is it 1.1" is asked: 100-continue
 * is an HTTP/1.1 expectation and a 1.0 client's Expect MUST be ignored. */
static int hm_req_is_11(const char *head, size_t headlen) {
    const char *eol = (const char *)memchr(head, '\r', headlen);
    size_t ll = eol ? (size_t)(eol - head) : headlen;
    return ll >= 8 && memcmp(head + ll - 8, "HTTP/1.1", 8) == 0;
}

/* Classify the Expect field. The value is a list, and 100-continue is the
 * only member this server understands, so anything else - including
 * 100-continue with something else beside it - is an expectation it cannot
 * meet and earns a 417 rather than a silent promise it will not keep. */
static int hm_expectation(const char *head, size_t headlen) {
    size_t vl = 0;
    const char *v;
    if (!hm_req_is_11(head, headlen)) return HM_EXPECT_NONE;
    v = hm_find_header(head, headlen, "expect", 6, &vl);
    if (!v || !vl) return HM_EXPECT_NONE;
    if (vl == 12 && hm_strncasecmp(v, "100-continue", 12) == 0)
        return HM_EXPECT_CONTINUE;
    return HM_EXPECT_OTHER;
}

/* Send the interim response, once per request. Called only where the headers
 * have been accepted AND the body is not all here yet: a request already
 * carrying its whole body is answered with its final status instead, which
 * RFC 9110 allows and every client handles, and a request that is going to
 * be refused is refused before this is ever reached. */
static void hm_send_continue(hm_conn *c, int expect) {
    static const char cont[] = "HTTP/1.1 100 Continue\r\n\r\n";
    if (expect != HM_EXPECT_CONTINUE || c->cont_sent) return;
    c->cont_sent = 1;
    hm_wb_put(c, cont, sizeof(cont) - 1);
}

/* Percent-decode a path once (RFC 3875 PATH_INFO): %XX only, no '+'. */
static SV *hm_pct_decode(pTHX_ const char *p, STRLEN n) {
    SV *sv = newSV(n + 1);
    char *d = SvPVX(sv);
    STRLEN i = 0, o = 0;
    SvPOK_on(sv);
    while (i < n) {
        if (p[i] == '%' && i + 2 < n
            && isxdigit((unsigned char)p[i+1]) && isxdigit((unsigned char)p[i+2])) {
            int hi = p[i+1], lo = p[i+2];
            hi = (hi <= '9') ? hi - '0' : (hi | 32) - 'a' + 10;
            lo = (lo <= '9') ? lo - '0' : (lo | 32) - 'a' + 10;
            d[o++] = (char)((hi << 4) | lo);
            i += 3;
        } else {
            d[o++] = p[i++];
        }
    }
    d[o] = 0;
    SvCUR_set(sv, o);
    return sv;
}

/* ---- $env fast path ------------------------------------------------------
 * The fixed $env keys as shared-string SVs: the hash is computed once and
 * cached in the shared string table, so hv_store_ent skips both the hashing
 * and the key copy on every request. Values that never vary per process
 * ([1,1], the 0/1 flags, psgi.errors) are read-only singletons shared by
 * refcount; per-connection constants (REMOTE_*, SERVER_*) are cached on the
 * connection. Anything a middleware conventionally assigns in place
 * (psgi.url_scheme, SCRIPT_NAME, PATH_INFO) stays a fresh SV per request. */

enum {
    HMK_REQUEST_METHOD, HMK_REQUEST_URI, HMK_PATH_INFO, HMK_QUERY_STRING,
    HMK_SERVER_PROTOCOL, HMK_SCRIPT_NAME, HMK_SERVER_NAME, HMK_SERVER_PORT,
    HMK_REMOTE_ADDR, HMK_REMOTE_HOST, HMK_REMOTE_PORT,
    HMK_PSGI_VERSION, HMK_PSGI_URL_SCHEME, HMK_PSGI_MULTITHREAD,
    HMK_PSGI_MULTIPROCESS, HMK_PSGI_RUN_ONCE, HMK_PSGI_STREAMING,
    HMK_PSGI_NONBLOCKING, HMK_PSGI_ERRORS, HMK_PSGIX_LOOP, HMK_PSGIX_IO,
    HMK_PSGI_INPUT, HMK_PSGIX_INPUT_BUFFERED, HMK_CONTENT_LENGTH,
    HMK_CONTENT_TYPE, HMK_PSGIX_HM_CONN, HMK_PSGIX_HM_STREAM, HMK_PSGIX_PROXY_ADDR,
    HMK_COUNT
};
static const char *const hm_env_key_name[HMK_COUNT] = {
    "REQUEST_METHOD", "REQUEST_URI", "PATH_INFO", "QUERY_STRING",
    "SERVER_PROTOCOL", "SCRIPT_NAME", "SERVER_NAME", "SERVER_PORT",
    "REMOTE_ADDR", "REMOTE_HOST", "REMOTE_PORT",
    "psgi.version", "psgi.url_scheme", "psgi.multithread",
    "psgi.multiprocess", "psgi.run_once", "psgi.streaming",
    "psgi.nonblocking", "psgi.errors", "psgix.loop", "psgix.io",
    "psgi.input", "psgix.input.buffered", "CONTENT_LENGTH",
    "CONTENT_TYPE", "psgix.hyperman.conn", "psgix.hyperman.stream",
    "psgix.proxy_addr"
};
static SV *hm_env_key[HMK_COUNT];
static SV *hm_env_zero, *hm_env_one;   /* read-only 0/1 flag values      */
static SV *hm_env_version;             /* read-only [1,1]                */
static SV *hm_env_errors;              /* read-only \*STDERR             */

/* The HTTP_* keys for the headers real traffic actually sends, as shared
 * key SVs made once. Storing with a plain char* key makes hv_common hash
 * the string and cut a fresh HEK against the shared string table on every
 * request, then release it again at env teardown; storing through a shared
 * SV reuses the precomputed hash and the one HEK for the process's life.
 * A browser request carries a dozen of these, so it pays per header, per
 * request. Anything not in this table just takes the char* path it always
 * took. Grouped by key length so the lookup is a couple of memcmps. */
static const char *const hm_hdrk_name[] = {
    "HTTP_TE",                                                     /*  7 */
    "HTTP_DNT",                                                    /*  8 */
    "HTTP_HOST",                                                   /*  9 */
    "HTTP_RANGE",                                                  /* 10 */
    "HTTP_ACCEPT", "HTTP_COOKIE", "HTTP_ORIGIN", "HTTP_PRAGMA",    /* 11 */
    "HTTP_REFERER", "HTTP_UPGRADE",                                /* 12 */
    "HTTP_USER_AGENT", "HTTP_CONNECTION",                          /* 15 */
    "HTTP_AUTHORIZATION", "HTTP_CACHE_CONTROL",
    "HTTP_IF_NONE_MATCH",                                          /* 18 */
    "HTTP_ACCEPT_ENCODING", "HTTP_ACCEPT_LANGUAGE",
    "HTTP_X_FORWARDED_FOR",                                        /* 20 */
    "HTTP_X_REQUESTED_WITH",                                       /* 21 */
    "HTTP_IF_MODIFIED_SINCE", "HTTP_X_FORWARDED_PROTO",
    "HTTP_SEC_WEBSOCKET_KEY",                                      /* 22 */
    "HTTP_SEC_WEBSOCKET_VERSION",                                  /* 26 */
    NULL
};
#define HM_HDRK_MAXLEN 26
#define HM_HDRK_PERLEN 5
static SV *hm_hdrk_sv[sizeof(hm_hdrk_name) / sizeof(char *)];
/* index+1 of each candidate, per key length; 0 ends the row */
static unsigned char hm_hdrk_by_len[HM_HDRK_MAXLEN + 1][HM_HDRK_PERLEN];

/* the cached key SV for keybuf/klen, or NULL for the char* fallback */
static SV *hm_hdrk_lookup(const char *keybuf, size_t klen) {
    const unsigned char *row;
    int i;
    if (klen > HM_HDRK_MAXLEN) return NULL;
    row = hm_hdrk_by_len[klen];
    for (i = 0; i < HM_HDRK_PERLEN && row[i]; i++) {
        int idx = row[i] - 1;
        if (memcmp(keybuf, hm_hdrk_name[idx], klen) == 0)
            return hm_hdrk_sv[idx];
    }
    return NULL;
}

/* The two bytes a repeated request header folds with. PSGI joins with the
 * comma of a list header (RFC 9110 5.3). Cookie is the one request header
 * whose list is "; " (RFC 6265 4.2.1), and an HTTP/2 client may send each
 * cookie as its own field (RFC 9113 8.2.3), which browsers do: folded with
 * a comma the session cookie's value grows a tail and its signature fails
 * on every request. The same fold serves HTTP/1.1, where a client that
 * splits Cookie is out of spec but the app is still owed one value. */
static const char *hm_hdr_join_sep(const char *keybuf, size_t klen) {
    return (klen == 11 && memcmp(keybuf, "HTTP_COOKIE", 11) == 0) ? "; " : ", ";
}

static void hm_env_init(pTHX) {
    int i;
    AV *ver;
    if (hm_env_key[0]) return;
    for (i = 0; i < HMK_COUNT; i++)
        hm_env_key[i] = newSVpvn_share(hm_env_key_name[i],
                                       strlen(hm_env_key_name[i]), 0);
    hm_env_zero = newSViv(0); SvREADONLY_on(hm_env_zero);
    hm_env_one  = newSViv(1); SvREADONLY_on(hm_env_one);
    ver = newAV();
    av_push(ver, newSViv(1)); av_push(ver, newSViv(1));
    SvREADONLY_on((SV *)ver);
    hm_env_version = newRV_noinc((SV *)ver); SvREADONLY_on(hm_env_version);
    hm_env_errors  = newRV_inc((SV *)PL_stderrgv); SvREADONLY_on(hm_env_errors);

    for (i = 0; hm_hdrk_name[i]; i++) {
        size_t l = strlen(hm_hdrk_name[i]);
        int j;
        hm_hdrk_sv[i] = newSVpvn_share(hm_hdrk_name[i], (I32)l, 0);
        for (j = 0; j < HM_HDRK_PERLEN; j++) {
            if (!hm_hdrk_by_len[l][j]) {
                hm_hdrk_by_len[l][j] = (unsigned char)(i + 1);
                break;
            }
        }
    }
}

#define hm_env_store(env, k, val) \
    (void)hv_store_ent((env), hm_env_key[(k)], (val), 0)

/* Build a full PSGI env from the request line, the header lines indexed by
 * idx (from hm_parse_index) and, if present, the request body. Sets
 * c->keepalive from the request. */
static HV *hm_build_env(pTHX_ char *head, size_t headlen,
                        const char *body, long clen, hm_conn *c,
                        const hm_hline *idx, int nlines) {
    HV *env = newHV();
    hm_env_init(aTHX);

    /* A fresh HV starts with 8 buckets and this function stores 25-odd keys
     * before the app adds its own, so left alone every request pays two
     * bucket-split reallocs (8 -> 16 -> 32) plus the rehash walk each one
     * does. Ask for the final size up front. 64 rather than 32: a typical
     * browser request carries a dozen HTTP_* headers on top of ours, and
     * middleware adds more. */
    hv_ksplit(env, 64);

    /* request line */
    char *line_end = (char *)memchr(head, '\r', headlen);
    size_t ll = line_end ? (size_t)(line_end - head) : headlen;
    char *sp1 = (char *)memchr(head, ' ', ll);
    const char *method = head; STRLEN mlen = ll;
    const char *uri = "/"; STRLEN ulen = 1;
    const char *proto = "HTTP/1.0"; STRLEN plen = 8;
    if (sp1) {
        mlen = (STRLEN)(sp1 - head);
        char *u = sp1 + 1;
        char *sp2 = (char *)memchr(u, ' ', ll - (u - head));
        if (sp2) {
            uri = u; ulen = (STRLEN)(sp2 - u);
            proto = sp2 + 1; plen = (STRLEN)((head + ll) - (sp2 + 1));
        } else { uri = u; ulen = (STRLEN)((head + ll) - u); }
    }
    const char *qmark = (const char *)memchr(uri, '?', ulen);
    const char *path = uri; STRLEN path_l = ulen;
    const char *qs = ""; STRLEN qs_l = 0;
    if (qmark) { path_l = (STRLEN)(qmark - uri); qs = qmark + 1; qs_l = ulen - path_l - 1; }

    hm_env_store(env, HMK_REQUEST_METHOD,  newSVpvn(method, mlen));
    hm_env_store(env, HMK_REQUEST_URI,     newSVpvn(uri, ulen));
    hm_env_store(env, HMK_PATH_INFO,       hm_pct_decode(aTHX_ path, path_l));
    hm_env_store(env, HMK_QUERY_STRING,    newSVpvn(qs, qs_l));
    hm_env_store(env, HMK_SERVER_PROTOCOL, newSVpvn(proto, plen));
    hm_env_store(env, HMK_SCRIPT_NAME,     newSVpvs(""));
    c->is_head = (mlen == 4 && memcmp(method, "HEAD", 4) == 0);
    if (!c->sname_sv) {
        c->sname_sv = newSVpv(c->lst && c->lst->host ? c->lst->host : "0.0.0.0", 0);
        c->sport_sv = newSViv(c->lst ? c->lst->port : 0);
    }
    /* Copies, not the cached SVs themselves. `$env->{REMOTE_ADDR} = $ip`
     * - which is what a reverse-proxy middleware does - assigns INTO the
     * value SV, and a shared one would then carry the forwarded address
     * into every later request on this keep-alive connection, before any
     * middleware had run on them. newSVsv shares the buffer copy-on-write
     * on perls that can, so the copy costs an SV head and no string. */
    hm_env_store(env, HMK_SERVER_NAME, newSVsv(c->sname_sv));
    hm_env_store(env, HMK_SERVER_PORT, newSVsv(c->sport_sv));
    if (c->peer[0]) {
        if (!c->peer_sv) {
            c->peer_sv      = newSVpv(c->peer, 0);
            c->peer_host_sv = newSVpv(c->peer, 0);
        }
        hm_env_store(env, HMK_REMOTE_ADDR, newSVsv(c->peer_sv));
        hm_env_store(env, HMK_REMOTE_HOST, newSVsv(c->peer_host_sv));
    }
    if (c->peer_port) {
        if (!c->peer_port_sv) c->peer_port_sv = newSViv(c->peer_port);
        hm_env_store(env, HMK_REMOTE_PORT, newSVsv(c->peer_port_sv));
    }
    /* Per-request copies, not the shared read-only SVs: an assignment into
     * the slot writes INTO the SV there, and Plack::Middleware::BufferedStreaming
     * does `$env->{'psgi.streaming'} = 1`, which died "Modification of a
     * read-only value" and made every request a 500 under it. The version
     * copy is a fresh RV onto the one read-only array. */
    hm_env_store(env, HMK_PSGI_VERSION,      newSVsv(hm_env_version));
    hm_env_store(env, HMK_PSGI_URL_SCHEME,
                 c->ssl ? newSVpvs("https") : newSVpvs("http"));
    hm_env_store(env, HMK_PSGI_MULTITHREAD,  newSViv(0));
    hm_env_store(env, HMK_PSGI_MULTIPROCESS, newSViv(1));
    hm_env_store(env, HMK_PSGI_RUN_ONCE,     newSViv(0));
    hm_env_store(env, HMK_PSGI_STREAMING,    newSViv(1));
    hm_env_store(env, HMK_PSGI_NONBLOCKING,  newSViv(1));
    /* A copy: `$env->{'psgi.errors'} = $fh` is an assignment INTO the slot's
     * SV, and the shared one is read-only, so an application redirecting
     * its errors died with "Modification of a read-only value". */
    hm_env_store(env, HMK_PSGI_ERRORS,       newSVsv(hm_env_errors));
    if (c->loop->log_wants_t0)                /* %D / %T in the log format */
        (void)hv_stores(env, "psgix.request_start", newSVnv((NV)hm_now_ns() / 1e9));

    /* psgix.loop: this worker's loop, so apps/clients can create loop
     * Futures; psgix.io: the client socket, dup'd so an app that
     * hijacks it keeps the socket alive after we close our fd. */
    hm_env_store(env, HMK_PSGIX_LOOP, hm_loop_to_sv(aTHX_ c->loop));
    if (!c->io_sv) c->io_sv = hm_fh_for_fd(aTHX_ c->fd);
    hm_env_store(env, HMK_PSGIX_IO, SvREFCNT_inc(c->io_sv));
    /* psgix.hyperman.conn: [fd, generation id] - the ticket an app hands
     * back to Hyperman::detach (or the ABI's conn_detach) to take this
     * socket over for a protocol upgrade. HTTP/1 only; the generation id
     * makes a stale ticket a no-op rather than a wrong-connection hit. */
    if (!c->cid_sv) {
        AV *cid = newAV();
        av_extend(cid, 2);
        av_store(cid, 0, newSViv(c->fd));
        av_store(cid, 1, newSVuv(c->id));
        /* The third element is the stream id, and -1 is "the whole
         * connection is the stream" - which is what HTTP/1.1 is. It rides on
         * the same cached arrayref rather than a second one, so
         * psgix.hyperman.stream costs a store and not an allocation. */
        av_store(cid, 2, newSViv(-1));
        c->cid_sv = newRV_noinc((SV *)cid);
    }
    hm_env_store(env, HMK_PSGIX_HM_CONN,   SvREFCNT_inc(c->cid_sv));
    hm_env_store(env, HMK_PSGIX_HM_STREAM, SvREFCNT_inc(c->cid_sv));
    if (c->proxy_addr[0])                   /* the address that connected */
        hm_env_store(env, HMK_PSGIX_PROXY_ADDR, newSVpv(c->proxy_addr, 0));
    if (c->ssl) hm_tls_env(aTHX_ c, env);

    c->keepalive = (plen == 8 && memcmp(proto, "HTTP/1.1", 8) == 0);
    c->req_11    = c->keepalive;   /* before Connection: close can clear it */
    /* Per request, not per connection: a keep-alive connection may send a
     * different Accept-Encoding on its next request, and the response path
     * has no env to read by the time it needs this. One bit, set below. */
    c->accepts_gzip = 0;

    /* header lines, from the framing pass's index (no rescan) */
    char keybuf[300];
    int have_cl = 0;
    int li;
    for (li = 0; li < nlines; li++) {
        const char *p = head + idx[li].off;
        size_t nk = idx[li].klen;
        const char *vp = head + idx[li].voff;
        size_t nv = idx[li].vlen;

        if (nk + 5 >= sizeof(keybuf)) continue;   /* absurd name: not a header
                                                     we would have stored before
                                                     either */
        /* Fold to the canonical HTTP_ form first, then every name test below
         * is a memcmp on known-case bytes rather than a strncasecmp on the
         * wire bytes - three case-blind scans per header gone. The fold maps
         * '-' to '_', which would let a literal "Content_Length" alias the
         * real thing, so the special names also check the wire byte at the
         * hyphen position, exactly as the old raw-name compare required. */
        {
            size_t i;
            int underscore = 0;
            memcpy(keybuf, "HTTP_", 5);
            for (i = 0; i < nk; i++) {
                unsigned char ch = (unsigned char)p[i];
                if (ch >= 'a' && ch <= 'z') ch -= 32;
                else if (ch == '-') ch = '_';
                else if (ch == '_') underscore = 1;
                keybuf[5 + i] = (char)ch;
            }
            /* A wire name with an underscore folds to the same env key as
             * the hyphenated name, so "X_Forwarded_For" would land in
             * HTTP_X_FORWARDED_FOR beside - or instead of - the header a
             * proxy in front vetted. Proxies strip the hyphenated form and
             * pass the underscored one untouched, which is the whole
             * attack. Dropped, as nginx and Apache do. */
            if (underscore) continue;
        }
        /* Accept-Encoding is answered on this one pass and remembered as a
         * bit on the connection: hm_queue_response has no env to consult by
         * the time it must decide. Not an arm of the chain below - the
         * header is still stored as HTTP_ACCEPT_ENCODING like any other. */
        if (nk == 15 && p[6] == '-' && c->loop->compress
            && memEQ(keybuf + 5, "ACCEPT_ENCODING", 15))
            c->accepts_gzip = (unsigned char)hz_accepts_gzip(vp, nv);

        if (nk == 14 && p[7] == '-' && memEQ(keybuf + 5, "CONTENT_LENGTH", 14)) {
            hm_env_store(env, HMK_CONTENT_LENGTH, newSVpvn(vp, nv));
            have_cl = 1;
        } else if (nk == 12 && p[7] == '-'
                   && memEQ(keybuf + 5, "CONTENT_TYPE", 12)) {
            hm_env_store(env, HMK_CONTENT_TYPE, newSVpvn(vp, nv));
        } else if (nk == 17 && p[8] == '-'
                   && memEQ(keybuf + 5, "TRANSFER_ENCODING", 17)) {
            /* consumed by framing (already dechunked); not exposed to the
             * app, whose psgi.input is the decoded body with CONTENT_LENGTH */
        } else {
            {
                /* repeated header: join values (hm_hdr_join_sep) */
                SV *ksv = hm_hdrk_lookup(keybuf, nk + 5);
                if (ksv) {
                    /* a common header: the shared key SV carries its hash
                     * and its HEK, so no hashing and no HEK churn */
                    HE *he = hv_fetch_ent(env, ksv, 0, 0);
                    if (he) {
                        sv_catpvn(HeVAL(he), hm_hdr_join_sep(keybuf, nk + 5), 2);
                        sv_catpvn(HeVAL(he), vp, nv);
                    } else {
                        (void)hv_store_ent(env, ksv, newSVpvn(vp, nv), 0);
                    }
                } else {
                    SV **old = hv_fetch(env, keybuf, (I32)(nk + 5), 0);
                    if (old) {
                        sv_catpvn(*old, hm_hdr_join_sep(keybuf, nk + 5), 2);
                        sv_catpvn(*old, vp, nv);
                    } else {
                        hv_store(env, keybuf, (I32)(nk + 5), newSVpvn(vp, nv), 0);
                    }
                }
            }
            /* the name is already canonical; only the value keeps wire case */
            if (nk == 10 && memEQ(keybuf + 5, "CONNECTION", 10)) {
                if (nv >= 5 && hm_strncasecmp(vp, "close", 5) == 0) c->keepalive = 0;
                else if (nv >= 10 && hm_strncasecmp(vp, "keep-alive", 10) == 0) c->keepalive = 1;
            }
        }
    }
    /* dechunked request: no Content-Length header, but we know the decoded
     * length - present it so apps/middleware see the real body size */
    if (!have_cl && clen > 0)
        hm_env_store(env, HMK_CONTENT_LENGTH, newSViv((IV)clen));

    if (c && c->spill_fd >= 0) {
        /* The body was drained to a file as it arrived, so there is no body
         * pointer to copy from: the descriptor IS psgi.input. The handle owns
         * it from here, and the connection stops tracking it. */
        SV *in = hm_input_from_fd(aTHX_ c->spill_fd);
        c->spill_fd = -1;
        c->spill_got = 0;
        hm_env_store(env, HMK_PSGI_INPUT,
                     in ? in : hm_empty_input_sv(aTHX));
    }
    else if (clen > 0)
        hm_env_store(env, HMK_PSGI_INPUT, hm_new_input(aTHX_ body, (STRLEN)clen));
    else
        hm_env_store(env, HMK_PSGI_INPUT, hm_empty_input_sv(aTHX));
    hm_env_store(env, HMK_PSGIX_INPUT_BUFFERED, SvREFCNT_inc(hm_env_one));

    return env;
}

/* ---- app invocation ------------------------------------------------------ */

/* Call the app with an env we keep alive (access_log needs it afterwards). */
static SV *hm_call_app(pTHX_ hm_loop *loop, SV *env_rv) {
    dSP;
    SV *resp = NULL;
    int count;
    ENTER; SAVETMPS;
    PUSHMARK(SP);
    XPUSHs(sv_2mortal(SvREFCNT_inc(env_rv)));
    PUTBACK;
    count = call_sv(loop->app, G_SCALAR | G_EVAL);
    SPAGAIN;
    if (!SvTRUE(ERRSV) && count > 0) {
        resp = POPs;
        SvREFCNT_inc(resp);
    } else if (count > 0) {
        (void)POPs;
    }
    hm_warn_died(aTHX_ "application");
    PUTBACK;
    FREETMPS; LEAVE;
    return resp;
}

/* No-logger fast path: the env is handed to the call and freed with it. */
static SV *hm_call_app0(pTHX_ hm_loop *loop, HV *env) {
    dSP;
    SV *resp = NULL;
    int count;
    ENTER; SAVETMPS;
    PUSHMARK(SP);
    XPUSHs(sv_2mortal(newRV_noinc((SV *)env)));
    PUTBACK;
    count = call_sv(loop->app, G_SCALAR | G_EVAL);
    SPAGAIN;
    if (!SvTRUE(ERRSV) && count > 0) {
        resp = POPs;
        SvREFCNT_inc(resp);
    } else if (count > 0) {
        (void)POPs;
    }
    hm_warn_died(aTHX_ "application");
    PUTBACK;
    FREETMPS; LEAVE;
    return resp;
}

/* Is any access logging configured for this loop? Guards the (small) cost of
 * keeping the env alive past the response purely for the logger. */
#define hm_logging(loop) ((loop)->log_cb || (loop)->log_fd >= 0)

/* ---- fast default access-log writer (Combined Log Format) ----------------
 * When access_log is a path/handle rather than a coderef, log lines are
 * formatted and buffered in C and flushed once per loop wakeup - no Perl
 * call on the hot path. The fd is shared (inherited) across workers and
 * opened O_APPEND, so whole-line writes stay atomic between them. */

#define HM_LOG_FLUSH_AT (32 * 1024)   /* flush the buffer once it grows past  */

static void hm_log_flush(hm_loop *loop) {
    size_t off = 0;
    if (loop->log_fd < 0 || loop->log_len == 0) return;
    while (off < loop->log_len) {
        ssize_t n = hm_os_write_file(loop->log_fd, loop->log_buf + off,
                                     loop->log_len - off);
        if (n > 0) { off += (size_t)n; continue; }
        if (n < 0 && errno == EINTR) continue;
        break;   /* EAGAIN/error: keep the unwritten tail for the next flush */
    }
    if (off && off < loop->log_len)
        memmove(loop->log_buf, loop->log_buf + off, loop->log_len - off);
    loop->log_len -= off;
}

static void hm_log_raw(hm_loop *loop, const char *p, size_t n) {
    if (loop->log_len + n > loop->log_cap) {
        size_t ncap = loop->log_cap ? loop->log_cap : 8192;
        while (ncap < loop->log_len + n) ncap *= 2;
        loop->log_buf = (char *)hm_xrealloc(loop->log_buf, ncap);
        loop->log_cap = ncap;
    }
    memcpy(loop->log_buf + loop->log_len, p, n);
    loop->log_len += n;
}

/* A plain token: '-' when empty (CLF uses '-' for missing fields). */
static void hm_log_tok(hm_loop *loop, const char *p, size_t n) {
    if (!p || n == 0) { hm_log_raw(loop, "-", 1); return; }
    hm_log_raw(loop, p, n);
}

/* A value going inside "quotes": escape ", \ and control bytes \xHH so a
 * hostile URI or User-Agent cannot forge or split a log line. */
static void hm_log_quoted(hm_loop *loop, const char *p, size_t n) {
    static const char hex[] = "0123456789abcdef";
    size_t i, run = 0;
    if (!p) return;
    for (i = 0; i < n; i++) {
        unsigned char ch = (unsigned char)p[i];
        if (ch == '"' || ch == '\\' || ch < 0x20 || ch == 0x7f) {
            char e[4];
            if (run) { hm_log_raw(loop, p + i - run, run); run = 0; }
            if (ch == '"' || ch == '\\') {
                e[0] = '\\'; e[1] = (char)ch; hm_log_raw(loop, e, 2);
            } else {
                e[0] = '\\'; e[1] = 'x'; e[2] = hex[ch >> 4]; e[3] = hex[ch & 0xf];
                hm_log_raw(loop, e, 4);
            }
        } else {
            run++;
        }
    }
    if (run) hm_log_raw(loop, p + n - run, run);
}

/* The Date header (RFC 9110 6.6.1, IMF-fixdate), recomputed at most once a
 * second. Formatted by hand: strftime's %a and %b follow the locale, and
 * the wire wants English. 37 bytes, always: "Date: " + 29 + CRLF. */
#define HM_DATE_LEN 37
static const char *hm_date_header(hm_loop *loop) {
    if (loop->now != loop->date_sec) {
        static const char days[] = "SunMonTueWedThuFriSat";
        static const char mons[] = "JanFebMarAprMayJunJulAugSepOctNovDec";
        struct tm tm;
        time_t t = loop->now;
        char *d = loop->date_hdr;
        if (!hm_gmtime_r(&t, &tm)) return NULL;
        memcpy(d, "Date: ", 6); d += 6;
        memcpy(d, days + tm.tm_wday * 3, 3); d += 3;
        *d++ = ','; *d++ = ' ';
        *d++ = (char)('0' + tm.tm_mday / 10); *d++ = (char)('0' + tm.tm_mday % 10);
        *d++ = ' ';
        memcpy(d, mons + tm.tm_mon * 3, 3); d += 3;
        *d++ = ' ';
        {
            int y = tm.tm_year + 1900;
            *d++ = (char)('0' + y / 1000 % 10); *d++ = (char)('0' + y / 100 % 10);
            *d++ = (char)('0' + y / 10 % 10);   *d++ = (char)('0' + y % 10);
        }
        *d++ = ' ';
        *d++ = (char)('0' + tm.tm_hour / 10); *d++ = (char)('0' + tm.tm_hour % 10);
        *d++ = ':';
        *d++ = (char)('0' + tm.tm_min / 10);  *d++ = (char)('0' + tm.tm_min % 10);
        *d++ = ':';
        *d++ = (char)('0' + tm.tm_sec / 10);  *d++ = (char)('0' + tm.tm_sec % 10);
        memcpy(d, " GMT\r\n", 6);
        loop->date_sec = loop->now;
    }
    return loop->date_hdr;
}

/* Does an app-supplied header name or value carry a byte that would end
 * the field early? A CR, LF or NUL in either splits the response, so a
 * response carrying one is refused as a 500 rather than serialised: the
 * server is the last place the bug can be caught. */
static int hm_hdr_bytes_ok(const char *p, STRLEN n) {
    return !memchr(p, '\r', n) && !memchr(p, '\n', n) && !memchr(p, '\0', n);
}
static int hm_headers_ok(pTHX_ AV *hav) {
    SSize_t i, hn;
    if (!hav) return 1;
    hn = av_len(hav) + 1;
    for (i = 0; i < hn; i++) {
        SV **e = av_fetch(hav, i, 0);
        STRLEN l;
        const char *p;
        if (!e || !*e) continue;
        p = SvPV(*e, l);
        if (!hm_hdr_bytes_ok(p, l)) return 0;
    }
    return 1;
}

/* The same escape, for a value inside a JSON string. */
/* The length of a well-formed UTF-8 sequence starting at p (2..4), or 0 for
 * a byte that does not start one. Overlongs, surrogates and > U+10FFFF
 * are 0 too: the point is that what passes through is valid JSON text. */
static size_t hm_utf8_seq(const unsigned char *p, size_t n) {
    unsigned char c = p[0];
    size_t len, i;
    if (c >= 0xc2 && c <= 0xdf) len = 2;
    else if (c >= 0xe0 && c <= 0xef) len = 3;
    else if (c >= 0xf0 && c <= 0xf4) len = 4;
    else return 0;
    if (n < len) return 0;
    for (i = 1; i < len; i++) if ((p[i] & 0xc0) != 0x80) return 0;
    if (len == 3) {
        if (c == 0xe0 && p[1] < 0xa0) return 0;             /* overlong  */
        if (c == 0xed && p[1] >= 0xa0) return 0;            /* surrogate */
    } else if (len == 4) {
        if (c == 0xf0 && p[1] < 0x90) return 0;             /* overlong  */
        if (c == 0xf4 && p[1] >= 0x90) return 0;            /* > 10FFFF  */
    }
    return len;
}

static void hm_log_json_esc(hm_loop *loop, const char *p, size_t n) {
    static const char hex[] = "0123456789abcdef";
    size_t i, run = 0;
    if (!p) return;
    for (i = 0; i < n; i++) {
        unsigned char ch = (unsigned char)p[i];
        if (ch >= 0x80) {
            /* Valid UTF-8 passes; a stray high byte (a URI or User-Agent
             * that is not UTF-8) is escaped, so the line stays valid JSON
             * and a strict pipeline does not drop it - which was a way to
             * keep one's requests out of the log. */
            size_t sl = hm_utf8_seq((const unsigned char *)p + i, n - i);
            if (sl) { run += sl; i += sl - 1; continue; }
        }
        if (ch == '"' || ch == '\\' || ch < 0x20 || ch >= 0x80) {
            char e[6];
            if (run) { hm_log_raw(loop, p + i - run, run); run = 0; }
            if (ch == '"' || ch == '\\') {
                e[0] = '\\'; e[1] = (char)ch; hm_log_raw(loop, e, 2);
            } else {
                e[0] = '\\'; e[1] = 'u'; e[2] = '0'; e[3] = '0';
                e[4] = hex[ch >> 4]; e[5] = hex[ch & 0xf];
                hm_log_raw(loop, e, 6);
            }
        } else {
            run++;
        }
    }
    if (run) hm_log_raw(loop, p + n - run, run);
}

/* A value from $env, escaped the way this log's format wants. */
static void hm_log_esc(hm_loop *loop, const char *p, size_t n) {
    if (loop->log_json) hm_log_json_esc(loop, p, n);
    else                hm_log_quoted(loop, p, n);
}

/* Cached apache-style timestamp, recomputed at most once per second. */
static const char *hm_log_time(hm_loop *loop) {
    if (loop->now != loop->log_ts_sec) {
        struct tm tm;
        time_t t = loop->now;
        hm_localtime_r(&t, &tm);
        strftime(loop->log_ts, sizeof(loop->log_ts), "%d/%b/%Y:%H:%M:%S %z", &tm);
        loop->log_ts_sec = loop->now;
    }
    return loop->log_ts;
}

/* The ISO 8601 stamp the JSON format carries, cached the same way. */
static const char *hm_log_time_iso(hm_loop *loop) {
    if (loop->now != loop->log_iso_sec) {
        struct tm tm;
        time_t t = loop->now;
        hm_localtime_r(&t, &tm);
        strftime(loop->log_iso, sizeof(loop->log_iso), "%Y-%m-%dT%H:%M:%S%z", &tm);
        loop->log_iso_sec = loop->now;
    }
    return loop->log_iso;
}

/* ---- the access-log format compiler ----------------------------------------
 * Apache's directives, the ones a PSGI server can honour:
 *   %h %a REMOTE_ADDR   %l "-"           %u REMOTE_USER    %t [time]
 *   %r request line     %s %>s status    %b bytes or "-"   %B bytes or 0
 *   %D microseconds     %T seconds       %p SERVER_PORT    %m METHOD
 *   %U PATH_INFO        %q ?query        %H SERVER_PROTOCOL %v SERVER_NAME
 *   %P pid              %{Name}i request header   %{Name}e any $env key
 *   %% a literal percent sign
 * Returns 0, or -1 with the offending directive in err. Every value is
 * escaped (CLF or JSON), so no directive is a way to forge a line. */
#define HM_LOG_COMBINED "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\""

static void hm_logtok_push(hm_logtok **toks, int *n, int *cap, hm_logtok t) {
    if (*n == *cap) {
        *cap = *cap ? *cap * 2 : 16;
        *toks = (hm_logtok *)hm_xrealloc(*toks, (size_t)*cap * sizeof(hm_logtok));
    }
    (*toks)[(*n)++] = t;
}
static void hm_logtok_lit(hm_logtok **toks, int *n, int *cap, const char *p, size_t l) {
    hm_logtok t;
    if (!l) return;
    if (*n && (*toks)[*n - 1].kind == HM_LT_LIT) {   /* extend the last run */
        hm_logtok *last = &(*toks)[*n - 1];
        last->lit = (char *)hm_xrealloc(last->lit, last->litlen + l);
        memcpy(last->lit + last->litlen, p, l);
        last->litlen += l;
        return;
    }
    t.kind = HM_LT_LIT; t.lit = (char *)hm_xmalloc(l); memcpy(t.lit, p, l);
    t.litlen = l; t.key = NULL;
    hm_logtok_push(toks, n, cap, t);
}
static void hm_logtok_env(pTHX_ hm_logtok **toks, int *n, int *cap,
                          const char *key, size_t klen) {
    hm_logtok t;
    t.kind = HM_LT_ENV; t.lit = NULL; t.litlen = 0;
    t.key  = newSVpvn_share(key, (I32)klen, 0);    /* hash computed once */
    hm_logtok_push(toks, n, cap, t);
}
static void hm_logtok_kind(hm_logtok **toks, int *n, int *cap, int kind) {
    hm_logtok t;
    t.kind = kind; t.lit = NULL; t.litlen = 0; t.key = NULL;
    hm_logtok_push(toks, n, cap, t);
}

static int hm_logfmt_compile(pTHX_ const char *fmt, hm_logtok **out, int *nout,
                             int *wants_t0, char *err, size_t errlen) {
    hm_logtok *toks = NULL;
    int n = 0, cap = 0;
    const char *p = fmt, *lit = fmt;
    *wants_t0 = 0;
    while (*p) {
        char d;
        if (*p != '%') { p++; continue; }
        hm_logtok_lit(&toks, &n, &cap, lit, (size_t)(p - lit));
        p++;
        if (*p == '>') p++;                        /* %>s is %s */
        if (*p == '{') {
            const char *name = p + 1, *end = strchr(name, '}');
            char key[300];
            size_t nl, i;
            if (!end || (end[1] != 'i' && end[1] != 'e')) {
                snprintf(err, errlen, "%%{...}%c", end ? end[1] : '?');
                goto bad;
            }
            nl = (size_t)(end - name);
            if (nl == 0 || nl + 6 > sizeof(key)) { snprintf(err, errlen, "%%{%.*s}", (int)nl, name); goto bad; }
            if (end[1] == 'i') {
                memcpy(key, "HTTP_", 5);
                for (i = 0; i < nl; i++) {
                    unsigned char ch = (unsigned char)name[i];
                    if (ch >= 'a' && ch <= 'z') ch -= 32;
                    else if (ch == '-') ch = '_';
                    key[5 + i] = (char)ch;
                }
                hm_logtok_env(aTHX_ &toks, &n, &cap, key, nl + 5);
            } else {
                hm_logtok_env(aTHX_ &toks, &n, &cap, name, nl);
            }
            p = end + 2;
            lit = p;
            continue;
        }
        d = *p;
        switch (d) {
        case '%': hm_logtok_lit(&toks, &n, &cap, "%", 1); break;
        case 'h': case 'a': hm_logtok_env(aTHX_ &toks, &n, &cap, "REMOTE_ADDR", 11); break;
        case 'l': hm_logtok_kind(&toks, &n, &cap, HM_LT_DASH); break;
        case 'u': hm_logtok_env(aTHX_ &toks, &n, &cap, "REMOTE_USER", 11); break;
        case 't': hm_logtok_kind(&toks, &n, &cap, HM_LT_TIME); break;
        case 'r': hm_logtok_kind(&toks, &n, &cap, HM_LT_REQLINE); break;
        case 's': hm_logtok_kind(&toks, &n, &cap, HM_LT_STATUS); break;
        case 'b': hm_logtok_kind(&toks, &n, &cap, HM_LT_BYTES); break;
        case 'B': hm_logtok_kind(&toks, &n, &cap, HM_LT_BYTES0); break;
        case 'D': hm_logtok_kind(&toks, &n, &cap, HM_LT_DUR_US); *wants_t0 = 1; break;
        case 'T': hm_logtok_kind(&toks, &n, &cap, HM_LT_DUR_S);  *wants_t0 = 1; break;
        case 'p': hm_logtok_env(aTHX_ &toks, &n, &cap, "SERVER_PORT", 11); break;
        case 'm': hm_logtok_env(aTHX_ &toks, &n, &cap, "REQUEST_METHOD", 14); break;
        case 'U': hm_logtok_env(aTHX_ &toks, &n, &cap, "PATH_INFO", 9); break;
        case 'q': hm_logtok_kind(&toks, &n, &cap, HM_LT_QUERY); break;
        case 'H': hm_logtok_env(aTHX_ &toks, &n, &cap, "SERVER_PROTOCOL", 15); break;
        case 'v': hm_logtok_env(aTHX_ &toks, &n, &cap, "SERVER_NAME", 11); break;
        case 'P': hm_logtok_kind(&toks, &n, &cap, HM_LT_PID); break;
        default:
            snprintf(err, errlen, "%%%c", d ? d : '?');
            goto bad;
        }
        p++;
        lit = p;
    }
    hm_logtok_lit(&toks, &n, &cap, lit, (size_t)(p - lit));
    *out = toks; *nout = n;
    return 0;
bad:
    {
        int i;
        for (i = 0; i < n; i++) {
            if (toks[i].lit) free(toks[i].lit);
            if (toks[i].key) SvREFCNT_dec(toks[i].key);
        }
        if (toks) free(toks);
    }
    return -1;
}

/* The JSON shape: one object per line, fixed keys, numbers unquoted. */
static void hm_logfmt_json(pTHX_ hm_logtok **out, int *nout) {
    hm_logtok *toks = NULL;
    int n = 0, cap = 0;
#define HM_JL(s) hm_logtok_lit(&toks, &n, &cap, s, sizeof(s) - 1)
#define HM_JE(k) hm_logtok_env(aTHX_ &toks, &n, &cap, k, sizeof(k) - 1)
    HM_JL("{\"time\":\"");         hm_logtok_kind(&toks, &n, &cap, HM_LT_TIME_ISO);
    HM_JL("\",\"remote_addr\":\""); HM_JE("REMOTE_ADDR");
    HM_JL("\",\"user\":\"");       HM_JE("REMOTE_USER");
    HM_JL("\",\"method\":\"");     HM_JE("REQUEST_METHOD");
    HM_JL("\",\"uri\":\"");        HM_JE("REQUEST_URI");
    HM_JL("\",\"protocol\":\"");   HM_JE("SERVER_PROTOCOL");
    HM_JL("\",\"status\":");       hm_logtok_kind(&toks, &n, &cap, HM_LT_STATUS);
    HM_JL(",\"bytes\":");          hm_logtok_kind(&toks, &n, &cap, HM_LT_BYTES);
    HM_JL(",\"referer\":\"");      HM_JE("HTTP_REFERER");
    HM_JL("\",\"user_agent\":\""); HM_JE("HTTP_USER_AGENT");
    HM_JL("\",\"duration_us\":");  hm_logtok_kind(&toks, &n, &cap, HM_LT_DUR_US);
    HM_JL("}");
#undef HM_JL
#undef HM_JE
    *out = toks; *nout = n;
}

static const char *hm_env_str(pTHX_ HV *env, const char *key, I32 klen, STRLEN *lenp) {
    SV **s = hv_fetch(env, key, klen, 0);
    if (s && *s && SvOK(*s)) return SvPV(*s, *lenp);
    *lenp = 0;
    return NULL;
}

/* One $env value into the line, escaped; the "absent" spelling differs
 * between the two shapes ("-" in CLF, an empty string in JSON). */
static void hm_log_env_val(pTHX_ hm_loop *loop, HV *env, const hm_logtok *t) {
    HE *he = hv_fetch_ent(env, t->key, 0, 0);
    SV *v = he ? HeVAL(he) : NULL;
    if (v && SvOK(v)) {
        STRLEN n;
        const char *s = SvPV(v, n);
        if (n) { hm_log_esc(loop, s, n); return; }
    }
    if (!loop->log_json) hm_log_raw(loop, "-", 1);
}

/* Build one access-log line into the loop's log buffer by walking the
 * compiled format. The default compiles to Combined:
 *   host - user [time] "METHOD URI PROTO" status bytes "referer" "ua" */
static void hm_log_line(pTHX_ hm_loop *loop, SV *env_rv, int status, ssize_t blen) {
    HV *env;
    char num[32];
    int ln, i;

    if (!(env_rv && SvROK(env_rv) && SvTYPE(SvRV(env_rv)) == SVt_PVHV)) return;
    env = (HV *)SvRV(env_rv);

    for (i = 0; i < loop->nlogtok; i++) {
        const hm_logtok *t = &loop->logtok[i];
        switch (t->kind) {
        case HM_LT_LIT:  hm_log_raw(loop, t->lit, t->litlen); break;
        case HM_LT_ENV:  hm_log_env_val(aTHX_ loop, env, t); break;
        case HM_LT_DASH: hm_log_raw(loop, "-", 1); break;
        case HM_LT_STATUS:
            ln = (int)hm_itoa(num, (long)status); hm_log_raw(loop, num, (size_t)ln);
            break;
        case HM_LT_BYTES:
            if (blen >= 0) { ln = (int)hm_utoa(num, (unsigned long)blen);
                             hm_log_raw(loop, num, (size_t)ln); }
            else if (loop->log_json) hm_log_raw(loop, "null", 4);
            else                     hm_log_raw(loop, "-", 1);
            break;
        case HM_LT_BYTES0:
            ln = (int)hm_utoa(num, blen >= 0 ? (unsigned long)blen : 0UL);
            hm_log_raw(loop, num, (size_t)ln);
            break;
        case HM_LT_TIME: {
            const char *ts = hm_log_time(loop);
            hm_log_raw(loop, "[", 1); hm_log_raw(loop, ts, strlen(ts)); hm_log_raw(loop, "]", 1);
            break;
        }
        case HM_LT_TIME_ISO: {
            const char *ts = hm_log_time_iso(loop);
            hm_log_raw(loop, ts, strlen(ts));
            break;
        }
        case HM_LT_REQLINE: {
            const char *s; STRLEN n;
            s = hm_env_str(aTHX_ env, "REQUEST_METHOD", 14, &n);
            if (s && n) hm_log_esc(loop, s, n); else hm_log_raw(loop, "-", 1);
            hm_log_raw(loop, " ", 1);
            s = hm_env_str(aTHX_ env, "REQUEST_URI", 11, &n);
            if (s && n) hm_log_esc(loop, s, n); else hm_log_raw(loop, "-", 1);
            hm_log_raw(loop, " ", 1);
            s = hm_env_str(aTHX_ env, "SERVER_PROTOCOL", 15, &n);
            if (s && n) hm_log_esc(loop, s, n); else hm_log_raw(loop, "-", 1);
            break;
        }
        case HM_LT_QUERY: {
            const char *s; STRLEN n;
            s = hm_env_str(aTHX_ env, "QUERY_STRING", 12, &n);
            if (s && n) { hm_log_raw(loop, "?", 1); hm_log_esc(loop, s, n); }
            break;
        }
        case HM_LT_PID:
            ln = (int)hm_itoa(num, (long)hm_os_getpid()); hm_log_raw(loop, num, (size_t)ln);
            break;
        case HM_LT_DUR_US:
        case HM_LT_DUR_S: {
            SV **t0 = hv_fetch(env, "psgix.request_start", 19, 0);
            if (t0 && *t0 && SvOK(*t0)) {
                double el = (double)hm_now_ns() / 1e9 - SvNV(*t0);
                unsigned long v = el < 0 ? 0UL
                    : (unsigned long)(t->kind == HM_LT_DUR_US ? el * 1e6 : el);
                ln = (int)hm_utoa(num, v); hm_log_raw(loop, num, (size_t)ln);
            }
            else if (loop->log_json) hm_log_raw(loop, "null", 4);
            else                     hm_log_raw(loop, "-", 1);
            break;
        }
        }
    }
    hm_log_raw(loop, "\n", 1);

    if (loop->log_len >= HM_LOG_FLUSH_AT) hm_log_flush(loop);
}

/* Record one response. Fast path: format in C to the access-log fd. Else, if
 * a coderef was given, call $cb->($env, $status, $bytes); errors in the logger
 * are swallowed so they cannot take the loop down. */
static void hm_access_log(pTHX_ hm_loop *loop, SV *env_rv, int status, ssize_t blen) {
    dSP;
    if (loop->log_fd >= 0) { hm_log_line(aTHX_ loop, env_rv, status, blen); return; }
    if (!loop->log_cb) return;
    ENTER; SAVETMPS; PUSHMARK(SP);
    XPUSHs(env_rv);
    XPUSHs(sv_2mortal(newSViv(status)));
    XPUSHs(blen >= 0 ? sv_2mortal(newSViv((IV)blen)) : &PL_sv_undef);
    PUTBACK;
    call_sv(loop->log_cb, G_DISCARD | G_EVAL);
    FREETMPS; LEAVE;
}

/* ---- response serialization ---------------------------------------------- */

/* Reusable per-connection write buffer: keep capacity across requests so the
 * steady-state response path does no malloc/free. */
static void hm_wb_reserve(hm_conn *c, size_t need) {
    if (c->wlen + need > c->wcap) {
        size_t ncap = c->wcap ? c->wcap : 4096;
        while (ncap < c->wlen + need) ncap *= 2;
        c->wbuf = (char *)hm_xrealloc(c->wbuf, ncap);
        c->wcap = ncap;
    }
}
static void hm_wb_put(hm_conn *c, const char *p, size_t n) {
    hm_wb_reserve(c, n);
    memcpy(c->wbuf + c->wlen, p, n);
    c->wlen += n;
}

static const char HM_ERR500[] =
    "HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain\r\n"
    "Content-Length: 21\r\nConnection: close\r\n\r\nInternal Server Error";

/* Serialize $resp: headers into c->wbuf, then try an immediate writev of
 * header + body chunks (zero-copy body). Whatever the kernel doesn't take
 * is buffered; a write watcher resumes it (backpressure). Falls back to
 * copy-into-buffer when bytes are already queued (pipelining) or the body
 * is a filehandle. Returns 0, or -1 if the connection was closed. */
static int hm_queue_response(pTHX_ hm_conn *c, SV *resp) {
    hm_loop *loop = c->loop;
    AV *rav, *hav = NULL;
    SV **s0, **s1, **s2;
    SV *fh_body = NULL;
    AV *bav = NULL;
    int status = 500, has_len = 0, has_conn = 0, has_date = 0;
    int gz_out = 0;
    size_t blen = 0;
    size_t hdr_start = c->wlen;
    SSize_t bn = 0;
    char line[64];
    int ln;

    if (loop->stopping) c->keepalive = 0;

    if (!(resp && SvROK(resp) && SvTYPE(SvRV(resp)) == SVt_PVAV)) {
        hm_wb_put(c, HM_ERR500, sizeof(HM_ERR500) - 1);
        c->keepalive = 0;
        c->last_status = 500;
        c->last_blen = 21;
        return 0;
    }
    rav = (AV *)SvRV(resp);
    s0 = av_fetch(rav, 0, 0);
    s1 = av_fetch(rav, 1, 0);
    s2 = av_fetch(rav, 2, 0);
    if (s0 && *s0) status = (int)SvIV(*s0);
    if (s1 && *s1 && SvROK(*s1) && SvTYPE(SvRV(*s1)) == SVt_PVAV) hav = (AV *)SvRV(*s1);
    if (!hm_headers_ok(aTHX_ hav)) {
        hm_errlog("Hyperman: response header carries CR, LF or NUL; refused\n");
        hm_wb_put(c, HM_ERR500, sizeof(HM_ERR500) - 1);
        c->keepalive = 0;
        c->last_status = 500;
        c->last_blen = 21;
        return 0;
    }
    if (s2 && !*s2) s2 = NULL;

    /* body length (first pass, no copy) */
    if (s2 && SvROK(*s2) && SvTYPE(SvRV(*s2)) == SVt_PVAV) {
        SSize_t i;
        bav = (AV *)SvRV(*s2);
        bn = av_len(bav) + 1;
        for (i = 0; i < bn; i++) {
            SV **e = av_fetch(bav, i, 0);
            if (e) { STRLEN l; (void)SvPV(*e, l); blen += l; }
        }
    } else if (s2 && SvROK(*s2)) {
        /* filehandle or IO::Handle-like body: stream it when it lifts to
         * a byte-countable source, else slurp exactly as before */
        UV clv = 0;
        int cl_seen = hm_hav_clen(aTHX_ hav, &clv);
        if (!c->bsrc.kind && hm_bsrc_lift(aTHX_ *s2, &c->bsrc, cl_seen, clv))
            blen = (size_t)c->bsrc.remaining;
        else {
            fh_body = hm_slurp_body(aTHX_ *s2);
            { STRLEN l; (void)SvPV(fh_body, l); blen = l; }
        }
    }

    /* ---- compression decision, before a byte of the response is written --
     * Cheapest test first, and the whole block is skipped on a server that
     * did not ask for it. The rules, in order:
     *   1. enabled, and this request accepted gzip
     *   2. the response carries NO Content-Encoding. This one rule covers
     *      every hands-off case at once - a framework's precompressed file
     *      already says `gzip`, an opted-out route says `identity` (which
     *      we strip below), and anything else was encoded deliberately.
     *   3. a plain in-memory body, not a glob/reader/streamed source:
     *      compressing those would undo the 0.20 sendfile and drip paths.
     *   4. big enough to be worth a gzip member's header and trailer
     *   5. a status that carries an entity, and never 206 (a range
     *      describes offsets into the UNCOMPRESSED representation)
     *   6. a compressible media type, from an allowlist                   */
    if (loop->compress && c->accepts_gzip && bav && !c->bsrc.kind && !fh_body
        && blen >= loop->compress_min
        && (status == 200 || status == 203)) {
        const char *ct = NULL;
        STRLEN ctl = 0;
        int enc_seen = 0;
        SSize_t i, hn = hav ? av_len(hav) + 1 : 0;
        for (i = 0; i + 1 < hn; i += 2) {
            SV **k = av_fetch(hav, i, 0);
            STRLEN kl;
            const char *ks;
            if (!k || !*k) continue;
            ks = SvPV(*k, kl);
            /* A response that sets a cookie is one a BREACH-style attacker
             * would want compressed beside a reflected input; left alone,
             * as the no-transform contract for a Set-Cookie response. */
            if (kl == 10 && (ks[0] | 32) == 's'
                && hm_strncasecmp(ks, "Set-Cookie", 10) == 0) {
                enc_seen = 1;
                break;
            }
            if ((ks[0] | 32) != 'c') continue;
            if (kl == 16 && hm_strncasecmp(ks, "Content-Encoding", 16) == 0) {
                enc_seen = 1;
                break;
            }
            if (kl == 12 && hm_strncasecmp(ks, "Content-Type", 12) == 0) {
                SV **v = av_fetch(hav, i + 1, 0);
                if (v && *v) ct = SvPV(*v, ctl);
            }
        }
        if (!enc_seen && hz_type_ok(ct, ctl)) {
            /* one contiguous copy of the body, then one deflate */
            char *flat, *gz = NULL;
            size_t gzl = 0, off = 0;
            SSize_t j;
            Newx(flat, blen ? blen : 1, char);
            for (j = 0; j < bn; j++) {
                SV **e = av_fetch(bav, j, 0);
                if (e) {
                    STRLEN l;
                    const char *p = SvPV(*e, l);
                    /* Bounded by the length the first pass measured: an
                     * overloaded or tied element whose stringification grew
                     * between the two calls would otherwise run past flat. */
                    if (l > blen - off) l = blen - off;
                    if (l) { memcpy(flat + off, p, l); off += l; }
                }
            }
            if (hz_gzip(flat, off, loop->compress_level, &gz, &gzl)) {
                /* route it down the fh_body path: one SV, one copy into the
                 * write buffer. A response that just paid a deflate is not
                 * the one to spend the writev fast path on. */
                fh_body = newSVpvn(gz, gzl);
                Safefree(gz);
                bav = NULL; bn = 0;
                blen = gzl;
                c->last_blen = blen;
                gz_out = 1;
            }
            Safefree(flat);
        }
    }

    /* status line + headers */
    c->last_status = status;
    c->last_blen = blen;
    {
        size_t sll;
        const char *sl = hm_status_line(status, &sll, line);
        hm_wb_put(c, sl, sll);
    }
    if (hav) {
        SSize_t i, hn = av_len(hav) + 1;
        for (i = 0; i + 1 < hn; i += 2) {
            SV **k = av_fetch(hav, i, 0);
            SV **v = av_fetch(hav, i + 1, 0);
            STRLEN kl, vl;
            const char *ks, *vs;
            if (!k || !*k || !v || !*v) continue;   /* a hole in the array */
            ks = SvPV(*k, kl);
            vs = SvPV(*v, vl);
            if (kl == 4 && (ks[0] | 32) == 'd' && hm_strncasecmp(ks, "Date", 4) == 0)
                has_date = 1;
            /* both interesting names start with 'c', so one folded-byte test
             * skips the case-blind scans for every other header */
            if ((ks[0] | 32) == 'c') {
                if (kl == 14 && hm_strncasecmp(ks, "Content-Length", 14) == 0) {
                    has_len = 1;
                    /* the app's length describes the bytes we just replaced */
                    if (gz_out) { has_len = 0; continue; }
                }
                if (kl == 10 && hm_strncasecmp(ks, "Connection", 10) == 0)     has_conn = 1;
                /* `identity` is the documented way for anything above to opt
                 * a response out (see hm_compress.h). It has done its job by
                 * now; sending it on would be noise. */
                if (kl == 16 && vl == 8 && loop->compress
                    && hm_strncasecmp(ks, "Content-Encoding", 16) == 0
                    && hm_strncasecmp(vs, "identity", 8) == 0)
                    continue;
            }
            /* The layer that compressed must be the layer that fixes the
             * validator, or a cache serves these bytes to a client that
             * asked for none. nginx does the same; it is in the POD because
             * an app author will otherwise find it by debugging. */
            if (gz_out && kl == 4 && hm_strncasecmp(ks, "ETag", 4) == 0) {
                hm_wb_put(c, ks, kl);
                hm_wb_put(c, ": ", 2);
                if (vl > 1 && vs[vl - 1] == '"') {
                    hm_wb_put(c, vs, vl - 1);
                    hm_wb_put(c, "-gzip\"", 6);
                } else {
                    hm_wb_put(c, vs, vl);
                    hm_wb_put(c, "-gzip", 5);
                }
                hm_wb_put(c, "\r\n", 2);
                continue;
            }
            hm_wb_put(c, ks, kl);
            hm_wb_put(c, ": ", 2);
            hm_wb_put(c, vs, vl);
            hm_wb_put(c, "\r\n", 2);
        }
    }
    if (gz_out) {
        hm_wb_put(c, "Content-Encoding: gzip\r\n", 24);
        hm_wb_put(c, "Vary: Accept-Encoding\r\n", 23);
    }
    /* Alt-Svc: how a client finds out this origin also speaks HTTP/3. It is
     * advertised on the HTTP/1.1 and HTTP/2 responses and never on an h3 one,
     * where it would be telling a client what it is already using. The value
     * is precomputed per listener in hm_worker_listeners - never built from a
     * client-supplied Host, which would let a request choose the header. */
    if (c->lst && c->lst->altsvc) {
        hm_wb_put(c, "Alt-Svc: ", 9);
        hm_wb_put(c, c->lst->altsvc, strlen(c->lst->altsvc));
        hm_wb_put(c, "\r\n", 2);
    }
    /* 1xx/204/304 carry no entity: no Content-Length, no body (RFC 7230) */
    {
        int no_entity = (status < 200 || status == 204 || status == 304);
        if (no_entity) {
            if (fh_body) { SvREFCNT_dec(fh_body); fh_body = NULL; }
            if (c->bsrc.kind) hm_bsrc_release(aTHX_ &c->bsrc);
            bav = NULL; bn = 0; blen = 0;
        } else if (!has_len) {
            memcpy(line, "Content-Length: ", 16);
            ln = 16 + (int)hm_utoa(line + 16, (unsigned long)blen);
            line[ln++] = '\r'; line[ln++] = '\n';
            hm_wb_put(c, line, (size_t)ln);
        }
        /* HEAD: the headers of the GET, including its Content-Length, and
         * not one body byte - which the app, like most apps, produced
         * anyway. Sending it would be read as the start of the next
         * response on this connection. */
        if (!no_entity && c->is_head) {
            if (fh_body) { SvREFCNT_dec(fh_body); fh_body = NULL; }
            if (c->bsrc.kind) hm_bsrc_release(aTHX_ &c->bsrc);
            bav = NULL; bn = 0; blen = 0;
            c->last_blen = 0;
        }
    }
    if (!has_date) {
        const char *dh = hm_date_header(loop);
        if (dh) hm_wb_put(c, dh, HM_DATE_LEN);
    }
    if (!has_conn)
        hm_wb_put(c, c->keepalive ? "Connection: keep-alive\r\n" : "Connection: close\r\n",
                  c->keepalive ? 24 : 19);
    hm_wb_put(c, "\r\n", 2);

    loop->bytes_out += (c->wlen - hdr_start) + blen;

    /* fast path: nothing queued before this response -> writev header+body
     * straight from the response SVs, no body copy */
    if (hdr_start == 0 && c->woff == 0 && !c->writing && !fh_body
        && (!c->ssl || hm_ktls_tx(c))
        && !c->bsrc.kind && bav && bn >= 0 && bn < HM_IOV_MAX) {
        hm_iovec iov[HM_IOV_MAX];
        size_t total = c->wlen;
        int niov = 1;
        SSize_t i;
        ssize_t n;
        HM_IOV_SET(iov, 0, c->wbuf, c->wlen);
        for (i = 0; i < bn; i++) {
            SV **e = av_fetch(bav, i, 0);
            if (e) {
                STRLEN l; const char *p = SvPV(*e, l);
                if (l) {
                    HM_IOV_SET(iov, niov, p, l);
                    niov++;
                    total += l;
                }
            }
        }
        n = hm_os_writev(c->fd, iov, niov);
        if (n < 0) {
            if (errno != EAGAIN && errno != EWOULDBLOCK) { hm_close(aTHX_ loop, c); return -1; }
            n = 0;
        }
        if ((size_t)n >= total) { c->wlen = 0; return 0; }   /* all written */
        /* partial: keep the unwritten remainder in wbuf */
        {
            size_t skip = (size_t)n;
            size_t hdrlen = c->wlen;
            if (skip < hdrlen) {
                memmove(c->wbuf, c->wbuf + skip, hdrlen - skip);
                c->wlen = hdrlen - skip;
                skip = 0;
            } else {
                skip -= hdrlen;
                c->wlen = 0;
            }
            for (i = 0; i < bn; i++) {
                SV **e = av_fetch(bav, i, 0);
                if (e) {
                    STRLEN l; const char *p = SvPV(*e, l);
                    if (skip >= l) { skip -= l; continue; }
                    hm_wb_put(c, p + skip, l - skip);
                    skip = 0;
                }
            }
        }
        loop->be->add_io(loop->be, c->fd, HM_EV_WRITE, 0);
        c->writing = 1;
        return 0;
    }

    /* slow path: copy the body into the write buffer */
    if (fh_body) {
        STRLEN l; const char *p = SvPV(fh_body, l);
        hm_wb_put(c, p, l);
        SvREFCNT_dec(fh_body);
    } else if (bav) {
        SSize_t i;
        for (i = 0; i < bn; i++) {
            SV **e = av_fetch(bav, i, 0);
            if (e) { STRLEN l; const char *p = SvPV(*e, l); hm_wb_put(c, p, l); }
        }
    }
    return 0;
}

/* Flush c->wbuf; on EAGAIN arm a write watcher. Returns 0 to keep the
 * connection, -1 if it was closed. A streaming body source (c->bsrc) is
 * drained here: buffer empties, source refills it a chunk at a time - or
 * skips the buffer entirely via sendfile(2) on a plaintext socket - so a
 * large file response never holds more than one chunk in memory. */
static int hm_flush(pTHX_ hm_conn *c) {
    hm_loop *loop = c->loop;
    int src_done = 0;
    if (c->wlen == 0 && !c->bsrc.kind) {
        if (!c->keepalive && !c->awaiting) { hm_close(aTHX_ loop, c); return -1; }
        if (c->stream_done) {                    /* a streamed body ended
                                                  * with nothing to flush  */
            c->stream_done = 0;
            if (c->rlen > 0 || loop->completion) hm_again_push(loop, c);
        }
        return 0;
    }
    for (;;) {
        while (c->woff < c->wlen) {
            ssize_t n = hm_cwrite(c, c->wbuf + c->woff, c->wlen - c->woff);
            if (n > 0) {
                c->woff += (size_t)n;
            } else if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
                /* arm a write watcher, unless the TLS engine is blocked
                 * wanting a read (the always-armed read watcher re-drives
                 * us then) */
                if (!c->writing && !c->tls_w_wants_r) {
                    loop->be->add_io(loop->be, c->fd, HM_EV_WRITE, 0);
                    c->writing = 1;
                }
                return 0;
            } else {
                hm_close(aTHX_ loop, c);
                return -1;
            }
        }
        /* buffer drained; keep the capacity for the next round */
        c->wlen = c->woff = 0;
        if (!c->bsrc.kind) break;

#if defined(HM_HAVE_SENDFILE_LINUX) || defined(HM_HAVE_SENDFILE_FREEBSD) \
    || defined(HM_HAVE_SENDFILE_MACOS)
        /* file straight to a plaintext socket, no copy through userspace */
        if (c->bsrc.kind == 1 && (!c->ssl || hm_ktls_tx(c)) && !c->bsrc.nosf) {
            size_t want = c->bsrc.remaining < HM_SENDFILE_MAX
                              ? (size_t)c->bsrc.remaining : HM_SENDFILE_MAX;
            ssize_t sent = -1;
            int again = 0;
#if defined(HM_HAVE_SENDFILE_LINUX)
            off_t off = c->bsrc.off;
            sent = sendfile(c->fd, c->bsrc.fd, &off, want);
            again = (sent < 0 && (errno == EAGAIN || errno == EWOULDBLOCK));
#elif defined(HM_HAVE_SENDFILE_FREEBSD)
            off_t sbytes = 0;
            int r = sendfile(c->bsrc.fd, c->fd, c->bsrc.off, want,
                             NULL, &sbytes, 0);
            if (r == 0 || errno == EAGAIN || errno == EWOULDBLOCK) {
                sent = (ssize_t)sbytes;
                again = (r != 0);
            }
#elif defined(HM_HAVE_SENDFILE_MACOS)
            off_t len = (off_t)want;
            int r = sendfile(c->bsrc.fd, c->fd, c->bsrc.off, &len, NULL, 0);
            if (r == 0 || errno == EAGAIN || errno == EWOULDBLOCK) {
                sent = (ssize_t)len;
                again = (r != 0);
            }
#endif
            if (sent > 0) {
                c->bsrc.off += sent;
                c->bsrc.remaining -= (UV)sent;
            }
            if (c->bsrc.remaining == 0) {
                hm_bsrc_release(aTHX_ &c->bsrc);
                src_done = 1;
                break;
            }
            if (again || sent > 0) {
                if (again) {
                    if (!c->writing) {
                        loop->be->add_io(loop->be, c->fd, HM_EV_WRITE, 0);
                        c->writing = 1;
                    }
                    return 0;
                }
                continue;                    /* kernel took it all; try more */
            }
            /* sendfile refused this pairing (filesystem, socket state):
             * fall back to the read path for the rest of this body */
            c->bsrc.nosf = 1;
        }
#endif
        {
            ssize_t n = hm_bsrc_fill(aTHX_ c);
            if (n <= 0) {
                /* spent (0), or ended early (-1, keepalive already off) */
                hm_bsrc_release(aTHX_ &c->bsrc);
                src_done = 1;
                if (c->wlen) continue;       /* write what the fill added */
                break;
            }
        }
    }
    /* fully written; keep the buffer + capacity for the next response */
    c->wlen = c->woff = 0;
    if (c->writing) {
        loop->be->remove_io(loop->be, c->fd, HM_EV_WRITE);
        c->writing = 0;
    }
    /* A stream handle that was told to stop producing can start again. The
     * callback may write, and writing calls back in here, so re-read the
     * connection afterwards rather than deciding on what was true before. */
    if (hm_stream_paused_n) {
        int fd = c->fd;
        hm_stream_drained(aTHX_ c);
        if (loop->conns[fd] != c)      return -1;
        if (c->wlen || c->bsrc.kind)   return 0;
    }
    /* hm_process stopped on a full write buffer with requests still in the
     * read buffer: the client has now read, so serve the next of them. */
    if (c->backlogged) { c->backlogged = 0; hm_again_push(loop, c); }
    if (!c->keepalive && !c->awaiting) { hm_close(aTHX_ loop, c); return -1; }
    /* requests that pipelined in behind the stream were parked; pick them
     * up on the next loop turn */
    /* Pipelined requests behind the stream are picked up next turn - and in
     * completion mode the SAME push is what re-arms the read, which is why
     * an empty buffer is scheduled too. Without that a connection that
     * streamed a body would never be read from again. */
    if (c->stream_done) { c->stream_done = 0; src_done = 1; }
    if (src_done && (c->rlen > 0 || loop->completion)) hm_again_push(loop, c);
    return 0;
}

/* ---- async delivery (Future + streaming) --------------------------------- */

/* Deliver an async response to a parked connection. No-op if the
 * connection is gone or the fd was reused. */
static void hm_deliver(pTHX_ int fd, UV id, SV *resp) {
    hm_loop *loop = hm_cur_loop;
    hm_conn *c = (loop && fd >= 0 && fd < HM_MAXFD) ? loop->conns[fd] : NULL;
    if (!(c && c->id == id)) return;
    /* Only a parked connection is owed a response. A responder called a
     * second time, or the 500 for a streaming handler that died AFTER it
     * had already answered, would otherwise be serialised onto a live
     * keep-alive connection as the reply to its next request. */
    if (!c->awaiting) return;
    /* A streamed response is open on this connection: a stream handle
     * opened after the handler parked (the documented pattern), whose
     * Future has now resolved with a value that is not a response, or a
     * second response for one request. Serialising it here put a status
     * line inside a chunked body. The handle owns the response; this one
     * is dropped. (A handler that DIED with a writer alive is the other
     * case, and hm_delayed closes the connection for that.) */
    if (c->stream_open) {
        if (c->resp_f) { SvREFCNT_dec(c->resp_f); c->resp_f = NULL; }
        return;
    }
    /* detached from inside a Future continuation (async work before an
     * upgrade): the app owns the socket, so drop the response it resolved
     * with and forget the connection */
    if (c->detached) {
        c->awaiting = 0;
        if (c->env_sv) {
            hm_access_log(aTHX_ loop, c->env_sv, 101, -1);
            SvREFCNT_dec(c->env_sv);
            c->env_sv = NULL;
        }
        hm_conn_release(aTHX_ loop, c);
        return;
    }
    c->awaiting = 0;
    if (c->resp_f) { SvREFCNT_dec(c->resp_f); c->resp_f = NULL; }
    if (c->env_sv) {
        SV *env_rv = c->env_sv;
        c->env_sv = NULL;
        if (hm_queue_response(aTHX_ c, resp) == 0 && loop->conns[fd] == c) {
            hm_access_log(aTHX_ loop, env_rv, c->last_status,
                          (ssize_t)c->last_blen);
            SvREFCNT_dec(env_rv);
            if (hm_flush(aTHX_ c) == 0 && loop->conns[fd] == c) {
                hm_process(aTHX_ c);
                if (loop->conns[fd] == c) hm_flush(aTHX_ c);
            }
        } else {
            SvREFCNT_dec(env_rv);
        }
        /* Parked connections have no read outstanding - the completion
         * handler skipped arming one while the response was being made - so
         * the re-arm is scheduled here, for the top of the next turn. */
        if (loop->conns[fd] == c && !c->awaiting && !c->bsrc.kind)
            hm_again_push(loop, c);
        return;
    }
    if (hm_queue_response(aTHX_ c, resp) == 0
        && loop->conns[fd] == c
        && hm_flush(aTHX_ c) == 0
        && loop->conns[fd] == c) {
        hm_process(aTHX_ c);
        if (loop->conns[fd] == c) hm_flush(aTHX_ c);
    }
    if (loop->conns[fd] == c && !c->awaiting && !c->bsrc.kind)
        hm_again_push(loop, c);
}

/* How a streamed HTTP/1.1 body is framed, decided once in hm_start_stream:
 *   EOF     - Connection: close, the body ends when the connection does. The
 *             only framing there was before 0.57, and still the one for
 *             HTTP/1.0, a request that asked to close, a 101 tunnel, an app
 *             that wrote its own Transfer-Encoding, and a draining server.
 *   CHUNKED - Transfer-Encoding: chunked, keep-alive kept. Every write is one
 *             chunk, close sends the last one, and the pipeline resumes.
 *   LENGTH  - the app gave Content-Length: bytes are counted; an overrun or
 *             a short body breaks the framing, so the connection closes.
 *   NOBODY  - 1xx, 204, 304: headers only, keep-alive kept. */
#define HM_SM_EOF     0
#define HM_SM_CHUNKED 1
#define HM_SM_LENGTH  2
#define HM_SM_NOBODY  3

/* Send status+headers for a streaming response; body follows via
 * hm_stream_write/hm_stream_close. */
/* 1 when the response was begun; 0 when it was refused because this
 * connection is not parked on a response, or one has already begun; -1
 * when the headers were refused (a 500 went out instead). Only a PARKED
 * connection with no response begun: a second responder call, or a stale
 * responder kept from an earlier request on this keep-alive connection,
 * otherwise wrote a status line and headers into the body being streamed,
 * or into the next request's response. */
static int hm_start_stream(pTHX_ int fd, UV id, int status, SV *headers) {
    hm_loop *loop = hm_cur_loop;
    hm_conn *c = (loop && fd >= 0 && fd < HM_MAXFD) ? loop->conns[fd] : NULL;
    char line[64];
    AV *hav = NULL;
    int has_date = 0, has_conn = 0, conn_close = 0, has_te = 0, has_len = 0;
    UV clen = 0;
    if (!(c && c->id == id)) return 0;
    if (!c->awaiting || c->stream_open) return 0;
    c->stream_open = 1;
    c->stream_serial++;
    c->stream_mode = HM_SM_EOF;
    c->stream_left = 0;
    c->stream_done = 0;
    if (c->env_sv) {
        hm_access_log(aTHX_ loop, c->env_sv, status, -1);
        SvREFCNT_dec(c->env_sv);
        c->env_sv = NULL;
    }
    if (headers && SvROK(headers) && SvTYPE(SvRV(headers)) == SVt_PVAV)
        hav = (AV *)SvRV(headers);
    if (!hm_headers_ok(aTHX_ hav)) {
        hm_errlog("Hyperman: response header carries CR, LF or NUL; refused\n");
        hm_wb_put(c, HM_ERR500, sizeof(HM_ERR500) - 1);
        c->last_status = 500;
        c->last_blen = 21;
        c->awaiting = 0;          /* nothing more is owed on this connection */
        c->stream_open = 0;
        hm_flush(aTHX_ c);
        return -1;
    }
    {
        size_t sll;
        const char *sl = hm_status_line(status, &sll, line);
        hm_wb_put(c, sl, sll);
    }
    if (hav) {
        SSize_t i, hn = av_len(hav) + 1;
        for (i = 0; i + 1 < hn; i += 2) {
            SV **k = av_fetch(hav, i, 0);
            SV **v = av_fetch(hav, i + 1, 0);
            STRLEN kl, vl;
            const char *ks, *vs;
            if (!k || !*k || !v || !*v) continue;
            ks = SvPV(*k, kl);
            vs = SvPV(*v, vl);
            if (kl == 4 && (ks[0] | 32) == 'd' && hm_strncasecmp(ks, "Date", 4) == 0)
                has_date = 1;
            else if (kl == 10 && (ks[0] | 32) == 'c'
                     && hm_strncasecmp(ks, "Connection", 10) == 0) {
                has_conn = 1;
                if (hm_ci_contains(vs, vl, "close")) conn_close = 1;
            }
            else if (kl == 14 && (ks[0] | 32) == 'c'
                     && hm_strncasecmp(ks, "Content-Length", 14) == 0)
                has_len = 1;
            else if (kl == 17 && (ks[0] | 32) == 't'
                     && hm_strncasecmp(ks, "Transfer-Encoding", 17) == 0)
                has_te = 1;
            hm_wb_put(c, ks, kl);
            hm_wb_put(c, ": ", 2);
            hm_wb_put(c, vs, vl);
            hm_wb_put(c, "\r\n", 2);
        }
    }
    if (!has_date) {
        const char *dh = hm_date_header(loop);
        if (dh) hm_wb_put(c, dh, HM_DATE_LEN);
    }
    if (has_len && !hm_hav_clen(aTHX_ hav, &clen)) has_len = 0;  /* unparseable */
    {
        int no_entity = (status < 200 || status == 204 || status == 304);
        if (status == 101 || loop->stopping || !c->req_11 || !c->keepalive
            || conn_close || has_te) {
            c->stream_mode = HM_SM_EOF;
            c->keepalive = 0;
        } else if (no_entity) {
            c->stream_mode = HM_SM_NOBODY;
        } else if (has_len) {
            c->stream_mode = HM_SM_LENGTH;
            c->stream_left = clen;
        } else {
            c->stream_mode = HM_SM_CHUNKED;
            hm_wb_put(c, "Transfer-Encoding: chunked\r\n", 28);
        }
    }
    /* A 101 is an upgrade: the caller's own Upgrade and Connection headers
     * are the response, and "close" would contradict them. The connection
     * still ends when the handle closes - keepalive is off above. */
    if (status == 101)                    hm_wb_put(c, "\r\n", 2);
    else if (c->stream_mode == HM_SM_EOF) hm_wb_put(c, "Connection: close\r\n\r\n", 21);
    else {
        if (!has_conn) hm_wb_put(c, "Connection: keep-alive\r\n", 24);
        hm_wb_put(c, "\r\n", 2);
    }
    hm_flush(aTHX_ c);
    return 1;
}

/* `serial` names the streamed response a Writer was handed out for; 0 skips
 * the check (the ABI stream handle has a registry of its own for that). A
 * writer kept past its response, or closed twice, must not touch the
 * response that followed on the same keep-alive connection. */
/* Queue body bytes on a streamed HTTP/1.1 response, framed as the mode says.
 * Shared by the Writer (hm_stream_write) and the ABI stream handle
 * (hm_stream.h), so the two cannot disagree about the framing. Returns the
 * octets queued: 0 when nothing was (HEAD, no body, past the end), and the
 * caller then has nothing to flush. */
static STRLEN hm_stream_body_put(pTHX_ hm_conn *c, const char *p, STRLEN l) {
    if (!c->awaiting && !c->tunnel) return 0;   /* the body has ended     */
    if (c->is_head && !c->tunnel) return 0;     /* HEAD: headers only     */
    if (c->stream_mode == HM_SM_NOBODY) return 0;
    if (c->stream_mode == HM_SM_LENGTH) {
        if (l > c->stream_left) {                /* overrun: the framing is
                                                  * broken, end the conn  */
            l = (STRLEN)c->stream_left;
            c->keepalive = 0;
        }
        c->stream_left -= l;
    }
    if (!l) return 0;                            /* a zero chunk ends a body */
    if (c->stream_mode == HM_SM_CHUNKED) {
        char sz[24];
        int n = snprintf(sz, sizeof(sz), "%" UVxf "\r\n", (UV)l);
        hm_wb_put(c, sz, (size_t)n);
        hm_wb_put(c, p, l);
        hm_wb_put(c, "\r\n", 2);
    } else {
        hm_wb_put(c, p, l);
    }
    c->loop->bytes_out += l;
    return l;
}

/* 1: queued, and the connection holds less than HM_WB_HIWAT unwritten.
 * 0: queued, but the connection is holding more than that - the producer
 *    should wait for a later write to answer 1 again before writing more
 *    (the Perl Writer's ->write hands this back; before 0.62 it returned
 *    nothing, and a producer feeding a slow reader grew the buffer without
 *    bound).
 * -1: not written: the writer is stale (closed, or from an earlier
 *    request on this connection). */
static int hm_stream_write(pTHX_ int fd, UV id, UV serial, SV *data) {
    hm_loop *loop = hm_cur_loop;
    hm_conn *c = (loop && fd >= 0 && fd < HM_MAXFD) ? loop->conns[fd] : NULL;
    STRLEN l;
    const char *p;
    if (!(c && c->id == id)) return -1;
    if (serial && c->stream_serial != serial) return -1;
    if (!c->awaiting && !c->tunnel) return -1;
    p = SvPV(data, l);
    if (hm_stream_body_put(aTHX_ c, p, l)) hm_flush(aTHX_ c);
    if (loop->conns[fd] != c) return -1;
    return (c->wlen - c->woff) > HM_WB_HIWAT ? 0 : 1;
}

static void hm_stream_close(pTHX_ int fd, UV id, UV serial) {
    hm_loop *loop = hm_cur_loop;
    hm_conn *c = (loop && fd >= 0 && fd < HM_MAXFD) ? loop->conns[fd] : NULL;
    if (!(c && c->id == id)) return;
    if (serial && c->stream_serial != serial) return;
    if (!c->awaiting) return;                    /* closed already         */
    c->stream_open = 0;
    if (c->stream_mode == HM_SM_CHUNKED && !c->is_head)
        hm_wb_put(c, "0\r\n\r\n", 5);
    else if (c->stream_mode == HM_SM_LENGTH && c->stream_left && !c->is_head)
        c->keepalive = 0;                        /* short body: cannot keep
                                                  * the connection in frame */
    c->stream_mode = HM_SM_EOF;
    c->stream_left = 0;
    c->awaiting = 0;
    if (c->keepalive) c->stream_done = 1;        /* hm_flush: serve the
                                                  * pipeline when drained  */
    hm_flush(aTHX_ c);   /* closes when drained if keepalive is off */
}

/* The parked-Future continuation: when the response Future resolves,
 * deliver its result (or a 500 on failure, logged to psgi.errors). */
XS_INTERNAL(hm_xs_park_cb);
XS_INTERNAL(hm_xs_park_cb) {
    dXSARGS;
    hm_clos *cl = hm_clos_of(aTHX_ cv);
    SV *f, *resp = NULL;
    IV st;
    int fd;
    UV id;
    if (!cl || items < 1) XSRETURN_EMPTY;
    f = ST(0);
    fd = (int)cl->i;
    id = cl->u;
    st = hm_any_state(aTHX_ f);
    if (st == HMF_DONE) {
        if (hmf_is_future(aTHX_ f)) {
            AV *vals = hmf_values_av(aTHX_ f);
            SV **e = (vals && av_len(vals) >= 0) ? av_fetch(vals, 0, 0) : NULL;
            resp = e ? sv_mortalcopy(*e) : NULL;
        } else {
            dSP;
            SV *got = NULL;
            int n;
            ENTER; SAVETMPS; PUSHMARK(SP);
            XPUSHs(f);
            PUTBACK;
            n = call_method("get", G_SCALAR | G_EVAL);
            SPAGAIN;
            if (n && !SvTRUE(ERRSV)) got = newSVsv(POPs);
            else if (n) (void)POPs;
            PUTBACK; FREETMPS; LEAVE;
            if (got) resp = sv_2mortal(got);   /* mortal in the outer frame */
        }
    } else if (st == HMF_FAILED) {
        AV *errs = newAV();
        SV **e;
        STRLEN l;
        const char *msg = "unknown error";
        hm_any_values(aTHX_ f, st, errs);
        e = av_len(errs) >= 0 ? av_fetch(errs, 0, 0) : NULL;
        if (e && SvOK(*e)) msg = SvPV(*e, l);
        hm_errlog("Hyperman: async response failed: %s%s",
                      msg, (*msg && msg[strlen(msg)-1] == '\n') ? "" : "\n");
        SvREFCNT_dec((SV *)errs);
    }
    if (!(resp && SvROK(resp) && SvTYPE(SvRV(resp)) == SVt_PVAV))
        resp = hm_500_resp(aTHX_ st == HMF_DONE
                           ? "Bad async response" : "Internal Server Error");
    hm_deliver(aTHX_ fd, id, resp);
    XSRETURN_EMPTY;
}

/* Park a connection awaiting a pending Future. */
static void hm_park(pTHX_ hm_conn *c, SV *future) {
    SV *cb = hm_closure(aTHX_ hm_xs_park_cb, NULL, NULL, NULL, NULL,
                        (IV)c->fd, c->id);
    hm_any_on_ready(aTHX_ future, cb);
    SvREFCNT_dec(cb);
}

/* psgi.streaming responder: [$s,$h,$b] delivers now; [$s,$h] starts a
 * stream and returns a Hyperman::Writer. */
XS_INTERNAL(hm_xs_responder);
XS_INTERNAL(hm_xs_responder) {
    dXSARGS;
    hm_clos *cl = hm_clos_of(aTHX_ cv);
    SV *r;
    AV *rav;
    SSize_t n;
    int fd;
    UV id;
    if (!cl) XSRETURN_EMPTY;
    fd = (int)cl->i;
    id = cl->u;
    if (items < 1 || !(SvROK(ST(0)) && SvTYPE(SvRV(ST(0))) == SVt_PVAV))
        croak("Hyperman responder expects an array reference");
    r = ST(0);
    rav = (AV *)SvRV(r);
    n = av_len(rav) + 1;
    if (n >= 3) {
        hm_conn *c = (hm_cur_loop && fd >= 0 && fd < HM_MAXFD)
                   ? hm_cur_loop->conns[fd] : NULL;
        if (c && c->id == id && c->stream_open)
            croak("Hyperman responder: a response has already been started "
                  "on this connection");
        hm_deliver(aTHX_ fd, id, r);
        XSRETURN_EMPTY;
    }
    if (n == 2) {
        SV **s0 = av_fetch(rav, 0, 0);
        SV **s1 = av_fetch(rav, 1, 0);
        AV *w = newAV();
        SV *wrv;
        int began = hm_start_stream(aTHX_ fd, id, s0 ? (int)SvIV(*s0) : 200,
                                    s1 ? *s1 : NULL);
        if (began == 0) {
            SvREFCNT_dec((SV *)w);
            croak("Hyperman responder: a response has already been started "
                  "on this connection");
        }
        if (began < 0) {
            SvREFCNT_dec((SV *)w);
            croak("Hyperman responder: a response header carries CR, LF or NUL");
        }
        av_push(w, newSViv(fd));
        av_push(w, newSVuv(id));
        /* the serial of the response just begun, read back from the conn
         * (hm_start_stream is a no-op on a stale ticket, so 0 then) */
        {
            hm_conn *c = (hm_cur_loop && fd >= 0 && fd < HM_MAXFD)
                       ? hm_cur_loop->conns[fd] : NULL;
            av_push(w, newSVuv(c && c->id == id ? c->stream_serial : 0));
        }
        av_push(w, newSVpvs("h1"));
        wrv = newRV_noinc((SV *)w);
        sv_bless(wrv, gv_stashpv("Hyperman::Writer", GV_ADD));
        ST(0) = sv_2mortal(wrv);
        XSRETURN(1);
    }
    croak("Hyperman responder expects [status, headers, body] or [status, headers]");
}

/* psgi.streaming: run the app's coderef with a responder. */
static void hm_delayed(pTHX_ hm_conn *c, SV *code) {
    SV *responder = hm_closure(aTHX_ hm_xs_responder, NULL, NULL, NULL, NULL,
                               (IV)c->fd, c->id);
    int fd = c->fd;
    UV id = c->id;
    dSP;
    ENTER; SAVETMPS;
    PUSHMARK(SP);
    XPUSHs(responder);
    PUTBACK;
    call_sv(code, G_DISCARD | G_EVAL);
    if (SvTRUE(ERRSV)) {
        hm_loop *loop = hm_cur_loop;
        hm_conn *c = (loop && fd >= 0 && fd < HM_MAXFD) ? loop->conns[fd] : NULL;
        if (c && c->id == id && c->awaiting && c->stream_open) {
            /* It answered through the responder, kept the writer alive, and
             * died with the body part written. A 500 cannot follow a status
             * line already sent, and ending the body as if complete would
             * hand the client a cut body it may cache: the connection ends
             * once what was written has drained, WITHOUT the terminating
             * chunk. (A writer that was a lexical has already been
             * destroyed by the unwind and ended the body itself, the
             * promise a dropped writer keeps.) */
            hm_warn_died(aTHX_ "streaming handler");
            c->stream_open = 0;
            c->stream_mode = HM_SM_EOF;
            c->stream_done = 0;
            c->keepalive = 0;
            c->awaiting = 0;
            if (c->resp_f) { SvREFCNT_dec(c->resp_f); c->resp_f = NULL; }
            hm_flush(aTHX_ c);
        } else {
            hm_deliver(aTHX_ fd, id, hm_500_resp(aTHX_ "Internal Server Error"));
        }
    }
    FREETMPS; LEAVE;
    SvREFCNT_dec(responder);
}

/* ---- HTTP/2 (h2c via nghttp2), reusing the helpers above ----------------- */

#include "hm_http2.h"

/* ---- ABI v6 stream handles, over both halves above ---------------------- */

#include "hm_stream.h"

/* ---- HTTP/3 over QUIC: the transport, then the semantics ---------------- */

#include "hm_quic.h"
#include "hm_http3.h"

/* ---- request processing -------------------------------------------------- */

static void hm_again_push(hm_loop *loop, hm_conn *c) {
    if (loop->again_n == loop->again_cap) {
        loop->again_cap = loop->again_cap ? loop->again_cap * 2 : 64;
        loop->again = (hm_again *)hm_xrealloc(loop->again,
                          loop->again_cap * sizeof(hm_again));
    }
    loop->again[loop->again_n].fd = c->fd;
    loop->again[loop->again_n].id = c->id;
    loop->again_n++;
    c->again_queued = 1;
}

/* Built-in https redirect: synthesize a 301 to the same host and target on
 * the https port, so a plain :80 listener can bounce browsers to :443 without
 * ever touching the app. Host comes from the request (its :port is stripped);
 * the target port is omitted when it is the standard 443. */
static SV *hm_redirect_response(pTHX_ HV *env, int https_port) {
    const char *host, *uri;
    STRLEN hlen, ulen;
    SV *loc;
    AV *rav, *hav, *bav;
    host = hm_env_str(aTHX_ env, "HTTP_HOST", 9, &hlen);
    uri  = hm_env_str(aTHX_ env, "REQUEST_URI", 11, &ulen);
    if (!uri) { uri = "/"; ulen = 1; }
    loc = newSVpvs("https://");
    /* Host and request-target are reflected into a response header, so stop at
     * the first control or space byte: a header value is only \r-delimited, so
     * a bare LF could otherwise smuggle a CRLF and inject/split the response.
     * A valid host[:port] / request-target contains no CTL or SP anyway. */
    if (host) {
        STRLEN i = 0;                                    /* host, up to :port */
        if (hlen && host[0] == '[') {                    /* [v6]:port: whole */
            while (i < hlen && (unsigned char)host[i] > ' ' && host[i] != ']') i++;
            if (i < hlen && host[i] == ']') i++;
        } else {
            while (i < hlen && (unsigned char)host[i] > ' ' && host[i] != ':') i++;
        }
        sv_catpvn(loc, host, i);
    }
    if (https_port != 443) sv_catpvf(loc, ":%d", https_port);
    {
        STRLEN i = 0;                                    /* request-target */
        while (i < ulen && (unsigned char)uri[i] > ' ') i++;
        sv_catpvn(loc, uri, i);
    }

    hav = newAV();
    av_push(hav, newSVpvs("Location"));       av_push(hav, loc);
    av_push(hav, newSVpvs("Content-Length")); av_push(hav, newSVpvs("0"));
    bav = newAV();
    rav = newAV();
    av_push(rav, newSViv(301));
    av_push(rav, newRV_noinc((SV *)hav));
    av_push(rav, newRV_noinc((SV *)bav));
    return newRV_noinc((SV *)rav);
}

/* Process complete requests buffered in c->rbuf, capped per wakeup so one
 * pipelined connection can't starve the others (fairness). */
static void hm_process(pTHX_ hm_conn *c) {
    hm_loop *loop = c->loop;
    int served = 0;

    /* HTTP/2: once a connection is in h2 mode, all bytes go to nghttp2.
     * On a fresh connection with http2 enabled, sniff the h2 preface. */
    if (c->h2) { hm_h2_input(aTHX_ c); return; }
    if (c->lst && c->lst->http2 && c->nreqs == 0 && c->rlen > 0) {
        if (hm_h2_detect(aTHX_ c) != 0) return;   /* started, or need more */
    }

    for (;;) {
        if (c->awaiting) break;   /* parked; resumes via hm_deliver */
        if (c->bsrc.kind) break;  /* a body is still streaming out; the
                                     flush that finishes it re-queues us */
        /* Responses the client has not read yet. A pipeline that never
         * reads would otherwise have every request in the read buffer
         * answered into a write buffer that only grows: hold the rest until
         * the drain re-queues this connection (hm_flush, backlogged). */
        if (c->wlen - c->woff > HM_WB_HIWAT) { c->backlogged = 1; break; }

        /* RFC 9112 2.2: at least one empty line before the request line is
         * ignored. Stripped here so the request line is the first line and
         * not an empty one with the real request line read as a header. */
        if (c->rlen >= 2 && c->rbuf[0] == '\r' && c->rbuf[1] == '\n') {
            size_t k = 0;
            while (k + 1 < c->rlen && c->rbuf[k] == '\r' && c->rbuf[k + 1] == '\n') k += 2;
            memmove(c->rbuf, c->rbuf + k, c->rlen - k);
            c->rlen -= k;
            if (c->rlen == 0) break;
        }

        char *end = (char *)hm_memmem(c->rbuf, c->rlen, "\r\n\r\n", 4);
        if (!end) break;

        if (served >= loop->max_pipeline) {     /* yield to other conns */
            hm_again_push(loop, c);
            break;
        }

        size_t headlen = (size_t)(end - c->rbuf);
        size_t bodystart = headlen + 4;

        /* one pass: framing decision + header-line index for the env build */
        hm_hline hidx_stack[HM_HIDX_STACK];
        const hm_hline *hidx = hidx_stack;
        int nhl = 0;
        long clen = 0;
        int frame = hm_parse_index(c->rbuf, headlen, &clen,
                                   hidx_stack, HM_HIDX_STACK, &nhl);
        if (frame == -2) {          /* > HM_HIDX_STACK header lines: rescan
                                       into the loop's growable index */
            if (nhl > loop->hidx_cap) {
                loop->hidx = (hm_hline *)hm_xrealloc(loop->hidx,
                                 (size_t)nhl * sizeof(hm_hline));
                loop->hidx_cap = nhl;
            }
            frame = hm_parse_index(c->rbuf, headlen, &clen,
                                   loop->hidx, loop->hidx_cap, &nhl);
            hidx = loop->hidx;
        }
        if (frame) {                            /* smuggling/framing: reject + close */
            if (frame == 501) {
                static const char e501[] =
                    "HTTP/1.1 501 Not Implemented\r\nContent-Length: 0\r\n"
                    "Connection: close\r\n\r\n";
                hm_wb_put(c, e501, sizeof(e501) - 1);
            } else {
                static const char e400[] =
                    "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n"
                    "Connection: close\r\n\r\n";
                hm_wb_put(c, e400, sizeof(e400) - 1);
            }
            c->keepalive = 0;
            break;
        }

        /* An expectation this server cannot meet is refused now, on the
         * headers alone - which is the answer the client asked for. */
        int expect = hm_expectation(c->rbuf, headlen);
        if (expect == HM_EXPECT_OTHER) {
            static const char e417[] =
                "HTTP/1.1 417 Expectation Failed\r\nContent-Length: 0\r\n"
                "Connection: close\r\n\r\n";
            hm_wb_put(c, e417, sizeof(e417) - 1);
            c->keepalive = 0;
            c->last_status = 417;
            break;
        }

        size_t body_consumed;   /* wire octets after the headers this request uses */
        if (clen < 0) {         /* chunked: validate, then decode in place */
            size_t enc = 0, dec = 0;
            int r = hm_chunked_scan_from(c->rbuf + bodystart, c->rlen - bodystart,
                                         &enc, &dec, loop->max_read,
                                         &c->chunk_pos, &c->chunk_dec);
            if (r == HM_CHUNKED_TOOBIG) {   /* well framed, just too large */
                static const char e413[] =
                    "HTTP/1.1 413 Payload Too Large\r\nContent-Length: 0\r\n"
                    "Connection: close\r\n\r\n";
                hm_wb_put(c, e413, sizeof(e413) - 1);
                c->keepalive = 0;
                c->last_status = 413;
                break;
            }
            if (r < 0) {                                 /* malformed chunked */
                static const char e400[] =
                    "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n"
                    "Connection: close\r\n\r\n";
                hm_wb_put(c, e400, sizeof(e400) - 1);
                c->keepalive = 0;
                break;
            }
            if (r == 0) {                                /* need more bytes */
                c->body_phase = 1;
                c->body_t0 = loop->now;
                c->body_rx = 0;
                if (c->rlen - bodystart >= loop->max_read) {   /* too big to fit */
                    static const char e413[] =
                        "HTTP/1.1 413 Payload Too Large\r\nContent-Length: 0\r\n"
                        "Connection: close\r\n\r\n";
                    hm_wb_put(c, e413, sizeof(e413) - 1);
                    c->keepalive = 0;
                }
                else hm_send_continue(c, expect);
                break;
            }
            hm_chunked_compact(c->rbuf + bodystart);     /* -> dec octets @ bodystart */
            clen = (long)dec;
            body_consumed = enc;
        } else {
            if (bodystart + (size_t)clen > loop->max_read) {   /* can never fit */
                static const char e413[] =
                    "HTTP/1.1 413 Payload Too Large\r\nContent-Length: 0\r\n"
                    "Connection: close\r\n\r\n";
                hm_wb_put(c, e413, sizeof(e413) - 1);
                c->keepalive = 0;
                break;
            }
            /* A large body is drained to a temp file as it arrives rather
             * than accumulated here. rbuf keeps the headers and whatever
             * arrived after the body; the file gets the body.
             *
             * Identity framing only. A chunked body has no Content-Length to
             * divert on and its decoder wants the octets contiguous, so those
             * still accumulate - documented rather than hidden.
             *
             * Nothing about the loop changes: this still `break`s to wait for
             * more, and is re-driven when the socket is readable again. What
             * changes is that the waiting no longer costs the body. */
            if ((long)clen > HM_SPILL_BODY) {
                size_t have = c->rlen - bodystart;
                if (c->spill_fd < 0 && (c->spill_fd = hm_body_tmpfile()) < 0)
                    goto hm_no_spill;           /* cannot: behave as before */
                if (have) {
                    size_t want = have;
                    size_t room = (size_t)(clen - c->spill_got);
                    if (want > room) want = room;   /* the rest is the next
                                                     * request, not this body */
                    if (want) {
                        ssize_t w = write(c->spill_fd, c->rbuf + bodystart, want);
                        if (w <= 0) { hm_close(aTHX_ loop, c); return; }
                        c->spill_got += w;
                        memmove(c->rbuf + bodystart, c->rbuf + bodystart + w,
                                c->rlen - bodystart - w);
                        c->rlen -= (size_t)w;
                    }
                }
                if (c->spill_got < (long)clen) {           /* await more */
                    c->body_phase = 1;
                    c->body_t0 = loop->now;
                    c->body_rx = 0;
                    hm_send_continue(c, expect);
                    break;
                }
                if (lseek(c->spill_fd, 0, SEEK_SET) < 0) {
                    hm_close(aTHX_ loop, c); return;
                }
                body_consumed = 0;              /* none of it is in rbuf */
                goto hm_have_body;
            }
        hm_no_spill:
            if (c->rlen - bodystart < (size_t)clen) {        /* await full body */
                c->body_phase = 1;
                c->body_t0 = loop->now;
                c->body_rx = 0;
                hm_send_continue(c, expect);
                break;
            }
            body_consumed = (size_t)clen;
        hm_have_body: ;
        }

#ifdef HM_HAVE_NGHTTP2
        /* HTTP/1.1 Upgrade: h2c (cleartext, first request only). h2c-over-TLS
         * is forbidden by RFC 7540; the TLS path uses ALPN. */
        if (c->lst && c->lst->http2 && !c->ssl && !c->h2 && c->nreqs == 0) {
            size_t ul = 0, sl = 0;
            const char *up = hm_find_header(c->rbuf, headlen, "upgrade", 7, &ul);
            const char *se = hm_find_header(c->rbuf, headlen, "http2-settings", 14, &sl);
            if (up && se && hm_ci_contains(up, ul, "h2c")) {
                static const char resp101[] =
                    "HTTP/1.1 101 Switching Protocols\r\n"
                    "Connection: Upgrade\r\nUpgrade: h2c\r\n\r\n";
                int is_head = (headlen >= 5 && hm_strncasecmp(c->rbuf, "HEAD ", 5) == 0);
                HV *env = hm_build_env(aTHX_ c->rbuf, headlen,
                                       c->rbuf + bodystart, clen, c, hidx, nhl);
                (void)hv_stores(env, "SERVER_PROTOCOL", newSVpvs("HTTP/2"));
                hm_wb_put(c, resp101, sizeof(resp101) - 1);
                {
                    size_t consumed = bodystart + body_consumed;
                    memmove(c->rbuf, c->rbuf + consumed, c->rlen - consumed);
                    c->rlen -= consumed;
                    c->nreqs++;
                    loop->requests++;
                    /* The request is consumed: the header and body clocks
                     * stop here, as they do for an ordinary request below.
                     * An h2 session never passes that point again, so one
                     * whose upgrade arrived in two reads was closed by the
                     * sweep as a slow request header_timeout later. */
                    c->req_start = 0;
                    c->body_phase = 0;
                    c->chunk_pos = c->chunk_dec = 0;
                }
                {
                    int rc;
                    c->in_app = 1;      /* the upgrade dispatches stream 1 */
                    rc = hm_h2_start_upgrade(aTHX_ c, se, sl, env, is_head);
                    c->in_app = 0;
                    if (rc < 0 || c->close_deferred) {
                        c->close_deferred = 0;
                        hm_close(aTHX_ loop, c);
                        return;
                    }
                    hm_read_resume(loop, c);
                }
                hm_h2_input(aTHX_ c);   /* client's h2 preface + frames, + flush */
                return;
            }
        }
#endif

        HV *env = hm_build_env(aTHX_ c->rbuf, headlen, c->rbuf + bodystart,
                               clen, c, hidx, nhl);
        SV *env_rv = NULL;
        SV *resp;
        c->in_app = 1;          /* see hm_close: a close meanwhile is deferred */
        if (c->lst && c->lst->redirect_https) {   /* built-in :80 -> :443 bounce */
            resp = hm_redirect_response(aTHX_ env, c->lst->redirect_https);
            if (hm_logging(loop)) env_rv = newRV_noinc((SV *)env);
            else                  SvREFCNT_dec((SV *)env);
        } else if (hm_logging(loop)) {      /* env survives for access_log */
            env_rv = newRV_noinc((SV *)env);
            resp = hm_call_app(aTHX_ loop, env_rv);
        } else {
            resp = hm_call_app0(aTHX_ loop, env);
        }
        c->in_app = 0;
        if (c->close_deferred) {
            /* The peer left, or the sweep gave up on it, while the handler
             * ran. Nothing it returned has anywhere to go. */
            c->close_deferred = 0;
            if (env_rv) SvREFCNT_dec(env_rv);
            if (resp)   SvREFCNT_dec(resp);
            hm_close(aTHX_ loop, c);
            return;
        }
        hm_read_resume(loop, c);

        /* The app called detach: the socket is its own now. Drop whatever it
         * returned (the sentinel), forget the connection, and do not touch c
         * again. Any bytes past this request are dropped with it - RFC 6455
         * forbids the client sending before the 101, so they are a protocol
         * violation, not data we owe anyone. */
        if (c->detached) {
            if (env_rv) {
                hm_access_log(aTHX_ loop, env_rv, 101, -1);
                SvREFCNT_dec(env_rv);
            }
            if (resp) SvREFCNT_dec(resp);
            c->nreqs++;
            loop->requests++;
            hm_conn_release(aTHX_ loop, c);
            return;
        }

        /* The app opened a 101 stream handle from inside the handler
         * (hm_stream_open): the connection is a tunnel now, its bytes
         * belong to the handle both ways, and the triplet the handler
         * returned is the same sentinel a detach leaves behind. Parked for
         * the life of the handle - stream_close unparks it, and the close
         * that follows (keepalive is off) ends the tunnel. Bytes that came
         * in with the request are the handle's too, so they are handed
         * over rather than parsed as a request that will never come. */
        if (c->tunnel == 2) {
            size_t consumed = bodystart + body_consumed;
            c->tunnel = 1;
            if (env_rv) {
                hm_access_log(aTHX_ loop, env_rv, 101, -1);
                SvREFCNT_dec(env_rv);
            }
            if (resp) SvREFCNT_dec(resp);
            memmove(c->rbuf, c->rbuf + consumed, c->rlen - consumed);
            c->rlen -= consumed;
            c->req_start = 0;
            c->nreqs++;
            loop->requests++;
            if (c->rlen) {
                size_t n = c->rlen;
                c->rlen = 0;
                if (!hm_stream_deliver(aTHX_ c->fd, c->id, -1, c->rbuf, n, 0)
                    && loop->conns[c->fd] == c)
                    c->rlen = n;        /* no read half yet: kept for it */
            }
            return;
        }

        size_t consumed = bodystart + body_consumed;
        memmove(c->rbuf, c->rbuf + consumed, c->rlen - consumed);
        c->rlen -= consumed;
        c->req_start = 0;
        c->body_phase = 0;
        c->chunk_pos = c->chunk_dec = 0;
        c->cont_sent = 0;       /* the next request on this connection asks
                                 * for itself */
        served++;
        c->nreqs++;
        hm_count_request(loop);

        /* async: handler returned a Future -> park and wait; hold the
         * future so a client disconnect can cancel it */
        if (resp && hm_is_awaitable(aTHX_ resp)) {
            c->awaiting = 1;
            c->resp_f = SvREFCNT_inc(resp);
            c->env_sv = env_rv;                 /* logged at delivery */
            hm_park(aTHX_ c, resp);
            SvREFCNT_dec(resp);
            if (loop->conns[c->fd] != c) return;   /* resolved + closed inline */
            break;
        }

        /* psgi.streaming: coderef response -> park, hand it a responder */
        if (resp && SvROK(resp) && SvTYPE(SvRV(resp)) == SVt_PVCV) {
            c->awaiting = 1;
            c->env_sv = env_rv;                 /* logged at respond time */
            hm_delayed(aTHX_ c, resp);
            SvREFCNT_dec(resp);
            if (loop->conns[c->fd] != c) return;   /* responder ran + closed */
            break;
        }

        {
            int rc = hm_queue_response(aTHX_ c, resp);
            if (resp) SvREFCNT_dec(resp);
            if (rc < 0) { if (env_rv) SvREFCNT_dec(env_rv); return; }
        }
        if (env_rv) {
            hm_access_log(aTHX_ loop, env_rv, c->last_status, (ssize_t)c->last_blen);
            SvREFCNT_dec(env_rv);
        }
        if (!c->keepalive) break;
    }
    if (loop->conns[c->fd] == c && c->rlen > 0 && !c->awaiting && !c->req_start)
        c->req_start = loop->now;
}

/* A tunnel's bytes belong to its handle, not the request parser: hand over
 * whatever the read left in rbuf, or keep it when no read half is registered
 * yet. ONE function for both ways a read completes - readiness (hm_readable)
 * and completion (HM_EV_RECV) - because the completion path once had no
 * tunnel branch at all and fed the bytes to hm_process, where they sat as
 * the start of a request that never came. Every Linux box with liburing
 * takes that path by default, and macOS never does, so the six smokers that
 * failed t/41 were the first time it ran. Returns 0 when the callback closed
 * the connection. */
static int hm_tunnel_bytes(pTHX_ hm_conn *c) {
    hm_loop *loop = c->loop;
    size_t have = c->rlen;
    c->rlen = 0;
    if (!hm_stream_deliver(aTHX_ c->fd, c->id, -1, c->rbuf, have, 0)) {
        if (loop->conns[c->fd] != c) return 0;
        c->rlen = have;         /* no read half yet: kept for it */
    }
    return loop->conns[c->fd] == c;
}

/* Stop and restart reading a connection whose buffer is full while a frame
 * on the stack still holds pointers into it (hm_readable, below). The
 * watcher is persistent and level-triggered, so leaving the bytes in the
 * socket without this would wake the loop every turn until the frame
 * returned. Readiness mode only: a completion-mode read is armed from the
 * top of a loop turn (hm_run_again), never from under a frame. */
static void hm_read_pause(hm_loop *loop, hm_conn *c) {
    if (c->read_paused) return;
    c->read_paused = 1;
    loop->be->remove_io(loop->be, c->fd, HM_EV_READ);
}
static void hm_read_resume(hm_loop *loop, hm_conn *c) {
    if (!c->read_paused) return;
    c->read_paused = 0;
    loop->be->add_io(loop->be, c->fd, HM_EV_READ, 0);
}

static void hm_readable(pTHX_ hm_conn *c) {
    hm_loop *loop = c->loop;
    for (;;) {
        if (c->rlen == c->rcap) {
            /* A synchronous handler on the stack (in_app) may be nghttp2
             * reading frames out of this very buffer, or hm_process holding
             * offsets into it: growing it here would move it from under
             * them. Leave the rest in the socket until that frame returns. */
            if (c->in_app && !hm_h2_reentrant(c)) {
                hm_read_pause(loop, c);
                break;
            }
            /* HTTP/2: the buffer holds frames, not a request, and nghttp2
             * parses them incrementally - a frame larger than the buffer is
             * consumed across reads. Hand over what is here and let the
             * level-triggered watcher bring the rest. The request ceiling
             * is per stream there (hm_h2_cb_data_chunk); applied to this
             * buffer it closed every upload over 16KB on a server whose
             * max_body was below that. */
            if (c->h2) break;
            /* The same, one read earlier: the preface is in the buffer but
             * hm_process has not run to detect it. Let it. */
            if (hm_h2_preface_pending(c)) break;
            /* grow for large bodies; unbounded headers are rejected */
            if (c->rcap >= loop->max_read
                || (c->rlen >= 65536 && !hm_memmem(c->rbuf, c->rlen, "\r\n\r\n", 4))) {
                /* The buffer is full and may not grow. Until max_body was
                 * configurable this only happened to a genuinely enormous
                 * request and a bare close was defensible; now that an
                 * operator can set the ceiling BELOW the initial buffer
                 * (HM_RBUF, 16KB), it is the ordinary way an oversize
                 * request ends - and dropping the connection with no
                 * response at all would leave every such client staring at
                 * a timeout rather than a 413. Answer, then close.
                 *
                 * Only for a request that at least has a complete header
                 * block: without one this is a client streaming garbage,
                 * which has earned nothing but the close it used to get. */
                if (c->rcap >= loop->max_read
                    && hm_memmem(c->rbuf, c->rlen, "\r\n\r\n", 4)) {
                    static const char e413[] =
                        "HTTP/1.1 413 Payload Too Large\r\nContent-Length: 0\r\n"
                        "Connection: close\r\n\r\n";
                    hm_wb_put(c, e413, sizeof(e413) - 1);
                    c->keepalive = 0;
                    c->last_status = 413;
                    c->last_blen = 0;
                    (void)hm_flush(aTHX_ c);
                    if (loop->conns[c->fd] != c) return;   /* flush closed it */
                }
                hm_close(aTHX_ loop, c);
                return;
            }
            {
                size_t ncap = c->rcap * 2;
                if (ncap > loop->max_read) ncap = loop->max_read;
                c->rbuf = (char *)hm_xrealloc(c->rbuf, ncap);
                c->rcap = ncap;
            }
        }
        size_t want = c->rcap - c->rlen;
        ssize_t n = hm_cread(c, c->rbuf + c->rlen, want);
        if (n > 0) {
            c->rlen += (size_t)n;
            c->last_rx = loop->now;
            c->body_rx += (UV)n;
            /* A tunnel (a 101 stream handle): the bytes are the handle's,
             * not a request. Handed up read by read, so the buffer never
             * grows toward the ceiling that ends an oversize request. The
             * callback may close the stream and with it the connection.
             * Only once SETTLED (1): at 2 the request line is still in
             * rbuf and the hm_process frame that opened the tunnel owns the
             * buffer, so the bytes wait for it (it hands them over itself). */
            if (c->tunnel == 1 && !hm_tunnel_bytes(aTHX_ c)) return;
            /* A SHORT read means the socket buffer is drained: the kernel
             * handed over everything it had, so the confirming read that
             * would follow can only return EAGAIN. Stopping here removes ONE
             * SYSCALL FROM EVERY REQUEST - measured, an ordinary keep-alive
             * request cost three (two reads and a writev) and the second read
             * was always the empty one.
             *
             * Safe because the backends are LEVEL-triggered - no EV_CLEAR, no
             * EPOLLET. Draining to EAGAIN is an edge-triggered obligation,
             * and under level triggering anything that arrived between the
             * read and this line simply reports the fd readable again on the
             * next turn of the loop. Nothing is lost, and nothing stalls.
             *
             * NOT for TLS: a short SSL_read says nothing about the socket,
             * because the engine may hold more plaintext than it just
             * returned, and stopping on it would leave that sitting in the
             * engine until the peer happened to send again. */
            if (!c->ssl && (size_t)n < want) {
                /* The disarm the EAGAIN branch below would have done. A write
                 * watcher left armed over an empty write buffer re-fires
                 * every turn and spins the worker at 100% CPU. */
                if (c->writing && c->woff == c->wlen) {
                    loop->be->remove_io(loop->be, c->fd, HM_EV_WRITE);
                    c->writing = 0;
                }
                break;
            }
        } else if (n == 0) {
            /* The peer half-closed a tunnel: the same thing a zero read
             * means to a socket owner, told to the handle as fin before
             * the connection goes and the abort callback says the rest. */
            if (c->tunnel) {
                (void)hm_stream_deliver(aTHX_ c->fd, c->id, -1, c->rbuf, 0, 1);
                if (loop->conns[c->fd] != c) return;
            }
            hm_close(aTHX_ loop, c);
            return;
        } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
            /* TLS read blocked wanting writable: arm a write watcher so a
             * write-readiness event re-drives us. Once it unblocks, drop that
             * watcher again unless there is queued output still waiting on
             * it. The backends are level-triggered, so a watcher left armed
             * over an empty write buffer is not merely untidy - it re-fires
             * every turn of the loop and spins the worker at 100% CPU. */
            if (c->tls_r_wants_w) {
                if (!c->writing) {
                    loop->be->add_io(loop->be, c->fd, HM_EV_WRITE, 0);
                    c->writing = 1;
                }
            } else if (c->writing && c->woff == c->wlen) {
                loop->be->remove_io(loop->be, c->fd, HM_EV_WRITE);
                c->writing = 0;
            }
            break;
        } else {
            hm_close(aTHX_ loop, c);
            return;
        }
    }
    hm_lru_touch(loop, c);
    /* A tunnel carries no requests. A connection whose synchronous handler
     * is on the stack (its ->get pumped this loop) keeps the new bytes in
     * rbuf for the hm_process frame already running on it: a second frame
     * would consume the pipeline from under the first one's offsets. */
    /* HTTP/2 is the exception once nghttp2 has returned: a handler waiting
     * for its body to drain is waiting for the window updates in these very
     * bytes, and hm_h2_input takes them without starting another request. */
    if (!c->tunnel && (!c->in_app || hm_h2_reentrant(c))) hm_process(aTHX_ c);
    if (loop->conns[c->fd] == c) hm_flush(aTHX_ c);
}

static hm_tw *hm_add_timer_watch_c(hm_loop *loop, double secs,
                                   hm_abi_timer_cb cb, void *ud);
static void hm_tls_handshake(pTHX_ hm_conn *c);

/* The first bytes on a proxy_protocol listener. PEEK, so the header can be
 * parsed without taking anything behind it off the socket: a ClientHello or
 * an h2 preface in the same segment stays in the kernel for hm_tls_wrap and
 * hm_h2_detect, which then run exactly as on a listener without a proxy.
 * Only a complete header is consumed, by a read of exactly its length. A
 * partial one is re-peeked when the level-triggered watcher fires again;
 * one that never completes is closed by hm_sweep after header_timeout. */
/* A partial PROXY header: the bytes stay in the socket (peeked, not read)
 * and the watcher is level-triggered, so returning to the loop would wake
 * it again at once - a peer that sent "PROXY TCP4 1" and stopped had the
 * worker re-peeking at 100% CPU until header_timeout. Reading is paused
 * and resumed by this timer; the sweep still closes a header that never
 * completes. */
typedef struct { hm_loop *loop; int fd; UV id; } hm_pp_retry;
static void hm_proxy_retry(pTHX_ void *ud) {
    hm_pp_retry *r = (hm_pp_retry *)ud;
    hm_conn *c = (r->fd >= 0 && r->fd < HM_MAXFD) ? r->loop->conns[r->fd] : NULL;
    PERL_UNUSED_CONTEXT;
    if (c && c->id == r->id && c->pp_wait) hm_read_resume(r->loop, c);
    free(r);
}

static void hm_proxy_readable(pTHX_ hm_conn *c) {
    hm_loop *loop = c->loop;
    unsigned char buf[HM_PP_MAX];
    hm_pp_result r;
    ssize_t n;
    long len;
    n = hm_os_recv_peek(c->fd, buf, sizeof(buf));
    if (n == 0) { hm_close(aTHX_ loop, c); return; }
    if (n < 0) {
        if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) return;
        hm_close(aTHX_ loop, c); return;
    }
    len = hm_pp_parse(buf, (size_t)n, &r);
    if (len == 0) {                                     /* more to come */
        hm_pp_retry *rt;
        if ((size_t)n >= sizeof(buf)) { hm_close(aTHX_ loop, c); return; }
        rt = (hm_pp_retry *)hm_xmalloc(sizeof(*rt));
        rt->loop = loop; rt->fd = c->fd; rt->id = c->id;
        hm_read_pause(loop, c);
        if (!hm_add_timer_watch_c(loop, 0.05, hm_proxy_retry, rt)) {
            free(rt);                 /* no timer: level-triggered it is */
            hm_read_resume(loop, c);
        }
        return;
    }
    if (len < 0) { hm_close(aTHX_ loop, c); return; }   /* not a PROXY header */
    /* take exactly the header; what follows is the client's first bytes */
    {
        ssize_t got = hm_os_recv(c->fd, buf, (size_t)len);
        if (got != len) { hm_close(aTHX_ loop, c); return; }
    }
    c->pp_wait = 0;
    c->req_start = 0;
    /* The proxy's own address is kept for $env; the client's replaces the
     * peer, so REMOTE_ADDR, the access log, the rate limiter and the
     * denylist all see the client from here on. */
    memcpy(c->proxy_addr, c->peer, sizeof(c->proxy_addr));
    c->proxy_port = c->peer_port;
    if (r.family) {
        static const unsigned char v4map[12] =
            { 0,0,0,0, 0,0,0,0, 0,0,0xff,0xff };
        c->peer[0] = '\0';
        /* ::ffff:a.b.c.d is the IPv4 client it names (see hm_fmt_peer) */
        if (r.family == AF_INET6 && memcmp(r.src, v4map, 12) == 0)
            (void)inet_ntop(AF_INET, (const unsigned char *)r.src + 12,
                            c->peer, (socklen_t)sizeof(c->peer));
        else
            (void)inet_ntop(r.family, r.src, c->peer, (socklen_t)sizeof(c->peer));
        c->peer_port = r.src_port;
        /* the denylist, again, now for the real client; the accept-time
         * check saw the proxy */
        if (hm_sa_deny_check(c->peer)) {
            loop->denied++;
            hm_close(aTHX_ loop, c);
            return;
        }
    }
    /* distinct clients counts clients, not proxies, so the accept path
     * skipped it on this listener */
    if (hm_sa_hll && c->peer[0]) hm_sa_hll_add(c->peer);
    if (c->lst && c->lst->tls_ctx) {
        hm_tls_wrap(c, c->lst->tls_ctx);
        hm_tls_handshake(aTHX_ c);
    } else {
        hm_readable(aTHX_ c);
    }
}

/* accept(2) failed for want of a descriptor. The listener is level-readable
 * and stays so, so simply returning would wake the loop again at once and
 * spin the worker at 100% CPU until the table had room. Reclaim an idle
 * keep-alive connection when there is one; otherwise stop listening for a
 * tenth of a second and come back. */
/* Listeners whose starvation timer could not be armed (hm_accept_starved):
 * re-added from the top of a loop turn. */
static void hm_accept_unstarve(hm_loop *loop) {
    int i;
    loop->starved_retry = 0;
    for (i = 0; i < loop->nlisteners; i++) {
        hm_listener *l = &loop->listeners[i];
        if (l->starved != 2) continue;
        l->starved = 0;
        if (!loop->stopping && l->fd >= 0)
            loop->be->add_io(loop->be, l->fd, HM_EV_READ, 0);
    }
}

typedef struct { hm_loop *loop; hm_listener *lst; } hm_starve;
static void hm_accept_rearm(pTHX_ void *ud) {
    hm_starve *s = (hm_starve *)ud;
    s->lst->starved = 0;
    if (!s->loop->stopping && s->lst->fd >= 0)
        s->loop->be->add_io(s->loop->be, s->lst->fd, HM_EV_READ, 0);
    free(s);
}
static void hm_accept_starved(pTHX_ hm_loop *loop, hm_listener *lst) {
    hm_conn *c = loop->lru_head;           /* oldest activity first */
    while (c) {
        if (!c->awaiting && !c->in_app && !c->h2 && !c->tunnel && !c->tls_hs
            && c->rlen == 0 && c->wlen == c->woff && !c->bsrc.kind
            && c->nreqs > 0) {
            hm_close(aTHX_ loop, c);
            return;
        }
        c = c->lru_next;
    }
    if (lst->starved) return;
    {
        hm_starve *s = (hm_starve *)hm_xmalloc(sizeof(*s));
        s->loop = loop;
        s->lst  = lst;
        lst->starved = 1;
        loop->be->remove_io(loop->be, lst->fd, HM_EV_READ);
        if (!hm_add_timer_watch_c(loop, 0.1, hm_accept_rearm, s)) {
            /* No timer to be had: on epoll a timer IS a descriptor, and
             * this is the descriptor famine. Re-added from the top of a
             * later loop turn instead (hm_accept_unstarve) - the next close
             * wakes the loop, and the interval sweep does within five
             * seconds regardless. Before this the listener stayed removed
             * and the worker never accepted again. */
            free(s);
            lst->starved = 2;
            loop->starved_retry = 1;
        }
    }
}

static void hm_accept(pTHX_ hm_loop *loop, hm_listener *lst) {
    int batch = 0;
    if (loop->stopping) return;
    for (;;) {
        struct sockaddr_storage ss;
        socklen_t slen = sizeof(ss);
        hm_conn *c;
        char peer[INET6_ADDRSTRLEN];
        int  peer_port;
#ifdef HM_HAVE_ACCEPT4
        /* Non-blocking and close-on-exec in the accept itself: two fewer
         * syscalls per connection, and no window in which a fork from a
         * handler inherits a client socket. */
        int fd = accept4(lst->fd, (struct sockaddr *)&ss, &slen,
                         SOCK_NONBLOCK | SOCK_CLOEXEC);
#else
        int fd = hm_os_accept(lst->fd, (struct sockaddr *)&ss, &slen);
#endif
        if (fd < 0) {                          /* EAGAIN: drained */
            if (errno == EMFILE || errno == ENFILE)
                hm_accept_starved(aTHX_ loop, lst);
            break;
        }
        if (fd >= HM_MAXFD) { hm_os_close(fd); continue; }
#if !defined(HM_HAVE_ACCEPT4) && !defined(_WIN32)
        /* Not into anything the application execs: a child that kept a
         * client socket open held the client past the response, and could
         * write onto it (onto a TLS socket, raw). hm_new_conn sets
         * non-blocking. */
        (void)fcntl(fd, F_SETFD, fcntl(fd, F_GETFD, 0) | FD_CLOEXEC);
#endif
        /* Denylist: format the peer and drop a blocked IP HERE, before a
         * connection object is allocated or a byte is read - the cheapest
         * possible rejection. Fails open (no arena -> deny_check is 0). */
        hm_fmt_peer(peer, sizeof(peer), &peer_port, (struct sockaddr *)&ss);
        if (hm_sa_deny_check(peer)) { hm_os_close(fd); loop->denied++; continue; }
        /* Distinct clients, opt-in: with the sketch off this is one load of
         * a NULL, with it on a hash and a CAS-max on the peer string. */
        if (hm_sa_hll && peer[0] && !lst->proxy_protocol) hm_sa_hll_add(peer);
        c = hm_new_conn(loop, fd, lst);
        memcpy(c->peer, peer, sizeof(c->peer));
        c->peer_port = peer_port;
        loop->accepts++;
        if (c->tls_failed) { hm_close(aTHX_ loop, c); continue; }
        /* fairness: don't grab the whole queue in one wakeup; the listener
         * stays level-readable, so the next iteration resumes */
        if (++batch >= 64) break;
    }
}

/* Drive the TLS handshake for a connection still in SSL_accept. On success,
 * switch to HTTP/2 if ALPN negotiated h2, then start serving. WANT_READ waits
 * on the always-armed read watcher; WANT_WRITE arms a write watcher. */
static void hm_tls_handshake(pTHX_ hm_conn *c) {
#ifdef HM_HAVE_OPENSSL
    hm_loop *loop = c->loop;
    SSL *ssl = (SSL *)c->ssl;
    int r;
    ERR_clear_error();
    r = SSL_accept(ssl);
    if (r == 1) {
        c->tls_hs = 0;
        hm_tls_capture_peer(c);       /* protocol/cipher + any client cert */
        /* What the kernel took, for stats and tests. The write paths do NOT
         * read this - they ask OpenSSL again each time, because the answer
         * can change under them (see hm_ktls_tx). */
        c->ktls_tx = hm_ktls_tx(c) ? 1 : 0;
        if (c->writing) {
            loop->be->remove_io(loop->be, c->fd, HM_EV_WRITE);
            c->writing = 0;
        }
#ifdef HM_HAVE_NGHTTP2
        if (c->lst && c->lst->http2) {
            const unsigned char *proto = NULL;
            unsigned int plen = 0;
            SSL_get0_alpn_selected(ssl, &proto, &plen);
            if (plen == 2 && memcmp(proto, "h2", 2) == 0
                && hm_h2_start(aTHX_ c) < 0) {
                hm_close(aTHX_ loop, c);
                return;
            }
        }
#endif
        hm_readable(aTHX_ c);   /* any early app data is already buffered */
        return;
    }
    {
        int e = SSL_get_error(ssl, r);
        if (e == SSL_ERROR_WANT_READ) return;   /* read watcher armed */
        if (e == SSL_ERROR_WANT_WRITE) {
            if (!c->writing) {
                loop->be->add_io(loop->be, c->fd, HM_EV_WRITE, 0);
                c->writing = 1;
            }
            return;
        }
        hm_close(aTHX_ loop, c);   /* handshake failed */
    }
#else
    (void)c;
#endif
}

/* ---- timeouts, shutdown, loop -------------------------------------------- */

/* Resolve an app io-readiness Future (fd became readable/writable), or run
 * a persistent watch_io callback. */
static void hm_io_event(pTHX_ hm_loop *loop, int fd, int mask, hm_iow *slot) {
    SV *sv = slot->sv;
    PERL_UNUSED_VAR(loop);
    if (slot->c_cb) { slot->c_cb(aTHX_ fd, mask, slot->c_ud); return; }
    if (!sv) return;
    if (slot->is_cb) {
        dSP;
        ENTER; SAVETMPS; PUSHMARK(SP);
        PUTBACK;
        call_sv(sv, G_DISCARD | G_EVAL);
        hm_warn_died(aTHX_ "watch_io callback");
        FREETMPS; LEAVE;
    } else {
        slot->sv = NULL;                    /* one-shot */
        hmf_done0(aTHX_ sv);
        SvREFCNT_dec(sv);
    }
}

/* Timeout sweep: close idle keep-alive connections and slow/partial-request
 * (slowloris) connections. Runs on a periodic backend timer. */
static void hm_sweep(pTHX_ hm_loop *loop) {
    hm_conn *c = loop->lru_head;
    time_t now = time(NULL);
    loop->now = now;
    while (c) {
        hm_conn *next = c->lru_next;
        /* in_app: a synchronous handler is running for it (and pumping
         * this loop); its request is in progress, not idle */
        if (!c->awaiting && !c->in_app) {
            double idle = difftime(now, c->last_active);
            /* Two guards for two phases. The header block is timed from the
             * request's first byte, which is the slowloris guard. Once the
             * headers are complete and a body is arriving, the clock is
             * progress: a body that has not delivered a byte for
             * body_timeout is dead, however long it has been coming - and
             * an upload slower than max_body in header_timeout is not. */
            int slow = c->req_start &&
                       (c->body_phase
                        ? difftime(now, c->last_rx)   > loop->body_timeout
                        : difftime(now, c->req_start) > loop->header_timeout);
            /* Two OPT-IN floors under the progress clock, for a deployment
             * that cannot afford a body dribbled a byte a minute for ever:
             * a minimum average rate over the body phase (judged after two
             * seconds, so a first slow segment is not a verdict), and a
             * ceiling on the whole request from its first byte. */
            if (!slow && c->req_start && c->body_phase && loop->body_min_rate) {
                double span = difftime(now, c->body_t0);
                if (span >= 2.0
                    && (double)c->body_rx / span < (double)loop->body_min_rate)
                    slow = 1;
            }
            if (!slow && c->req_start && loop->request_timeout > 0
                && difftime(now, c->req_start) > loop->request_timeout)
                slow = 1;
            if (idle > loop->idle_timeout || slow)
                hm_close(aTHX_ loop, c);
        }
        c = next;
    }
}

/* Graceful shutdown: stop accepting, close idle (served) connections,
 * finish in-flight work bounded by shutdown_grace; responses during
 * shutdown get Connection: close. */
static void hm_on_signal(pTHX_ hm_loop *loop) {
    hm_conn *c;
    if (loop->stopping) { loop->stop = 1; return; }   /* second signal: hard */
    loop->stopping = 1;
    {
        int i;
        for (i = 0; i < loop->nlisteners; i++) {
            if (loop->listeners[i].fd >= 0)
                loop->be->remove_io(loop->be, loop->listeners[i].fd, HM_EV_READ);
            /* Stop taking datagrams too. A QUIC connection already up is
             * not on this fd in any fd-indexed sense, so nothing else here
             * reaches it; draining those needs a graceful shutdown that
             * sends CONNECTION_CLOSE, which is not implemented. */
            if (loop->listeners[i].udp_fd >= 0)
                loop->be->remove_io(loop->be, loop->listeners[i].udp_fd, HM_EV_READ);
        }
    }
    hm_quic_shutdown(aTHX_ loop);
    c = loop->lru_head;
    while (c) {
        hm_conn *next = c->lru_next;
        if (!c->awaiting && !c->in_app && c->rlen == 0 && c->wlen == c->woff
            && c->nreqs > 0)
            hm_close(aTHX_ loop, c);
        c = next;
    }
    if (loop->nconns == 0) { loop->stop = 1; return; }
    if (!loop->hardstop_tw) {
        loop->hardstop_tw = (hm_tw *)hm_xcalloc(1, sizeof(hm_tw));
        loop->hardstop_tw->kind = HM_TW_HARDSTOP;
        loop->hardstop_tw->loop = loop;
        loop->hardstop_tw->due  = hm_tw_due(loop->shutdown_grace);
        if (loop->be->add_timer(loop->be, loop->shutdown_grace, 1, loop->hardstop_tw) < 0)
            hm_errlog("Hyperman: cannot arm the shutdown_grace timer: %s\n", strerror(errno));
    }
}

/* Drain a snapshot of the deferred (run-soon) queue. */
static void hm_run_deferred(pTHX_ hm_loop *loop) {
    SSize_t n = av_len(loop->deferred) + 1;
    SSize_t i;
    if (!n) return;
    ENTER; SAVETMPS;
    for (i = 0; i < n; i++) {
        SV *cb = sv_2mortal(av_shift(loop->deferred));
        dSP;
        PUSHMARK(SP);
        PUTBACK;
        call_sv(cb, G_DISCARD | G_EVAL);
        hm_warn_died(aTHX_ "deferred callback");
    }
    FREETMPS; LEAVE;
}

/* Re-process connections that hit the fairness cap with requests still
 * buffered, now that everyone else has been serviced. */
static void hm_run_again(pTHX_ hm_loop *loop) {
    int n = loop->again_n;
    int i;
    if (!n) return;
    loop->again_n = 0;
    for (i = 0; i < n; i++) {
        hm_conn *c = loop->conns[loop->again[i].fd];
        if (c && c->id == loop->again[i].id) c->again_queued = 0;
        if (c && c->id == loop->again[i].id && !c->awaiting
            && (!c->in_app || hm_h2_reentrant(c)) && !c->bsrc.kind) {
            hm_process(aTHX_ c);
            if (loop->conns[c->fd] == c) {
                hm_flush(aTHX_ c);
                /* THE re-arm point for completion mode, and the reason
                 * anything that wants a read armed calls hm_again_push
                 * rather than arming for itself: this runs at the top of a
                 * loop turn, so hm_process is not on the stack and growing
                 * the read buffer cannot pull the ground out from under the
                 * pointers it holds into it. */
                if (loop->conns[c->fd] == c) hm_rearm_read(loop, c);
            }
        }
    }
}

static void hm_dispatch(pTHX_ hm_loop *loop, hm_event *ev) {
    switch (ev->kind) {
    case HM_EV_TIMER: {
        hm_tw *tw = (hm_tw *)ev->udata;
        if (!tw || tw->dead) return;      /* cancelled earlier in this batch */
        if (tw->kind == HM_TW_SWEEP) { hm_sweep(aTHX_ loop); return; }
        /* A oneshot delivered before its deadline (NetBSD's kqueue timed a
         * 300ms Fetch request out at 246ms on a smoker) is re-armed for the
         * remainder and not fired. The backend has already dropped it, so the
         * same watcher is free to re-add; each re-arm is shorter, so a
         * backend that is always early still converges. Within a
         * millisecond is the backends' own rounding. */
        if (tw->due > 0) {
            double left = tw->due - (double)hm_now_ns() / 1e9;
            /* could not re-arm (no descriptor for a timerfd): early beats
             * never, so it fires now */
            if (left > 1e-3 && loop->be->add_timer(loop->be, left, 1, tw) == 0)
                return;
        }
        if (tw->kind == HM_TW_HARDSTOP) { loop->stop = 1; return; }
        if (tw->kind == HM_TW_C) {          /* ABI timer: no Perl frame */
            hm_abi_timer_cb cb = tw->c_cb;
            void *ud = tw->c_ud;
            free(tw);
            cb(aTHX_ ud);
            return;
        }
        {
            SV *sv = tw->sv;
            int kind = tw->kind;
            free(tw);
            if (kind == HM_TW_FUTURE) {
                hmf_done0(aTHX_ sv);
            } else {
                dSP;
                ENTER; SAVETMPS; PUSHMARK(SP);
                PUTBACK;
                call_sv(sv, G_DISCARD | G_EVAL);
                hm_warn_died(aTHX_ "timer callback");
                FREETMPS; LEAVE;
            }
            SvREFCNT_dec(sv);
        }
        return;
    }
    case HM_EV_SIGNAL:
        if (ev->fd == SIGUSR1) {
            /* With a scoreboard the supervisor prints every row itself and
             * never forwards USR1; this line is the no-arena fallback. */
            if (!hm_sa_board_live())
                hm_errlog("Hyperman worker %d: requests=%lu accepts=%lu conns=%d "
                    "bytes_out=%lu backend=%s%s\n",
                    hm_os_getpid(),
                    (unsigned long)loop->requests, (unsigned long)loop->accepts,
                    loop->nconns, (unsigned long)loop->bytes_out,
                    loop->be->name, loop->stopping ? " (draining)" : "");
            return;
        }
        hm_on_signal(aTHX_ loop);
        return;
    case HM_EV_RECV: {
        /* A read the core asked for has finished; the bytes are already in
         * the buffer it supplied. */
        int fd = ev->fd;
        hm_conn *c = (fd >= 0 && fd < HM_MAXFD) ? loop->conns[fd] : NULL;

        /* Is this OUR read? A completion can outlive its connection - the fd
         * may have been closed and reused since - and the token is the only
         * thing that can tell the difference. Anything that does not match
         * belongs to a connection that is gone, and the buffer it was reading
         * into is parked waiting for exactly this moment. */
        if (!c || !c->recv_inflight || HM_RECV_TOKEN(c) != ev->token) {
            hm_quar_release(loop, ev->token);
            return;
        }
        c->recv_inflight = 0;
        if (ev->res <= 0) {   /* EOF, error, or the cancel from a close */
            /* The peer half-closed a tunnel: fin to the handle first, as
             * hm_readable's zero read does, then the close says the rest. */
            if (c->tunnel && ev->res == 0) {
                (void)hm_stream_deliver(aTHX_ c->fd, c->id, -1, c->rbuf, 0, 1);
                if (loop->conns[fd] != c) return;
            }
            hm_close(aTHX_ loop, c);
            return;
        }
        c->rlen += (size_t)ev->res;
        c->last_rx = loop->now;
        c->body_rx += (UV)ev->res;
        c->last_active = loop->now;
        hm_lru_touch(loop, c);
        /* A tunnel carries no requests: its bytes go to the handle. The
         * parser would hold them as an unfinished request line forever. */
        if (c->tunnel == 1) { if (!hm_tunnel_bytes(aTHX_ c)) return; }
        /* The same rule hm_readable applies: a synchronous handler's frame
         * is on the stack for this connection (its ->get pumped the loop,
         * and this read was in flight when it was handed the pipeline), or
         * a tunnel was opened inside it and the request is still in rbuf.
         * That frame owns the buffer. The bytes wait in it, nothing is
         * dispatched or re-armed from under it, and whoever called that
         * frame re-arms when it returns (hm_deliver's hm_again_push, or the
         * re-arm below in the frame that called hm_process). Without this a
         * second hm_process re-parsed the request at rbuf[0], called the
         * application twice, and left the outer frame to memmove with a
         * consumed count larger than what remained. */
        else if (c->tunnel == 2 || (c->in_app && !hm_h2_reentrant(c))) return;
        else hm_process(aTHX_ c);
        if (loop->conns[fd] != c) return;      /* dispatch closed it */
        hm_flush(aTHX_ c);
        if (loop->conns[fd] != c) return;      /* flush closed it */
        /* Re-armed even while PARKED or streaming, and that is not an
         * optimisation - it is how a disconnect is noticed at all.
         *
         * A readiness backend keeps a persistent watcher on every connection,
         * so a client that walks away from a parked async request wakes the
         * loop with EOF and the response Future gets cancelled. Completion
         * mode has no such watcher: with no read outstanding the close is
         * simply never seen, the Future is never cancelled, and the
         * connection sits there until the idle sweep. t/13-async-psgi.t
         * catches exactly this.
         *
         * Safe because the request's bytes are gone from the buffer before it
         * parks - hm_process consumes them, and hm_new_input COPIES the body
         * into an SV - so nothing points into rbuf across a park, and the
         * kernel only ever writes past rlen. */
        hm_rearm_read(loop, c);
        return;
    }
    case HM_EV_READ: {
        int fd = ev->fd;
        hm_conn *c;
        hm_listener *lst;
        if ((lst = hm_listener_for_fd(loop, fd))) { hm_accept(aTHX_ loop, lst); return; }
        /* Between the two, because a UDP listener fd is never in conns[]:
         * QUIC multiplexes every connection over the one socket, so there is
         * nothing fd-indexed to find. */
        if ((lst = hm_listener_for_udp(loop, fd))) {
            hm_quic_readable(aTHX_ loop, lst);
            return;
        }
        if ((c = loop->conns[fd])) {
            if (c->pp_wait)             hm_proxy_readable(aTHX_ c);
            else if (c->tls_hs)         hm_tls_handshake(aTHX_ c);
            else                        hm_readable(aTHX_ c);
        }
        else if (loop->io_r[fd].sv || loop->io_r[fd].c_cb)
            hm_io_event(aTHX_ loop, fd, HM_EV_READ, &loop->io_r[fd]);
        return;
    }
    case HM_EV_WRITE: {
        int fd = ev->fd;
        hm_conn *c;
        if ((c = loop->conns[fd])) {
            /* TLS post-handshake: when the write watcher is armed because
             * SSL_read wanted a writable socket, route through hm_readable so
             * the blocked read is re-driven (it flushes pending output at the
             * end). Otherwise this is ordinary write backpressure and the
             * read side is not blocked at all - going through hm_readable
             * then costs an SSL_read that can only return EAGAIN, once per
             * writable wakeup for the whole of a large response. */
            if (c->tls_hs)                       hm_tls_handshake(aTHX_ c);
            else if (c->ssl && c->tls_r_wants_w) hm_readable(aTHX_ c);
            else                                 hm_flush(aTHX_ c);
        }
        else if (loop->io_w[fd].sv || loop->io_w[fd].c_cb)
            hm_io_event(aTHX_ loop, fd, HM_EV_WRITE, &loop->io_w[fd]);
        return;
    }
    }
}

/* Run the loop until ->stop, or (when awaiting) until $until is ready.
 * Re-entrant: ->get/->await inside a handler pumps this same loop, so the
 * worker keeps serving other connections while awaiting. */
static void hm_loop_run(pTHX_ hm_loop *loop, SV *until) {
    hm_loop *prev = hm_cur_loop;
    hm_event evs[HM_MAXEV];
    /* The fire queue is pumped by whoever enqueues, unless a pump is
     * already on the stack (hm_fq_active), which makes a continuation that
     * enqueues from inside a callback leave its work for the pump above
     * it. That pump is above THIS frame too when a handler reached through
     * the queue (a parked response delivering the next pipelined request)
     * calls ->get: the nested run here must pump for itself, or every
     * continuation it waits on - a ->then, a combinator - sits in the queue
     * until it returns, which is never. Restored on the way out; a croak
     * out of here leaves it 0, which only lets the outer pump be nested
     * once, and nesting a pump is safe (they share one queue). */
    int fq_was = hm_fq_active;
    hm_fq_active = 0;
    hm_cur_loop = loop;
    loop->running++;
    /* Cleared for the OUTERMOST run only. A nested run is a handler's ->get
     * pumping this loop, and a stop asked for while it runs - the second
     * TERM, the shutdown_grace hard-stop, the last connection closing while
     * draining - is the outer run's to act on: the nested one breaks and
     * leaves the flag set. Clearing it on exit, as this once did, swallowed
     * every stop that landed under a handler, and the hard-stop timer is
     * one-shot so the worker then never left. */
    if (loop->running == 1) loop->stop = 0;
    for (;;) {
        double timeout = -1.0;
        int n, i;
        loop->now = time(NULL);
        if (loop->recycle_pending && !loop->stopping) {
            loop->recycle_pending = 0;
            hm_on_signal(aTHX_ loop);   /* max_requests recycle: drain + exit */
        }
        hm_run_deferred(aTHX_ loop);
        hm_run_again(aTHX_ loop);
        if (loop->starved_retry) hm_accept_unstarve(loop);
        if (loop->stop) break;
        if (until && hmf_state(aTHX_ until) != HMF_PENDING) break;
        if (av_len(loop->deferred) >= 0 || loop->again_n) timeout = 0.0;
        /* QUIC's deadlines do not live in the backend's timer list - see the
         * costing in hm_quic.h - so the wait is clamped to the nearest one
         * here instead. Rounding UP is correct: ngtcp2 copes with a late
         * expiry and not with an early one, and the backends already round
         * to milliseconds. One compare when there is no QUIC listener. */
        if (loop->qheap.n) {
            /* Through a function rather than reaching into the heap here:
             * hm_qconn is an opaque forward declaration on a build without
             * QUIC, and dereferencing it would compile only where the
             * feature is present - which is not where the mistake shows up. */
            uint64_t now_ns = hm_now_ns();
            uint64_t due    = hm_quic_next_deadline(loop);
            if (due != (uint64_t)-1) {
                double secs = due <= now_ns ? 0.0
                                            : (double)(due - now_ns) / 1e9;
                if (timeout < 0.0 || secs < timeout) timeout = secs;
            }
        }
        n = loop->be->wait(loop->be, evs, HM_MAXEV, timeout);
        if (n < 0) { if (errno == EINTR) continue; break; }
        for (i = 0; i < n; i++) hm_dispatch(aTHX_ loop, &evs[i]);
        if (loop->tw_gy && loop->running == 1) hm_tw_gy_sweep(loop);
        if (loop->qheap.n) hm_quic_expire(aTHX_ loop);
        if (loop->log_fd >= 0 && loop->log_len) hm_log_flush(loop);
        if (loop->stop) break;
        if (until && hmf_state(aTHX_ until) != HMF_PENDING) break;
    }
    if (loop->tw_gy && loop->running == 1) hm_tw_gy_sweep(loop);
    loop->running--;
    hm_cur_loop = prev;
    hm_fq_active = fq_was;
}

/* ---- loop lifecycle ------------------------------------------------------- */

static hm_loop *hm_loop_new(pTHX_ const char *backend_name) {
    hm_loop *loop = (hm_loop *)hm_xcalloc(1, sizeof(hm_loop));
    if (!loop) return NULL;
    if (!backend_name || !*backend_name)
        backend_name = getenv("HYPERMAN_BACKEND");
    loop->be = hm_backend_create(backend_name);
    if (!loop->be) { free(loop); return NULL; }
    loop->pid = hm_os_getpid();
    loop->conns = (hm_conn **)hm_xcalloc(HM_MAXFD, sizeof(hm_conn *));
    loop->io_r  = (hm_iow *)hm_xcalloc(HM_MAXFD, sizeof(hm_iow));
    loop->io_w  = (hm_iow *)hm_xcalloc(HM_MAXFD, sizeof(hm_iow));
    loop->deferred = newAV();
    loop->log_fd = -1;
    loop->now = time(NULL);
    loop->idle_timeout   = 60.0;
    loop->header_timeout = 30.0;
    loop->body_timeout   = 60.0;
    loop->max_pipeline   = 32;
    loop->max_read       = 16 * 1024 * 1024;   /* request ceiling */
    loop->compress       = 0;                  /* off by default */
    loop->compress_min   = HZ_MIN_LENGTH;
    loop->compress_level = HZ_LEVEL;
    loop->shutdown_grace = 30.0;
    /* Completion-mode reads, on wherever the backend can do them.
     *
     * That is io_uring alone, and io_uring IS auto-selected wherever
     * liburing was found at build time (the chooser goes kqueue, io_uring,
     * epoll, poll), so on such a Linux box this is the default read path -
     * which is why every change to how bytes ARRIVE lands on both the
     * readiness and the completion path (t/49 sweeps both).
     *
     * HYPERMAN_COMPLETION overrides, exactly as HYPERMAN_BACKEND overrides
     * the chooser; run(completion => ...) overrides both, being the most
     * specific thing the caller said. */
    loop->completion = loop->be->submit_recv ? 1 : 0;
    {
        const char *v = getenv("HYPERMAN_COMPLETION");
        if (v && *v)
            loop->completion =
                (*v != '0' && loop->be->submit_recv) ? 1 : 0;
    }
    return loop;
}

/* Free a loop.
 *
 * A loop belongs to the process that created it, and every DESCRIPTOR in it -
 * the backend's own (kqueue/epoll), the connection sockets, the log file -
 * belongs to that process too. A loop inherited across a fork is therefore
 * freed WITHOUT closing anything: the memory is ours to release, the fds are
 * not.
 *
 * Closing them is destructive, not merely untidy. kqueue descriptors do not
 * survive fork(2): the kernel invalidates the child's copy, so the number is
 * free and the child's very next kqueue() is handed it back - closing the
 * inherited loop then shuts the CHILD'S OWN queue and nothing it watches
 * fires again. The epoll and connection fds are the parent's, still live, and
 * closing our duplicates frees numbers the child immediately reuses. */
/* A loop inherited across a fork must never reach its backend. On Linux an
 * epoll INSTANCE is a shared kernel object: fork duplicates the fd but both
 * processes' epoll_ctl calls edit the SAME interest list, so a child
 * "cleaning up" inherited watchers silently deregisters the PARENT's fds -
 * the parent then waits on an empty set forever (the DBIx::Loop
 * disown-in-a-forked-child hang, Punk t/39). kqueue merely hands the child
 * a dead descriptor, which is why macOS never showed it. The copied
 * user-space tables are ours to update; the kernel object is not. */
#define HM_LOOP_INHERITED(loop) ((loop)->pid != hm_os_getpid())

static void hm_loop_free(pTHX_ hm_loop *loop) {
    int i;
    int owned;
    if (!loop) return;
    owned = (loop->pid == hm_os_getpid());
    /* Read buffers still parked for completions that will now never be
     * reaped. The ring is torn down with the backend below, so nothing can
     * be writing into them by the time this returns. */
    hm_quar_free_all(loop);
    hm_tw_gy_sweep(loop);
    for (i = 0; i < HM_MAXFD; i++) {
        if (loop->conns[i]) {
            hm_conn *c = loop->conns[i];
            if (owned) hm_os_close(c->fd);
            if (c->h2)     hm_h2_free(aTHX_ c->h2);
            if (c->io_sv)  SvREFCNT_dec(c->io_sv);
            if (c->resp_f) SvREFCNT_dec(c->resp_f);
            if (c->env_sv) SvREFCNT_dec(c->env_sv);
            if (c->peer_sv)      SvREFCNT_dec(c->peer_sv);
            if (c->peer_host_sv) SvREFCNT_dec(c->peer_host_sv);
            if (c->peer_port_sv) SvREFCNT_dec(c->peer_port_sv);
            if (c->sname_sv)     SvREFCNT_dec(c->sname_sv);
            if (c->sport_sv)     SvREFCNT_dec(c->sport_sv);
            if (c->wbuf) free(c->wbuf);
            if (c->rbuf) free(c->rbuf);
            free(c);
            loop->conns[i] = NULL;
        }
        if (loop->io_r[i].sv) SvREFCNT_dec(loop->io_r[i].sv);
        if (loop->io_w[i].sv) SvREFCNT_dec(loop->io_w[i].sv);
    }
    while (loop->pool) {
        hm_conn *c = loop->pool;
        loop->pool = c->pool_next;
        if (c->wbuf) free(c->wbuf);
        if (c->rbuf) free(c->rbuf);
        free(c);
    }
    if (loop->log_fd >= 0 && owned) { hm_log_flush(loop); hm_os_close(loop->log_fd); }
    if (loop->log_buf) free(loop->log_buf);
    for (i = 0; i < loop->nlisteners; i++) {
        /* Before the fds: freeing a connection writes a CONNECTION_CLOSE. */
        if (owned) hm_quic_srv_free(aTHX_ &loop->listeners[i]);
        if (loop->listeners[i].host)   free(loop->listeners[i].host);
        if (loop->listeners[i].altsvc) free(loop->listeners[i].altsvc);
        if (loop->listeners[i].tls_sni) SvREFCNT_dec(loop->listeners[i].tls_sni);
        /* `owned` for the same reason every other descriptor here is: a loop
         * belonging to another process holds fd NUMBERS that are ours, and
         * closing them would close whatever this process has since put
         * there. An inherited UDP listener is the third case of that, after
         * the backend descriptor and the connections. */
        if (loop->listeners[i].udp_fd >= 0 && owned)
            hm_os_close(loop->listeners[i].udp_fd);
    }
    if (loop->listeners) free(loop->listeners);
    /* A weak copy of the wrapper (hm_loop_to_sv): dropping it frees the RV
     * alone and runs no DESTROY. */
    if (loop->self_sv) {
        SV *self = loop->self_sv;
        loop->self_sv    = NULL;
        loop->perl_owned = 0;
        SvREFCNT_dec(self);
    }
    if (loop->log_cb)  SvREFCNT_dec(loop->log_cb);
    if (loop->app) SvREFCNT_dec(loop->app);
    SvREFCNT_dec((SV *)loop->deferred);
    if (loop->again) free(loop->again);
    if (loop->hidx) free(loop->hidx);
    if (loop->sweep_tw) free(loop->sweep_tw);
    if (loop->hardstop_tw) free(loop->hardstop_tw);
    if (loop->qheap.a) free(loop->qheap.a);
    /* destroy() releases the backend's memory either way; `foreign` is what
     * stops it closing descriptors that are not ours - see the note above */
    if (!owned) loop->be->foreign = 1;
    loop->be->destroy(loop->be);
    free(loop->conns);
    free(loop->io_r);
    free(loop->io_w);
    free(loop);
}

/* Attach the PSGI server to a loop: listener, shutdown/stats signals via
 * the backend's signal watcher, and the timeout sweep timer. */
static void hm_attach_server(pTHX_ hm_loop *loop, SV *app) {
    int i;
    loop->app = SvREFCNT_inc(app);
    loop->attached = 1;
    for (i = 0; i < loop->nlisteners; i++) {
        loop->be->add_io(loop->be, loop->listeners[i].fd, HM_EV_READ, 0);
        if (loop->listeners[i].udp_fd >= 0) {
            /* After the fork, in the worker that will serve: the SSL_CTX and
             * the CID table belong to this process alone. */
            hm_quic_srv_attach(aTHX_ loop, &loop->listeners[i], i);
            loop->be->add_io(loop->be, loop->listeners[i].udp_fd, HM_EV_READ, 0);
        }
    }
#ifndef _WIN32
    /* Ignore the default dispositions: these arrive through the backend's
     * signal watcher instead. Windows has no signal to ignore - the
     * WSAPoll backend's console handler is the only source there. */
    signal(SIGPIPE, SIG_IGN);
    signal(SIGTERM, SIG_IGN);
    signal(SIGINT,  SIG_IGN);
    signal(SIGUSR1, SIG_IGN);
#endif
    loop->be->add_signal(loop->be, SIGTERM);
    loop->be->add_signal(loop->be, SIGINT);
    loop->be->add_signal(loop->be, SIGUSR1);
    loop->sweep_tw = (hm_tw *)hm_xcalloc(1, sizeof(hm_tw));
    loop->sweep_tw->kind = HM_TW_SWEEP;
    loop->sweep_tw->loop = loop;
    if (loop->be->add_timer(loop->be, HM_SWEEP_SECS, 0, loop->sweep_tw) < 0)
        hm_errlog("Hyperman: cannot arm the timeout sweep: %s\n", strerror(errno));
}

/* register a timer watcher resolving a Future / running a callback */
static void hm_add_timer_watch(pTHX_ hm_loop *loop, double secs, SV *sv, int kind) {
    hm_tw *tw = (hm_tw *)hm_xcalloc(1, sizeof(hm_tw));
    tw->kind = kind;
    tw->sv   = SvREFCNT_inc(sv);
    tw->loop = loop;
    tw->due  = hm_tw_due(secs);
    /* A backend can refuse: on epoll every timer is a descriptor, so under
     * EMFILE there is none to be had. Silently dropping it left a ->get on
     * the timer's future pumping the loop for ever. */
    if (!HM_LOOP_INHERITED(loop) && loop->be->add_timer(loop->be, secs, 1, tw) < 0) {
        int e = errno;
        SvREFCNT_dec(sv);
        free(tw);
        croak("Hyperman: cannot arm a timer: %s", strerror(e));
    }
}

/* Let go of whatever a watcher slot held. A one-shot future that was still
 * pending is CANCELLED, not dropped: a `->get` waiting on it would otherwise
 * pump the loop for a readiness nothing is watching for any more. */
static void hm_iow_release_sv(pTHX_ SV *sv, int is_cb) {
    if (!sv) return;
    if (!is_cb && hmf_is_future(aTHX_ sv) && hmf_state(aTHX_ sv) == HMF_PENDING)
        hmf_cancel(aTHX_ sv);
    SvREFCNT_dec(sv);
}

/* The slot is EMPTIED before the old occupant is released, and the new
 * occupant (where there is one) is seated before that too: releasing a
 * pending future fires its callbacks, and one of those may watch this same
 * fd again. With the release first, that inner registration landed in the
 * slot and was then overwritten by the outer one - a future that never
 * fired - or, on the remove path, was left in a slot whose kind had been
 * reset, so the next event settled a callback CV as if it were a future. */
static void hm_iow_release(pTHX_ hm_iow *slot) {
    SV *sv = slot->sv;
    int is_cb = slot->is_cb;
    if (!sv) return;
    slot->sv = NULL;
    hm_iow_release_sv(aTHX_ sv, is_cb);
}

static void hm_add_io_watch(pTHX_ hm_loop *loop, int fd, const char *mode,
                            SV *sv, int is_cb) {
    hm_iow *slot;
    SV *old;
    int old_cb;
    int mask;
    if (fd < 0 || fd >= HM_MAXFD) croak("Hyperman: bad fd %d", fd);
    if (mode[0] == 'w') { slot = &loop->io_w[fd]; mask = HM_EV_WRITE; }
    else                { slot = &loop->io_r[fd]; mask = HM_EV_READ;  }
    old    = slot->sv;
    old_cb = slot->is_cb;
    slot->sv    = SvREFCNT_inc(sv);
    slot->is_cb = (unsigned char)is_cb;
    slot->c_cb  = NULL;
    slot->c_ud  = NULL;
    if (!HM_LOOP_INHERITED(loop))
        loop->be->add_io(loop->be, fd, mask, is_cb ? 0 : 1);
    hm_iow_release_sv(aTHX_ old, old_cb);
}

/* ABI: persistent C watcher, replacing whatever held the slot. */
static void hm_add_io_watch_c(pTHX_ hm_loop *loop, int fd, int mask,
                              hm_abi_io_cb cb, void *ud) {
    hm_iow *slot;
    SV *old;
    int old_cb;
    if (fd < 0 || fd >= HM_MAXFD) croak("Hyperman: bad fd %d", fd);
    slot = (mask & HM_EV_WRITE) ? &loop->io_w[fd] : &loop->io_r[fd];
    old    = slot->sv;
    old_cb = slot->is_cb;
    slot->sv    = NULL;
    slot->is_cb = 0;
    slot->c_cb  = cb;
    slot->c_ud  = ud;
    if (!HM_LOOP_INHERITED(loop))
        loop->be->add_io(loop->be, fd,
                         (mask & HM_EV_WRITE) ? HM_EV_WRITE : HM_EV_READ, 0);
    hm_iow_release_sv(aTHX_ old, old_cb);
}

/* Drop one direction's watcher (any kind); idempotent. */
static void hm_del_io_watch(pTHX_ hm_loop *loop, int fd, int mask) {
    hm_iow *slot;
    SV *old;
    int old_cb;
    if (fd < 0 || fd >= HM_MAXFD) return;
    slot = (mask & HM_EV_WRITE) ? &loop->io_w[fd] : &loop->io_r[fd];
    if (!slot->sv && !slot->c_cb) return;
    if (!HM_LOOP_INHERITED(loop))
        loop->be->remove_io(loop->be, fd,
                            (mask & HM_EV_WRITE) ? HM_EV_WRITE : HM_EV_READ);
    old    = slot->sv;
    old_cb = slot->is_cb;
    slot->sv    = NULL;
    slot->is_cb = 0;
    slot->c_cb  = NULL;
    slot->c_ud  = NULL;
    hm_iow_release_sv(aTHX_ old, old_cb);
}

/* ABI: one-shot C timer; the returned watcher doubles as the cancel handle
 * (freed on fire or on cancel, never both - single-threaded). */
static hm_tw *hm_add_timer_watch_c(hm_loop *loop, double secs,
                                   hm_abi_timer_cb cb, void *ud) {
    hm_tw *tw = (hm_tw *)hm_xcalloc(1, sizeof(hm_tw));
    tw->kind = HM_TW_C;
    tw->loop = loop;
    tw->c_cb = cb;
    tw->c_ud = ud;
    tw->due  = hm_tw_due(secs);
    if (loop->be->add_timer(loop->be, secs, 1, tw) < 0) {
        /* NULL, and a line in the log: the C callers (the scoreboard tick,
         * the bus poll, the accept re-arm) each have a fallback or a reason
         * to be loud, and an ABI consumer is told the handle is NULL. */
        hm_errlog("Hyperman: cannot arm a %.3fs timer: %s\n", secs, strerror(errno));
        free(tw);
        return NULL;
    }
    return tw;
}

static void hm_del_timer_watch(pTHX_ hm_loop *loop, hm_tw *tw) {
    if (!tw || tw->dead) return;
    if (!HM_LOOP_INHERITED(loop))
        loop->be->del_timer(loop->be, tw);
    if (tw->sv) { SvREFCNT_dec(tw->sv); tw->sv = NULL; }
    /* Not freed here: an event naming it may already be in the batch being
     * dispatched (see hm_tw.dead). hm_dispatch skips it; hm_tw_gy_sweep
     * frees it once the outermost loop's batch is done. */
    tw->dead = 1;
    tw->gy_next = loop->tw_gy;
    loop->tw_gy = tw;
}

/* Free the cancelled timers. Only between batches of the OUTERMOST loop
 * run: a nested run (a handler's ->get) sits inside one of the outer
 * batch's events, and the outer batch may still name any of them. */
static void hm_tw_gy_sweep(hm_loop *loop) {
    hm_tw *tw = loop->tw_gy, *n;
    loop->tw_gy = NULL;
    while (tw) { n = tw->gy_next; free(tw); tw = n; }
}

static hm_loop *hm_need_loop(void) {
    if (!hm_cur_loop)
        croak("Hyperman: no event loop is running in this process");
    return hm_cur_loop;
}

/* ---- listener + process model -------------------------------------------- */

/* Create, bind, and listen a non-blocking TCP socket. Returns fd or -1. */
/* Resolve a listen address ONCE, at startup: an IPv4 or IPv6 literal (with
 * or without brackets), a hostname, or nothing for every IPv4 interface.
 * getaddrinfo's first answer is taken. Failure is an error string for the
 * croak and never a silent fallback: a host that did not parse used to bind
 * INADDR_ANY, which exposed the service on every interface for a typo. */
static int hm_resolve(const char *host, int port, int socktype,
                      struct sockaddr_storage *out, socklen_t *outlen,
                      char *err, size_t errlen) {
    struct addrinfo hints, *res = NULL;
    char hbuf[256], pbuf[16];
    const char *h = host;
    int rc;
    if (!h || !*h) h = "0.0.0.0";
    if (h[0] == '[') {
        size_t l = strlen(h);
        if (l < 3 || h[l - 1] != ']' || l - 2 >= sizeof(hbuf)) {
            snprintf(err, errlen, "malformed bracketed address");
            return -1;
        }
        memcpy(hbuf, h + 1, l - 2);
        hbuf[l - 2] = '\0';
        h = hbuf;
    }
    memset(&hints, 0, sizeof(hints));
    hints.ai_family   = AF_UNSPEC;
    hints.ai_socktype = socktype;
    hints.ai_flags    = AI_NUMERICSERV | AI_PASSIVE;
    snprintf(pbuf, sizeof(pbuf), "%d", port);
    rc = getaddrinfo(h, pbuf, &hints, &res);
    if (rc != 0 || !res) {
        snprintf(err, errlen, "%s", rc ? gai_strerror(rc) : "no address");
        if (res) freeaddrinfo(res);
        return -1;
    }
    if (res->ai_addrlen > sizeof(*out)) { freeaddrinfo(res); snprintf(err, errlen, "address too long"); return -1; }
    memcpy(out, res->ai_addr, res->ai_addrlen);
    *outlen = (socklen_t)res->ai_addrlen;
    freeaddrinfo(res);
    return 0;
}

/* A unix-domain address. sun_path is 104 bytes on macOS and 108 on Linux,
 * and a path that does not fit is refused rather than truncated. */
static int hm_resolve_unix(const char *path, struct sockaddr_storage *out,
                           socklen_t *outlen, char *err, size_t errlen) {
#ifdef _WIN32
    (void)path; (void)out; (void)outlen;
    snprintf(err, errlen, "unix-domain listeners are not supported on Windows");
    return -1;
#else
    struct sockaddr_un *un = (struct sockaddr_un *)out;
    size_t l = strlen(path);
    if (l == 0 || l >= sizeof(un->sun_path)) {
        snprintf(err, errlen, "path is empty or longer than %d bytes",
                 (int)sizeof(un->sun_path) - 1);
        return -1;
    }
    memset(out, 0, sizeof(*out));
    un->sun_family = AF_UNIX;
    memcpy(un->sun_path, path, l + 1);
    *outlen = (socklen_t)(offsetof(struct sockaddr_un, sun_path) + l + 1);
    return 0;
#endif
}

/* An IPv6 listener is v6 ONLY, so `::` and `0.0.0.0` can both be listed on
 * one port; without it the second bind fails on Linux and the v4-mapped
 * addresses the dual-stack socket would report differ by platform. */
static void hm_v6only(int fd, const struct sockaddr *sa) {
#ifdef IPV6_V6ONLY
    if (sa->sa_family == AF_INET6) {
        int one = 1;
        hm_os_setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &one, sizeof(one));
    }
#else
    (void)fd; (void)sa;
#endif
}

static int hm_make_listener(const struct sockaddr *sa, socklen_t salen, int reuseport) {
    int one = 1;
    int fd;
#ifndef _WIN32
    /* A path listener: a socket file left by an earlier run is replaced; a
     * regular file at the path is somebody else's and is refused. */
    if (sa->sa_family == AF_UNIX) {
        const char *p = ((const struct sockaddr_un *)sa)->sun_path;
        struct stat st;
        if (stat(p, &st) == 0) {
            int pfd;
            if (!S_ISSOCK(st.st_mode)) { errno = EEXIST; return -1; }
            /* Somebody answering at the path is a live server, and this
             * would have unlinked its socket from under it (a second
             * instance started by mistake took the path silently). Refused
             * as a TCP bind of a busy port is. */
            pfd = hm_os_socket(AF_UNIX, SOCK_STREAM, 0);
            if (pfd >= 0) {
                int alive = connect(pfd, sa, salen) == 0;
                hm_os_close(pfd);
                if (alive) { errno = EADDRINUSE; return -1; }
            }
            (void)unlink(p);
        }
    }
#endif
    fd = hm_os_socket(sa->sa_family, SOCK_STREAM, 0);
    if (fd < 0) return -1;
#ifndef _WIN32
    /* Not into anything the application execs. A re-exec successor is the
     * one program meant to inherit it, and hm_reexec clears this for that
     * child alone. */
    (void)fcntl(fd, F_SETFD, fcntl(fd, F_GETFD, 0) | FD_CLOEXEC);
#endif
    hm_v6only(fd, sa);
#ifdef _WIN32
    /* SO_REUSEADDR on Windows lets a second bind STEAL a live listener,
     * which is the opposite of the Unix meaning we want here (rebind a
     * TIME_WAIT port). SO_EXCLUSIVEADDRUSE is the flag that buys what
     * SO_REUSEADDR buys everywhere else. */
    hm_os_setsockopt(fd, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, &one, sizeof(one));
#else
    hm_os_setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
#endif
#ifdef SO_REUSEPORT
    if (reuseport)
        hm_os_setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one));
#else
    (void)reuseport;
#endif
    if (hm_os_bind(fd, sa, salen) < 0) {
        hm_os_close(fd); return -1;
    }
    if (hm_os_listen(fd, SOMAXCONN) < 0) { hm_os_close(fd); return -1; }
    hm_set_nonblock(fd);
    return fd;
}

/* The QUIC listener: the same address as its TCP twin, over UDP. No
 * listen(2) - there is no accept queue, because there is no accept: one
 * socket carries every connection and the Destination Connection ID in each
 * packet says which (hm_quic.h). SO_REUSEPORT is honoured the same way, and
 * carries the same caveat the TCP side does not have: the kernel's UDP
 * reuseport hash is over the 4-tuple, so a client that migrates lands on a
 * different worker than the one holding its connection. That is why
 * migration is deferred rather than half-supported. */
static int hm_make_udp_listener(const struct sockaddr *sa, socklen_t salen, int reuseport) {
    int one = 1;
    int fd = hm_os_socket(sa->sa_family, SOCK_DGRAM, 0);
    if (fd < 0) return -1;
#ifndef _WIN32
    (void)fcntl(fd, F_SETFD, fcntl(fd, F_GETFD, 0) | FD_CLOEXEC);
#endif
    hm_v6only(fd, sa);
#ifdef _WIN32
    hm_os_setsockopt(fd, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, &one, sizeof(one));
#else
    hm_os_setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
#endif
#ifdef SO_REUSEPORT
    if (reuseport)
        hm_os_setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one));
#else
    (void)reuseport;
#endif
    if (hm_os_bind(fd, sa, salen) < 0) {
        hm_os_close(fd); return -1;
    }
    hm_set_nonblock(fd);
    return fd;
}

/* ---- the supervisor half: POSIX only --------------------------------------
 * Everything from here to the end of hm_run_server's pool branch needs
 * fork(2), waitpid(2) and POSIX signal delivery. Windows has none of the
 * three, and perl's fork emulation is ithreads - N interpreters inside ONE
 * process, sharing the file-scope state this server keeps per process - so
 * a pool there would corrupt, not scale. Windows runs the single-worker
 * in-process path instead (hm_run_server, above the pool branch), and an
 * explicit workers > 1 is refused at boot. */
#ifndef _WIN32

/* Supervisor signal flags: TERM/INT = shut the pool down, HUP/USR2 =
 * recycle workers with zero downtime, USR1 = stats dump. */
static volatile sig_atomic_t hm_sup_term = 0;
static volatile sig_atomic_t hm_sup_hup  = 0;
static volatile sig_atomic_t hm_sup_usr1 = 0;
static volatile sig_atomic_t hm_sup_tick = 0;   /* ALRM: the liveness look */
static volatile sig_atomic_t hm_sup_usr2 = 0;   /* re-exec a successor     */
/* The successor supervisor a USR2 started, until it has taken over (it
 * TERMs us) or died trying (the reaper clears it). */
static pid_t hm_resup_pid = 0;
/* What this process was started as, for the exec: $^X and perl's argv from
 * the first argument on. Copied at BOOT, because `$0 = ...` on Linux writes
 * over the memory PL_origargv points into. */
static char **hm_boot_argv = NULL;
/* Set in a successor: the supervisor that handed its sockets over and is
 * told to drain once this pool is up. */
static pid_t hm_resup_old = 0;
/* An alarm the caller had pending when run() was called: the deadline it asked
 * for, and whether it has come due. The supervisor's tick owns SIGALRM, so the
 * caller's is carried rather than cancelled. */
static time_t hm_sup_alarm_at = 0;
static int    hm_sup_alarmed  = 0;
static void hm_sup_sig(int sig) {
    if      (sig == SIGHUP)              hm_sup_hup  = 1;
    else if (sig == SIGUSR2)             hm_sup_usr2 = 1;
    else if (sig == SIGUSR1)             hm_sup_usr1 = 1;
    else if (sig == SIGALRM)             hm_sup_tick = 1;
    else if (sig == SIGCHLD)             ;   /* no-op: just wakes sigsuspend() */
    else                                 hm_sup_term = 1;
}

/* Wedged is not dead. A worker that is alive but has stopped turning its
 * loop keeps its pid and its row; what stops is the row's `updated` stamp,
 * which the one-second tick moves. Three looks three seconds apart with the
 * stamp unmoved and the pid answering kill(0) is the signal, logged once per
 * episode. The respawn policy is untouched: this says, it does not act. */
#define HM_SUP_TICK_SECS   3
#define HM_SUP_WEDGED_TICKS 3
static uint64_t hm_child_seen[1024];
static int      hm_child_stale[1024];

static void hm_sup_watch_board(void) {
    int i;
    if (!hm_sa_board_live()) return;
    for (i = 0; i < hm_nchildren; i++) {
        sa_sb_reading rd;
        pid_t pid = hm_children[i];
        if (hm_sa_board_row_of((uint64_t)pid, &rd) < 0) { hm_child_stale[i] = 0; continue; }
        if (kill(pid, 0) != 0) continue;      /* gone: the reaper's business */
        if (rd.updated == hm_child_seen[i]) {
            if (++hm_child_stale[i] == HM_SUP_WEDGED_TICKS)
                hm_errlog("Hyperman: worker %d is alive but has not "
                        "updated its scoreboard row for %d seconds: wedged?\n",
                        (int)pid, HM_SUP_TICK_SECS * HM_SUP_WEDGED_TICKS);
        }
        else {
            hm_child_seen[i]  = rd.updated;
            hm_child_stale[i] = 0;
        }
    }
}

#endif /* !_WIN32 - the rest of this section is shared; hm_spawn and the
        * pool branch of hm_run_server re-open the guard below */

/* One listener as configured by run(): its bind address, optional TLS, h2,
 * and https-redirect target. The bound fd and built SSL_CTX are filled in by
 * hm_run_server before the workers fork. */
typedef struct {
    const char *host;
    int         port;
    const char *path;               /* a unix-domain listener instead of host:port */
    int         mode_set, mode;     /* its file mode, when given                 */
    int         inherit;            /* adopt this one from LISTEN_FDS            */
    int         proxy_protocol;     /* every connection opens with a PROXY header */
    const char *tls_cert, *tls_key, *tls_ca;
    int         tls_verify;
    SV         *tls_sni;
    int         http2;
    int         http3;
    int         redirect_https;
    int         fd;                 /* bound fd (parent), or -1 under reuseport */
    int         udp_fd;             /* the QUIC socket, same rules; -1 if no h3 */
    void       *tls_ctx;            /* built SSL_CTX*, or NULL for plain        */
    struct sockaddr_storage sa;     /* host:port resolved once, in the parent;  */
    socklen_t   salen;              /* what every bind, incl. a reuseport
                                     * rebind in a worker, binds             */
} hm_listener_spec;

/* Validate one listener's options at run() time (before any bind/fork). */
static void hm_check_listener(pTHX_ const hm_listener_spec *s) {
    if (s->tls_cert || s->tls_key || s->tls_ca || s->tls_sni || s->tls_verify) {
        if (!(s->tls_cert && s->tls_key))
            croak("Hyperman->run: tls_cert and tls_key must be given together");
        if (!hm_tls_available())
            croak("Hyperman->run: TLS requested but OpenSSL support was not "
                  "built (install libssl-dev and reinstall Hyperman)");
        if (s->tls_verify && !s->tls_ca)
            croak("Hyperman->run: tls_verify needs tls_ca (the CA to verify "
                  "client certificates against)");
    }
    if (s->http2 && !hm_h2_available())
        croak("Hyperman->run: http2 requested but nghttp2 support was not "
              "built (install libnghttp2-dev and reinstall Hyperman)");
    if (s->http3) {
        if (!hm_h3_available())
            croak("Hyperman->run: http3 requested but QUIC support was not "
                  "built (it needs ngtcp2, ngtcp2_crypto_ossl, nghttp3 and "
                  "OpenSSL 3.5 or newer - reinstall Hyperman once they are "
                  "installed)");
        /* There is no cleartext QUIC the way there is h2c: the transport's
         * handshake IS a TLS handshake, so a certificate is not optional. */
        if (!(s->tls_cert && s->tls_key))
            croak("Hyperman->run: http3 needs tls_cert and tls_key (QUIC has "
                  "no cleartext mode)");
    }
    if (s->redirect_https && (s->tls_cert || s->tls_key))
        croak("Hyperman->run: redirect_https on a TLS listener makes no sense");
    if (s->path) {
        if (s->http3)
            croak("Hyperman->run: http3 needs an IP listener; a path listener "
                  "has no UDP twin");
    }
    if (s->proxy_protocol && s->http3)
        warn("Hyperman->run: proxy_protocol applies to the TCP side of this "
             "listener only; QUIC has no byte stream before its handshake and "
             "reports real datagram sources");
}

/* Overlay per-listener options from a `listen => [ {..} ]` hashref onto a spec
 * pre-seeded with the top-level defaults. Unknown keys croak. A bare true
 * redirect_https means "the standard 443". */
static void hm_listener_from_hv(pTHX_ HV *h, hm_listener_spec *s) {
    HE *he;
    hv_iterinit(h);
    while ((he = hv_iternext(h))) {
        STRLEN klen; const char *k = HePV(he, klen); SV *v = HeVAL(he);
        if      (strEQ(k, "port"))     s->port = (int)SvIV(v);
        else if (strEQ(k, "host"))     { if (SvOK(v)) s->host = SvPV_nolen(v); }
        else if (strEQ(k, "path"))     { if (SvOK(v)) s->path = SvPV_nolen(v); }
        else if (strEQ(k, "inherit"))  s->inherit = SvTRUE(v) ? 1 : 0;
        else if (strEQ(k, "proxy_protocol")) s->proxy_protocol = SvTRUE(v) ? 1 : 0;
        else if (strEQ(k, "mode")) {
            if (SvOK(v)) {
                if (SvIOK(v) || SvNOK(v)) s->mode = (int)SvIV(v);
                else {
                    char *end;
                    const char *m = SvPV_nolen(v);
                    s->mode = (int)strtol(m, &end, 8);
                    if (*m == '\0' || *end != '\0')
                        croak("Hyperman->run: mode '%s' is not an octal number", m);
                }
                s->mode_set = 1;
            }
        }
        else if (strEQ(k, "tls_cert")) s->tls_cert = SvOK(v) ? SvPV_nolen(v) : NULL;
        else if (strEQ(k, "tls_key"))  s->tls_key  = SvOK(v) ? SvPV_nolen(v) : NULL;
        else if (strEQ(k, "tls_ca"))   s->tls_ca   = SvOK(v) ? SvPV_nolen(v) : NULL;
        else if (strEQ(k, "tls_sni"))  s->tls_sni  = SvOK(v) ? v : NULL;
        else if (strEQ(k, "http2"))    s->http2 = SvTRUE(v) ? 1 : 0;
        else if (strEQ(k, "http3"))    s->http3 = SvTRUE(v) ? 1 : 0;
        else if (strEQ(k, "redirect_https")) s->redirect_https = (int)SvIV(v);
        else if (strEQ(k, "tls_verify")) {
            const char *m = SvPV_nolen(v);
            if      (strEQ(m, "require"))  s->tls_verify = 2;
            else if (strEQ(m, "optional")) s->tls_verify = 1;
            else if (strEQ(m, "none"))     s->tls_verify = 0;
            else croak("Hyperman->run: tls_verify must be none/optional/require");
        }
        else croak("Hyperman->run: unknown listen option '%s'", k);
    }
    if (s->redirect_https == 1) s->redirect_https = 443;
    /* A path listener is seeded with the top-level host and port like every
     * other spec; the path is the address, so those defaults are dropped
     * rather than read as a contradiction. */
    if (s->path) { s->host = NULL; s->port = 0; }
}

/* Everything a worker needs; one struct so spawn plumbing stays sane. */
typedef struct {
    SV         *app;
    hm_listener_spec *lspecs;       /* one or more listeners                 */
    int         nlspecs;
    double      idle_t, header_t, body_t, request_t, grace;
    UV          body_min_rate;
    int         max_pipe, reuseport, affinity, nworkers;
    int         completion;        /* -1 = say nothing, 0/1 = force        */
    int         compress;
    size_t      compress_min;
    int         compress_level;
    size_t      max_read;          /* request ceiling; 16MB by default    */
    SV         *compress_types;
    UV          max_requests;
    SV         *log_cb;              /* access_log coderef, or NULL          */
    int         log_fd;             /* fast access-log fd, or -1            */
    const char *log_path;           /* access_log file to open (parent), or NULL */
    const char *log_format;         /* access_log_format, NULL = Combined   */
    hm_logtok  *logtok;             /* ... compiled, before the fork         */
    int         nlogtok, log_json, log_wants_t0;
    int         err_fd;             /* error_log fd, or -1 = stderr         */
    const char *err_path;           /* error_log file to open (parent), or NULL */
    /* Privileges, dropped in the parent after every bind and before the
     * first fork (hm_drop_privileges). Names are resolved FIRST, before the
     * bind, so an unknown user is a croak with the port still free. */
    const char *drop_user;          /* user => name or uid, or NULL          */
    const char *drop_group;         /* group => name or gid, or NULL         */
    const char *pid_path;           /* pid_file, or NULL                     */
    int         listen_fds;         /* adopt every listener from LISTEN_FDS  */
    int         umask_set;          /* umask => given                        */
    unsigned    umask_val;
    int         drop_uid_set, drop_gid_set;
    long        drop_uid, drop_gid;
    char        drop_name[64];      /* the user's name, for initgroups      */
    SV         *deny;               /* arrayref of IPs to denylist, or NULL */
    unsigned    deny_cap, rate_cap; /* arena table sizes, 0 = default        */
    unsigned    bus_slots, bus_slot_size, bus_groups;  /* 0 = default        */
    UV          http3_max_conns;    /* QUIC connection ceiling, 0 = off      */
    const char *arena_name;         /* Shared::Arena name, NULL = anonymous  */
    STRLEN      arena_name_len;
    UV          arena_size;         /* bytes, 0 = computed from the tenants  */
    int         distinct_clients;   /* count distinct peers at accept (HLL)  */
} hm_worker_cfg;

/* Populate the loop's listener array from the (already bound) specs. Under
 * reuseport each worker rebinds, so fds are passed in; otherwise they are the
 * parent's inherited fds. Returns 0, or -1 if a reuseport rebind failed. */
static int hm_worker_listeners(hm_loop *loop, const hm_worker_cfg *cfg,
                               const int *fds) {
    int i;
    loop->listeners = (hm_listener *)hm_xcalloc(cfg->nlspecs, sizeof(hm_listener));
    loop->nlisteners = cfg->nlspecs;
    for (i = 0; i < cfg->nlspecs; i++) {
        const hm_listener_spec *s = &cfg->lspecs[i];
        hm_listener *l = &loop->listeners[i];
        l->fd = fds ? fds[i] : s->fd;
        if (l->fd < 0) return -1;
        l->udp_fd = fds ? fds[cfg->nlspecs + i] : s->udp_fd;
        l->is_unix = s->path ? 1 : 0;
        l->proxy_protocol = s->proxy_protocol;
        l->host = strdup(s->path ? s->path
                         : s->host && *s->host ? s->host : "0.0.0.0");
        l->port = s->port;
        l->tls_ctx = s->tls_ctx;
        l->http2 = s->http2;
        l->http3 = s->http3;
        l->redirect_https = s->redirect_https;
        /* Copied, not borrowed: hm_tls_reload frees the context it
         * replaces, and a rebuild has to name the same cert and key. */
        l->tls_cert   = s->tls_cert ? strdup(s->tls_cert) : NULL;
        l->tls_key    = s->tls_key  ? strdup(s->tls_key)  : NULL;
        l->tls_ca     = s->tls_ca   ? strdup(s->tls_ca)   : NULL;
        l->tls_verify = s->tls_verify;
        l->tls_sni    = s->tls_sni ? SvREFCNT_inc(s->tls_sni) : NULL;
        /* Precomputed once here rather than per response: the value never
         * changes for the life of the listener, and a header built per
         * request would be built on the hot path for every request the
         * listener ever serves. */
        if (l->udp_fd >= 0) {
            char b[64];
            int n = snprintf(b, sizeof(b), "h3=\":%d\"; ma=86400", l->port);
            l->altsvc = (n > 0 && n < (int)sizeof(b)) ? strdup(b) : NULL;
        }
    }
    return 0;
}

/* Rebuild this worker's TLS contexts from a fresh SNI map.
 *
 * The listener array is the WORKER'S: hm_worker_listeners above ran after
 * the fork and copied the parent's tls_ctx pointer into it, so swapping
 * that pointer changes this process and nothing else. The listening fd is
 * not touched, so nothing is unbound and no connection is refused;
 * connections already handshook keep the SSL they hold, and the next one
 * this worker accepts uses the new context.
 *
 * NOTHING IS INSTALLED THAT SERVES LESS THAN WHAT IS RUNNING. Two ways a
 * rebuild can come back worse, and both keep the old context:
 *
 *   - the default certificate will not load. It is the one every
 *     hostname falls back to, so a fleet keeps serving the stale one
 *     rather than none.
 *   - a per-host certificate will not load. hm_tls_ctx_build skips it
 *     with a message and still returns a usable default, which is right
 *     at boot but silently drops that domain to the fallback here. So
 *     the built registry is counted against what was asked for, and a
 *     context carrying fewer hosts is thrown away. The cost is that one
 *     corrupt PEM defers everybody's update to the next poll, against
 *     installing a context that quietly stopped serving a domain.
 *
 * Returns the number of listeners rebuilt. */
static int hm_tls_reload(pTHX_ hm_loop *loop, SV *sni) {
    int i, done = 0;
    int given = sni && SvROK(sni) && SvTYPE(SvRV(sni)) == SVt_PVHV;
    if (!loop) return 0;

    for (i = 0; i < loop->nlisteners; i++) {
        hm_listener *l = &loop->listeners[i];
        void *fresh;
        unsigned char tkey[HM_TLS_TKEY_MAX];
        const unsigned char *tk = NULL;
        size_t tklen = 0;
        /* No map given: the listener's own, so a reload called for the
         * default certificate alone keeps every SNI host. Without this the
         * rebuilt context had no registry and the "fewer hosts" refusal
         * below did not apply to it, so every name fell to the default. */
        SV *use = given ? sni : l->tls_sni;
        int want = (use && SvROK(use) && SvTYPE(SvRV(use)) == SVt_PVHV)
                 ? (int)HvUSEDKEYS((HV *)SvRV(use)) : -1;
        if (!l->tls_ctx || !l->tls_cert || !l->tls_key) continue;  /* plain */

        /* Carry the running context's session-ticket key into the new one.
         * The boot-time contexts are built before the fork, so every worker
         * starts out able to decrypt every other worker's tickets - but this
         * reload runs independently in each of them, and SSL_CTX_new mints a
         * fresh random key. Left alone, the first rotation would leave a
         * ticket issued by one worker undecryptable by its siblings, and
         * every resumption a client attempted would quietly become a full
         * handshake: the signature and key exchange that tickets exist to
         * avoid, on a fleet that rotates certificates continuously. */
        if (hm_tls_get_ticket_key(l->tls_ctx, tkey, &tklen)) tk = tkey;

        fresh = hm_tls_ctx_build(aTHX_ l->tls_cert, l->tls_key, l->tls_ca,
                                 l->tls_verify, use, l->http2, tk, tklen);
        if (!fresh) continue;                   /* keep what we are serving */

        if (want > 0 && hm_tls_sni_count(fresh) < want) {
            hm_errlog("Hyperman TLS: reload built %d of %d SNI hosts; "
                            "keeping the running certificates\n",
                    hm_tls_sni_count(fresh), want);
            hm_tls_ctx_free(fresh);
            continue;
        }

        /* The QUIC context serves the SAME certificate over the same origin,
         * so it is rebuilt with the TCP one and the two are swapped together
         * or not at all. Swapping only the TCP side leaves a fleet serving a
         * fresh certificate over TCP and a stale one over QUIC - a mismatch
         * no TCP-only monitor can see, on the transport a browser prefers.
         *
         * The new QUIC context is built BEFORE either is installed, so a
         * failure to build it keeps both of the running pair rather than
         * leaving a half-swapped listener. */
        if (l->udp_fd >= 0 && l->quic) {
            void *qfresh = hm_tls_quic_ctx_build(l->tls_cert, l->tls_key,
                                                 l->tls_ca, l->tls_verify,
                                                 tk, tklen);
            if (!qfresh) {
                hm_errlog("Hyperman TLS: reload could not rebuild the "
                                "QUIC context; keeping both running "
                                "certificates\n");
                hm_tls_ctx_free(fresh);
                continue;
            }
            hm_quic_ctx_swap(aTHX_ l, qfresh);
        }

        hm_tls_ctx_free(l->tls_ctx);
        l->tls_ctx = fresh;
        done++;
    }
    return done;
}

/* The bus wakeup, on this worker's loop.
 *
 * Lives here rather than in hm_sa_bus.h because it is the one part that needs
 * both halves: hm_sa_bus.h deliberately knows nothing about the server, and
 * the loop knows nothing about the ring.
 *
 * The callback MUST NOT CROAK. It is called from the event loop with no Perl
 * frame around it, and a die from a subscriber would unwind through the loop
 * and take the worker with it - so the dispatch below is the only thing here,
 * and anything Perl-facing wraps itself in G_EVAL before it gets this far. */
static void hm_bus_wake_cb(pTHX_ int fd, int mask, void *ud) {
    PERL_UNUSED_ARG(fd);
    PERL_UNUSED_ARG(mask);
    PERL_UNUSED_ARG(ud);
    PERL_UNUSED_CONTEXT;
    /* Empty the pipe and clear the flag BEFORE dispatching, so a publish that
     * lands while we are dispatching sets it again and pokes afresh, rather
     * than being folded into a wakeup that has already been handled. The
     * order inside the ring's wake_drained matters too - see hm_sa_bus.h. */
    hm_sa_bus_waker_drained();
    (void)hm_sa_bus_dispatch();
}

/* The one-second tick: the loop's counters into this worker's scoreboard
 * row in one seqlock bracket, a heartbeat on the peer row, and the next
 * tick armed. A C timer is one-shot and freed on fire, so re-arming here is
 * the whole of the repetition. Stops with the loop: a draining worker keeps
 * mirroring until its last connection closes, and nothing here holds the
 * loop open once it wants to end. */
static void hm_board_tick(pTHX_ void *ud) {
    hm_loop *loop = (hm_loop *)ud;
    hm_sa_board_mirror((uint64_t)loop->requests, (uint64_t)loop->accepts,
                       (uint64_t)loop->denied, loop->nconns,
                       (uint64_t)loop->bytes_out, (uint64_t)loop->datagrams,
                       (uint64_t)loop->h3_requests);
    if (HM_SA && hm_sa_region) HM_SA->beat(hm_sa_region);
    if (!loop->stopping)
        (void)hm_add_timer_watch_c(loop, 1.0, hm_board_tick, loop);
}

/* The polling fallback's tick: drain the ring, re-arm. */
static void hm_bus_poll_cb(pTHX_ void *ud) {
    hm_loop *loop = (hm_loop *)ud;
    (void)hm_sa_bus_dispatch();
    if (!loop->stop) (void)hm_add_timer_watch_c(loop, 0.05, hm_bus_poll_cb, loop);
}

static void hm_bus_worker_attach(pTHX_ hm_loop *loop) {
    int fd;
    if (!hm_sa_bus_live()) return;
    /* A worker inherited the parent's cursors along with everything else, and
     * a cursor is a position in a stream this process has not been reading.
     * Left alone it either replays what the parent already handled or skips
     * what it has not. From now on. */
    hm_sa_bus_reset_cursors();

    /* THE LAST WORD ON WHETHER THIS PROCESS CAN BE WOKEN.
     *
     * hm_spawn asks for a slot by worker index, which is what keeps two
     * workers off one descriptor. It can come back empty-handed: the index it
     * asked for may still be held by a worker that is draining after a HUP,
     * and single-worker mode never goes through hm_spawn at all. Either way
     * the process ends up with no descriptor.
     *
     * Before this, that was the end of it - no watch was installed and this
     * worker never dispatched again, so a published invalidation or room
     * broadcast reached every worker but this one, for as long as it ran, with
     * nothing in any statistic to say so. Single-worker mode never delivered
     * to its own subscribers at all.
     *
     * So take whatever is going rather than go deaf. Which slot a worker holds
     * has never mattered; holding one is the whole of it. */
    fd = hm_sa_bus_waker_fd();
    if (fd < 0) {
        (void)hm_sa_bus_waker_take(-1);
        fd = hm_sa_bus_waker_fd();
    }
    if (fd < 0) {
        /* No descriptor: the slots were all held, or this process attached
         * the arena by name (a re-exec successor's pool does) and the pipes
         * in it belong to another process. Poll the ring instead, every
         * fifty milliseconds. Slower to notice a message than a poke, never
         * deaf. */
        hm_errlog("Hyperman: worker %d has no wakeup descriptor; polling the "
                  "bus every 50ms instead", hm_os_getpid());
        (void)hm_add_timer_watch_c(loop, 0.05, hm_bus_poll_cb, loop);
        return;
    }
    hm_add_io_watch_c(aTHX_ loop, fd, HM_EV_READ, hm_bus_wake_cb, NULL);
    /* Anything published between the fork and this attach is already on the
     * ring with no poke coming, because nothing was watching to be poked. */
    (void)hm_sa_bus_dispatch();
}

static void hm_worker(pTHX_ const hm_worker_cfg *cfg, const int *fds) {
    hm_loop *loop = hm_loop_new(aTHX_ NULL);
    if (!loop) croak("Hyperman: cannot create event loop");
    if (cfg->idle_t   > 0) loop->idle_timeout   = cfg->idle_t;
    if (cfg->header_t > 0) loop->header_timeout = cfg->header_t;
    if (cfg->body_t   > 0) loop->body_timeout   = cfg->body_t;
    if (cfg->request_t > 0) loop->request_timeout = cfg->request_t;
    if (cfg->body_min_rate) loop->body_min_rate  = cfg->body_min_rate;
    if (cfg->max_pipe > 0) loop->max_pipeline   = cfg->max_pipe;
    if (cfg->grace    > 0) loop->shutdown_grace = cfg->grace;
    loop->max_requests = cfg->max_requests;
    /* Only when the caller actually said so: -1 leaves whatever the loop
     * decided for itself from the backend and the environment. */
    if (cfg->completion >= 0)
        loop->completion = (cfg->completion && loop->be->submit_recv) ? 1 : 0;
    loop->compress      = cfg->compress;
    if (cfg->compress_min) loop->compress_min = cfg->compress_min;
    if (cfg->compress_level) loop->compress_level = cfg->compress_level;
    if (cfg->max_read) loop->max_read = cfg->max_read;
    loop->http3_max_conns = cfg->http3_max_conns;
    loop->compress_types = cfg->compress_types
                           ? SvREFCNT_inc(cfg->compress_types) : NULL;
    loop->logtok       = cfg->logtok;        /* compiled once, in the parent */
    loop->nlogtok      = cfg->nlogtok;
    loop->log_json     = cfg->log_json;
    loop->log_wants_t0 = cfg->log_wants_t0;
    if (hm_worker_listeners(loop, cfg, fds) < 0) {
        hm_loop_free(aTHX_ loop);
        _exit(1);
    }
    if (cfg->log_fd >= 0)
        loop->log_fd = cfg->log_fd;               /* fast C writer wins */
    else if (cfg->log_cb && SvOK(cfg->log_cb))
        loop->log_cb = SvREFCNT_inc(cfg->log_cb);
    hm_attach_server(aTHX_ loop, cfg->app);
    /* v4 on_worker_start: the loop exists and has not started turning, and in
     * a prefork server this is already the child. The one moment a consumer
     * can attach something loop-bound and have it belong to the process that
     * will actually serve. Fires here rather than at either call site so the
     * single-worker case, which never forks, gets it too. */
    hm_worker_hook_fire(aTHX_ (void *)loop);

    /* The bus wakeup. The descriptors were made before the fork; this worker
     * takes the one that is its own and puts it on the loop, so a publish from
     * a sibling returns this loop from epoll with the message already on the
     * ring rather than leaving it to be found whenever somebody next looks.
     *
     * After hm_worker_hook_fire, so an application's own on_worker_start has
     * already run and anything it subscribed is registered before the first
     * wakeup can arrive. */
    hm_bus_worker_attach(aTHX_ loop);
    /* A peer slot of this worker's own in the Shared::Arena, keyed on the
     * pid it has now rather than the one it inherited. */
    hm_sa_worker_join();
    /* This worker's scoreboard row, mirrored now and then once a second.
     * The tick also beats the peer row, so a worker that only ever receives
     * on the bus never looks dead to the ring's hole escalation. */
    if (hm_sa_board_live() && hm_sa_board_take() >= 0)
        hm_board_tick(aTHX_ loop);

    hm_loop_run(aTHX_ loop, NULL);
    hm_loop_free(aTHX_ loop);
}

#ifndef _WIN32
/* Fork one worker. With reuseport each worker binds its own listeners; with
 * affinity (Linux) worker widx is pinned to core widx % ncpu. */
static pid_t hm_spawn(pTHX_ const hm_worker_cfg *cfg, int widx) {
    pid_t pid = fork();
    if (pid == 0) {
        int  stackfds[8];
        int *fds = NULL;

        /* A RESPAWNED worker is forked from inside the supervisor's loop,
         * where the pool's signals are handled by hm_sup_sig and BLOCKED.
         * The child inherits both. Until the worker's own loop has taken
         * over its signals, a TERM landing here would run the supervisor's
         * handler and set a flag nobody in this process reads: the signal
         * is swallowed, the worker never learns the pool is shutting down,
         * and the supervisor waits for it for ever. Found by a test that
         * killed a worker and sent TERM the moment its replacement had a
         * scoreboard row. Default dispositions and an empty mask, so a
         * TERM in that window ends the child, which is what TERM means. */
        {
            sigset_t none;
            sigemptyset(&none);
            signal(SIGTERM, SIG_DFL); signal(SIGINT,  SIG_DFL);
            signal(SIGHUP,  SIG_DFL); signal(SIGUSR1, SIG_DFL);
            signal(SIGUSR2, SIG_DFL); signal(SIGCHLD, SIG_DFL);
            signal(SIGALRM, SIG_DFL);
            alarm(0);
            sigprocmask(SIG_SETMASK, &none, NULL);
        }

        /* Tell perl it has been forked. $$ is a plain SV that perl only
         * refreshes inside pp_fork; forking from C here happens behind its
         * back, so without this the worker keeps reporting the supervisor's
         * pid - and POSIX::getpid is `sub getpid { $$ }`, so that reads wrong
         * too. Anything in the application that names a log line, a temp file
         * or a pid file after $$ then collides across the whole pool. This is
         * what pp_fork itself does in the child. */
        {
            GV *pidgv = gv_fetchpvs("$", GV_ADD|GV_NOTQUAL, SVt_PV);
            if (pidgv) {
                SvREADONLY_off(GvSV(pidgv));
                sv_setiv(GvSV(pidgv), (IV)getpid());
                SvREADONLY_on(GvSV(pidgv));
            }
        }

#if defined(__linux__) && defined(CPU_SET)
        if (cfg->affinity) {
            long ncpu = sysconf(_SC_NPROCESSORS_ONLN);
            if (ncpu > 0) {
                cpu_set_t set;
                CPU_ZERO(&set);
                CPU_SET((int)(widx % ncpu), &set);
                sched_setaffinity(0, sizeof(set), &set);
            }
        }
#else
        (void)widx;
#endif
        if (cfg->reuseport) {
            int i, n = cfg->nlspecs;
            /* Two descriptors per listener now, TCP in the first half and
             * QUIC in the second, so hm_worker_listeners reads fds[i] and
             * fds[n + i]. */
            fds = 2 * n <= (int)(sizeof(stackfds)/sizeof(stackfds[0]))
                  ? stackfds
                  : (int *)hm_xmalloc(sizeof(int) * 2 * n);
            for (i = 0; i < n; i++) {
                fds[i] = hm_make_listener((struct sockaddr *)&cfg->lspecs[i].sa,
                                          cfg->lspecs[i].salen, 1);
                if (fds[i] < 0) _exit(1);
                fds[n + i] = -1;
                if (cfg->lspecs[i].http3) {
                    fds[n + i] = hm_make_udp_listener((struct sockaddr *)&cfg->lspecs[i].sa,
                                                      cfg->lspecs[i].salen, 1);
                    if (fds[n + i] < 0) _exit(1);
                }
            }
        }
        /* This child's own waker slot, claimed before hm_worker so the
         * attach below finds it. Index by worker number, so two workers never
         * share a descriptor. */
        (void)hm_sa_bus_waker_take(widx);
        hm_worker(aTHX_ cfg, fds);
        _exit(0);
    }
    return pid;
}
#endif /* !_WIN32 */

/* The full server: validate + bind, then either run in-process (1 worker)
 * or supervise a pool (respawn with backoff, HUP/USR2 recycle, USR1 stats,
 * TERM/INT bounded drain). */
/* ---- privileges and the pid file ----------------------------------------- */

static int hm_all_digits(const char *s) {
    if (!*s) return 0;
    for (; *s; s++) if (*s < '0' || *s > '9') return 0;
    return 1;
}

/* Turn user/group names into ids. Called before anything is bound: a typo
 * in `user` must not leave a server listening as root. */
static void hm_resolve_drop(pTHX_ hm_worker_cfg *cfg) {
    if (!cfg->drop_user && !cfg->drop_group && !cfg->umask_set && !cfg->pid_path)
        return;
#ifdef _WIN32
    croak("Hyperman->run: user, group, umask and pid_file are not supported "
          "on Windows");
#else
    cfg->drop_name[0] = '\0';
    if (cfg->drop_user) {
        if (hm_all_digits(cfg->drop_user)) {
            struct passwd *pw;
            cfg->drop_uid = atol(cfg->drop_user);
            cfg->drop_uid_set = 1;
            pw = getpwuid((uid_t)cfg->drop_uid);
            if (pw) {
                strncpy(cfg->drop_name, pw->pw_name, sizeof(cfg->drop_name) - 1);
                if (!cfg->drop_group) { cfg->drop_gid = (long)pw->pw_gid; cfg->drop_gid_set = 1; }
            } else if (!cfg->drop_group) {
                /* No entry to take a group from: without one the workers
                 * would keep gid 0 and root's supplementary groups behind a
                 * harmless-looking uid. */
                croak("Hyperman->run: user %s has no passwd entry; name a "
                      "group => as well", cfg->drop_user);
            }
        } else {
            struct passwd *pw = getpwnam(cfg->drop_user);
            if (!pw) croak("Hyperman->run: user '%s': no such user", cfg->drop_user);
            cfg->drop_uid = (long)pw->pw_uid;
            cfg->drop_uid_set = 1;
            strncpy(cfg->drop_name, pw->pw_name, sizeof(cfg->drop_name) - 1);
            if (!cfg->drop_group) { cfg->drop_gid = (long)pw->pw_gid; cfg->drop_gid_set = 1; }
        }
    }
    if (cfg->drop_group) {
        if (hm_all_digits(cfg->drop_group)) {
            cfg->drop_gid = atol(cfg->drop_group);
        } else {
            struct group *gr = getgrnam(cfg->drop_group);
            if (!gr) croak("Hyperman->run: group '%s': no such group", cfg->drop_group);
            cfg->drop_gid = (long)gr->gr_gid;
        }
        cfg->drop_gid_set = 1;
    }
#endif
}

/* Group first, then the user: once the uid is gone the gid cannot follow.
 * Each step is checked and a failure CROAKS - a server that could not drop
 * must not go on serving as root as if it had. */
static void hm_drop_privileges(pTHX_ const hm_worker_cfg *cfg) {
#ifndef _WIN32
    if (cfg->umask_set) umask((mode_t)cfg->umask_val);
    /* Not root: a re-exec successor forked by a supervisor that had already
     * dropped. It cannot change its ids and need not, if it already has the
     * ones asked for; anything else is refused, as before. */
    if (geteuid() != 0 && (cfg->drop_uid_set || cfg->drop_gid_set)) {
        if (cfg->drop_uid_set && (getuid() != (uid_t)cfg->drop_uid
                                  || geteuid() != (uid_t)cfg->drop_uid))
            croak("Hyperman->run: not root, and not uid %ld either: cannot "
                  "become user '%s'", cfg->drop_uid, cfg->drop_user);
        if (cfg->drop_gid_set && getegid() != (gid_t)cfg->drop_gid)
            croak("Hyperman->run: not root, and not gid %ld either: cannot "
                  "change group", cfg->drop_gid);
        return;
    }
    if (cfg->drop_gid_set) {
        if (cfg->drop_name[0]) {
            if (initgroups(cfg->drop_name, (gid_t)cfg->drop_gid) < 0)
                croak("Hyperman->run: initgroups(%s): %s", cfg->drop_name, strerror(errno));
        } else {
            gid_t g = (gid_t)cfg->drop_gid;
            if (setgroups(1, &g) < 0)
                croak("Hyperman->run: setgroups: %s", strerror(errno));
        }
        if (setgid((gid_t)cfg->drop_gid) < 0)
            croak("Hyperman->run: setgid(%ld): %s", cfg->drop_gid, strerror(errno));
    }
    if (cfg->drop_uid_set) {
        if (setuid((uid_t)cfg->drop_uid) < 0)
            croak("Hyperman->run: setuid(%ld): %s", cfg->drop_uid, strerror(errno));
        if (cfg->drop_uid != 0 && (getuid() != (uid_t)cfg->drop_uid
                                   || geteuid() != (uid_t)cfg->drop_uid))
            croak("Hyperman->run: still not uid %ld after setuid", cfg->drop_uid);
    }
#else
    (void)cfg;
#endif
}

/* The pid file is written whole and renamed into place, so a reader never
 * sees it empty, and removed at exit ONLY if it still holds this pid: two
 * supervisors overlap during a re-exec reload, and the one leaving must not
 * take the other's file with it. */
#ifndef O_NOFOLLOW
#define O_NOFOLLOW 0
#endif
/* loud: a failure croaks (boot). Quiet: -1 and a log line, for the
 * supervisor re-asserting its own pid after a successor died. */
static int hm_write_pidfile_(pTHX_ const hm_worker_cfg *cfg, int loud) {
    const char *path = cfg->pid_path;
    char tmp[1024], num[32];
    int fd = -1, n, tries, in_place = 0;
    if (!path) return 0;
    /* The temporary is created EXCLUSIVELY and never through a symlink: it
     * is written as root, before the drop, in a directory others may be
     * able to write to, and a predictable name opened with O_TRUNC was an
     * invitation to point it at a file of theirs. The suffix is random too,
     * so a planted name is not even a denial of service. */
    for (tries = 0; tries < 8 && fd < 0; tries++) {
        unsigned long r = (unsigned long)(hm_now_ns() ^ ((uint64_t)hm_os_getpid() << 20));
        n = snprintf(tmp, sizeof(tmp), "%s.%lx.tmp", path, r + (unsigned long)tries * 7919UL);
        if (n <= 0 || n >= (int)sizeof(tmp))
            croak("Hyperman->run: pid_file path too long");
        fd = hm_os_open_file(tmp, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0644);
        if (fd < 0 && errno != EEXIST) break;
    }
    if (fd < 0 && (errno == EACCES || errno == EPERM) && geteuid() != 0) {
        /* A re-exec successor already running as the dropped user, in a
         * directory only root may create in (/run): the file itself was
         * chowned to that user when root wrote it, so it is rewritten in
         * place. Not atomic - a reader can catch it empty - but a pid file
         * the successor cannot update at all named a dead process. */
        fd = hm_os_open_file(path, O_WRONLY | O_TRUNC | O_NOFOLLOW | O_CLOEXEC, 0644);
        in_place = 1;
    }
    if (fd < 0) {
        if (loud) croak("Hyperman->run: pid_file '%s': %s", in_place ? path : tmp,
                        strerror(errno));
        hm_errlog("Hyperman: pid_file '%s': %s\n", path, strerror(errno));
        return -1;
    }
    n = (int)hm_itoa(num, (long)hm_os_getpid());
    num[n++] = '\n';
    if (hm_os_write_file(fd, num, (size_t)n) != n) {
        int e = errno;
        hm_os_close(fd);
        if (!in_place) (void)unlink(tmp);
        if (loud) croak("Hyperman->run: pid_file '%s': write: %s", path, strerror(e));
        hm_errlog("Hyperman: pid_file '%s': write: %s\n", path, strerror(e));
        return -1;
    }
#ifndef _WIN32
    /* Owned by the user the server will run as, so the successor of a
     * re-exec (born dropped) and the exit path can both still write it. */
    if (cfg->drop_uid_set && geteuid() == 0)
        (void)fchown(fd, (uid_t)cfg->drop_uid,
                     cfg->drop_gid_set ? (gid_t)cfg->drop_gid : (gid_t)-1);
#endif
    hm_os_close(fd);
    if (in_place) return 0;
    if (rename(tmp, path) < 0) {
        int e = errno;
        (void)unlink(tmp);
        if (loud) croak("Hyperman->run: pid_file '%s': rename: %s", path, strerror(e));
        hm_errlog("Hyperman: pid_file '%s': rename: %s\n", path, strerror(e));
        return -1;
    }
    return 0;
}
static void hm_write_pidfile(pTHX_ const hm_worker_cfg *cfg) {
    (void)hm_write_pidfile_(aTHX_ cfg, 1);
}

/* Removed at exit if it still holds this pid. A process that dropped
 * privileges may not unlink in the directory any more (/run is root's);
 * then the file is emptied instead, which is what a reader should take as
 * "not running", where a stale pid is a live lie. */
static void hm_unlink_pidfile(const char *path) {
    char buf[32];
    int fd, n;
    if (!path) return;
    fd = hm_os_open_file(path, O_RDONLY | O_NOFOLLOW, 0);
    if (fd < 0) return;
    n = (int)read(fd, buf, sizeof(buf) - 1);
    hm_os_close(fd);
    if (n <= 0) return;
    buf[n] = '\0';
    if (atol(buf) != (long)hm_os_getpid()) return;
    if (unlink(path) == 0) return;
    if (errno == EACCES || errno == EPERM) {
        fd = hm_os_open_file(path, O_WRONLY | O_TRUNC | O_NOFOLLOW, 0);
        if (fd >= 0) hm_os_close(fd);
    }
}

/* Remove a variable from the C environment AND perl's %ENV, which is a hash
 * of its own, filled at startup: unsetenv alone leaves the application
 * reading the old value from the hash. Deleting through the hash updates
 * the C side via its magic; the explicit unsetenv covers a perl built
 * without that. */
static void hm_env_unset(pTHX_ const char *name) {
    HV *penv = GvHV(PL_envgv);
    if (penv) (void)hv_delete(penv, name, (I32)strlen(name), G_DISCARD);
#ifndef _WIN32
    unsetenv(name);
#endif
}

/* Socket activation: a service manager that bound the sockets hands them
 * over as fds 3.., naming the count in LISTEN_FDS and this pid in
 * LISTEN_PID. Specs marked `inherit` (or all of them under listen_fds) take
 * them in order instead of binding. Each one is checked to be a listening
 * socket of the family the spec resolved to, and both variables are removed
 * at once so no worker - and no server an application forks - adopts them
 * again. The same helper will seat a re-exec successor (08). */
static void hm_adopt_fds(pTHX_ hm_worker_cfg *cfg) {
    const char *nfds = getenv("LISTEN_FDS"), *lpid = getenv("LISTEN_PID");
    int i, n, next = 3, want = 0;
    /* A spec that already holds a socket was seated by hm_adopt_env: a
     * re-exec successor of a socket-activated server. LISTEN_FDS was
     * removed from the environment at the first boot, so counting those
     * specs made every USR2 under systemd croak "not set". */
    for (i = 0; i < cfg->nlspecs; i++)
        if ((cfg->listen_fds || cfg->lspecs[i].inherit) && cfg->lspecs[i].fd < 0)
            want++;
    if (!want) return;
#ifdef _WIN32
    croak("Hyperman->run: socket activation is not supported on Windows");
#else
    if (!nfds || !lpid)
        croak("Hyperman->run: a listener asks to inherit a socket but LISTEN_FDS "
              "and LISTEN_PID are not set");
    if (atol(lpid) != (long)hm_os_getpid())
        croak("Hyperman->run: LISTEN_PID is %s, this is %d: the sockets were "
              "not handed to this process", lpid, hm_os_getpid());
    if (cfg->reuseport)
        croak("Hyperman->run: an inherited socket cannot be rebound per worker; "
              "drop reuseport");
    n = atoi(nfds);
    for (i = 0; i < cfg->nlspecs; i++) {
        hm_listener_spec *s = &cfg->lspecs[i];
        struct sockaddr_storage got;
        socklen_t gl = sizeof(got);
        int fd, acc = 0;
        socklen_t al = sizeof(acc);
        if (!(cfg->listen_fds || s->inherit) || s->fd >= 0) continue;
        if (next - 3 >= n)
            croak("Hyperman->run: LISTEN_FDS=%d but listener %d wants one too", n, i);
        fd = next++;
        if (s->http3)
            croak("Hyperman->run: an inherited listener cannot carry http3");
        if (getsockname(fd, (struct sockaddr *)&got, &gl) < 0)
            croak("Hyperman->run: inherited fd %d: getsockname: %s", fd, strerror(errno));
        if (got.ss_family != s->sa.ss_family)
            croak("Hyperman->run: inherited fd %d is address family %d, listener %d "
                  "resolved to %d", fd, (int)got.ss_family, i, (int)s->sa.ss_family);
        if (getsockopt(fd, SOL_SOCKET, SO_ACCEPTCONN, &acc, &al) == 0 && !acc)
            croak("Hyperman->run: inherited fd %d is not a listening socket", fd);
        hm_set_nonblock(fd);
        /* As a bound listener is (hm_make_listener): not into anything the
         * application execs; hm_reexec clears it for the successor alone. */
        (void)fcntl(fd, F_SETFD, fcntl(fd, F_GETFD, 0) | FD_CLOEXEC);
        s->fd = fd;
    }
    hm_env_unset(aTHX_ "LISTEN_FDS");
    hm_env_unset(aTHX_ "LISTEN_PID");
    hm_env_unset(aTHX_ "LISTEN_FDNAMES");
#endif
}

/* ---- re-exec reload (USR2) -------------------------------------------------
 *
 * HUP reforks the pool from this process, which still holds the application
 * as it was compiled at boot. USR2 starts a NEW supervisor - a fresh perl, a
 * fresh compile of the application - that takes the bound listeners over,
 * forks its own pool, and only then asks this one to drain. The listen fds
 * are held by one process or the other the whole time, so no connection is
 * refused. A successor that fails to start changes nothing here; this
 * supervisor keeps serving and says so.
 *
 * What crosses the exec: the listener fds, named in HYPERMAN_REEXEC by the
 * host:port (or path) each was bound for, so the successor's configuration
 * may reorder or add listeners; the arena NAME, when there is one, so both
 * pools share the one arena during the overlap; and the old supervisor's
 * pid, to TERM. An anonymous arena is not shared: it is a private mapping
 * with no name to attach to, and the successor starts a fresh one (the
 * denylist and the rate counters begin again at the handoff). */

#ifndef _WIN32
static void hm_reexec(pTHX_ const hm_worker_cfg *cfg, const sigset_t *orig) {
    char env[4096];
    size_t n = 0;
    int i, k;
    pid_t pid;
    SV *perl;
    if (hm_resup_pid > 0 && kill(hm_resup_pid, 0) == 0) {
        hm_errlog("Hyperman: USR2 ignored: successor %d is still starting",
                  (int)hm_resup_pid);
        return;
    }
    if (!hm_boot_argv || !hm_boot_argv[0]) {
        hm_errlog("Hyperman: USR2 ignored: no argv to re-exec with");
        return;
    }
    k = snprintf(env + n, sizeof(env) - n, "sup=%d", hm_os_getpid());
    if (k < 0 || (size_t)k >= sizeof(env) - n) return;
    n += (size_t)k;
    if (cfg->arena_name && cfg->arena_name_len) {
        k = snprintf(env + n, sizeof(env) - n, ";arena=%.*s",
                     (int)cfg->arena_name_len, cfg->arena_name);
        if (k < 0 || (size_t)k >= sizeof(env) - n) {
            hm_errlog("Hyperman: USR2 ignored: the arena name does not fit the "
                      "handover environment");
            return;
        }
        n += (size_t)k;
    }
    for (i = 0; i < cfg->nlspecs; i++) {
        const hm_listener_spec *s = &cfg->lspecs[i];
        if (s->fd < 0) continue;                  /* reuseport: nothing held */
        if (s->path)
            k = snprintf(env + n, sizeof(env) - n, ";u=%s|%d", s->path, s->fd);
        else
            k = snprintf(env + n, sizeof(env) - n, ";l=%s|%d|%d|%d",
                         s->host && *s->host ? s->host : "0.0.0.0", s->port,
                         s->fd, s->udp_fd);
        if (k < 0 || (size_t)k >= sizeof(env) - n) {
            hm_errlog("Hyperman: USR2 ignored: too many listeners to hand over");
            return;
        }
        n += (size_t)k;
    }
    perl = get_sv("\030", 0);                     /* $^X */
    if (!perl || !SvOK(perl)) {
        hm_errlog("Hyperman: USR2 ignored: $^X is not set");
        return;
    }
    pid = fork();
    if (pid < 0) {
        hm_errlog("Hyperman: USR2: fork: %s", strerror(errno));
        return;
    }
    if (pid == 0) {
        /* The supervisor runs with its signals BLOCKED and the mask survives
         * exec, as does a pending alarm: a successor born with either would
         * never wake in its own sigsuspend. */
        sigprocmask(SIG_SETMASK, orig, NULL);
        alarm(0);
        signal(SIGTERM, SIG_DFL); signal(SIGINT,  SIG_DFL);
        signal(SIGHUP,  SIG_DFL); signal(SIGUSR1, SIG_DFL);
        signal(SIGUSR2, SIG_DFL); signal(SIGCHLD, SIG_DFL);
        signal(SIGALRM, SIG_DFL);
        for (i = 0; i < cfg->nlspecs; i++) {
            const hm_listener_spec *s = &cfg->lspecs[i];
            if (s->fd >= 0)     (void)fcntl(s->fd,     F_SETFD, 0);
            if (s->udp_fd >= 0) (void)fcntl(s->udp_fd, F_SETFD, 0);
        }
        setenv("HYPERMAN_REEXEC", env, 1);
        execv(SvPV_nolen(perl), hm_boot_argv);
        _exit(127);
    }
    hm_resup_pid = pid;
    hm_errlog("Hyperman: USR2: successor %d started; serving until it takes over",
              (int)pid);
}

/* The successor's half: HYPERMAN_REEXEC names the sockets and the arena.
 * Runs after the addresses are resolved and before anything binds. */
static void hm_adopt_env(pTHX_ hm_worker_cfg *cfg) {
    const char *raw = getenv("HYPERMAN_REEXEC");
    char *buf, *tok, *save = NULL;
    int i;
    if (!raw || !*raw) return;
    buf = strdup(raw);
    if (!buf) return;
    hm_env_unset(aTHX_ "HYPERMAN_REEXEC");
    for (i = 0; i < cfg->nlspecs; i++) { cfg->lspecs[i].fd = -1; cfg->lspecs[i].udp_fd = -1; }
    for (tok = strtok_r(buf, ";", &save); tok; tok = strtok_r(NULL, ";", &save)) {
        if (strncmp(tok, "sup=", 4) == 0) {
            hm_resup_old = (pid_t)atol(tok + 4);
        } else if (strncmp(tok, "arena=", 6) == 0) {
            if (!cfg->arena_name || !cfg->arena_name_len) {
                cfg->arena_name     = strdup(tok + 6);    /* process lifetime */
                cfg->arena_name_len = strlen(tok + 6);
            }
        } else if (strncmp(tok, "l=", 2) == 0 || strncmp(tok, "u=", 2) == 0) {
            int unix_ = tok[0] == 'u';
            char *f1, *f2, *f3, *f4, *s2 = NULL;
            int fd = -1, udp = -1, port = 0, matched = 0;
            const char *name;
            f1 = strtok_r(tok + 2, "|", &s2);
            f2 = strtok_r(NULL, "|", &s2);
            f3 = strtok_r(NULL, "|", &s2);
            f4 = strtok_r(NULL, "|", &s2);
            if (!f1 || !f2) continue;
            name = f1;
            if (unix_) { fd = atoi(f2); }
            else { if (!f3) continue; port = atoi(f2); fd = atoi(f3); udp = f4 ? atoi(f4) : -1; }
            /* reuseport pools rebind per worker and hold nothing to adopt */
            if (!cfg->reuseport) {
                for (i = 0; i < cfg->nlspecs; i++) {
                    hm_listener_spec *s = &cfg->lspecs[i];
                    if (s->fd >= 0) continue;
                    if (unix_ ? (s->path && strcmp(s->path, name) == 0)
                              : (!s->path && s->port == port
                                 && strcmp(s->host && *s->host ? s->host : "0.0.0.0", name) == 0)) {
                        struct sockaddr_storage got;
                        socklen_t gl = sizeof(got);
                        int acc = 0;
                        socklen_t al = sizeof(acc);
                        if (getsockname(fd, (struct sockaddr *)&got, &gl) < 0
                            || got.ss_family != s->sa.ss_family
                            || (getsockopt(fd, SOL_SOCKET, SO_ACCEPTCONN, &acc, &al) == 0 && !acc))
                            croak("Hyperman->run: re-exec: handed fd %d for %s is not "
                                  "the listener it was said to be", fd, name);
                        hm_set_nonblock(fd);
                        (void)fcntl(fd, F_SETFD, fcntl(fd, F_GETFD, 0) | FD_CLOEXEC);
                        s->fd = fd;
                        if (udp >= 0 && s->http3) {
                            hm_set_nonblock(udp);
                            (void)fcntl(udp, F_SETFD, fcntl(udp, F_GETFD, 0) | FD_CLOEXEC);
                            s->udp_fd = udp;
                        } else if (udp >= 0) {
                            hm_os_close(udp);
                        }
                        matched = 1;
                        break;
                    }
                }
            }
            if (!matched) {                     /* a listener the new config dropped */
                if (fd >= 0)  hm_os_close(fd);
                if (udp >= 0) hm_os_close(udp);
            }
        }
    }
    free(buf);
    hm_errlog("Hyperman: re-exec successor %d: sockets adopted from supervisor %d",
              hm_os_getpid(), (int)hm_resup_old);
}

/* The successor's pool is up: tell the old supervisor to drain. Waited for a
 * scoreboard row per child (ten seconds at most), or one second flat when
 * there is no board, so the old pool is not torn down ahead of the new one
 * answering. */
static void hm_reexec_takeover(const pid_t *kids, int nkids) {
    int i, tries;
    if (hm_resup_old <= 0) return;
    if (hm_sa_board_live()) {
        for (tries = 0; tries < 100; tries++) {
            int ready = 1;
            for (i = 0; i < nkids; i++) {
                sa_sb_reading rd;
                if (hm_sa_board_row_of((uint64_t)kids[i], &rd) < 0) { ready = 0; break; }
            }
            if (ready) break;
            poll(NULL, 0, 100);
        }
    } else {
        poll(NULL, 0, 1000);
    }
    if (kill(hm_resup_old, SIGTERM) == 0)
        hm_errlog("Hyperman: re-exec: pool up, supervisor %d asked to drain",
                  (int)hm_resup_old);
    hm_resup_old = 0;
}
#endif /* !_WIN32 */

static void hm_run_server(pTHX_ hm_worker_cfg *cfg) {
    int workers = cfg->nworkers;

    /* Before the arena, before the bind: an unknown user or group is refused
     * while nothing is held, and on Windows the whole family is. */
    hm_resolve_drop(aTHX_ cfg);
#ifndef _WIN32
    /* The umask FIRST: it is asked for so that what the server creates - the
     * unix socket, the pid file, the logs - comes out with it, and all of
     * those are made below. Applied at the drop it reached only the files
     * the application made. */
    if (cfg->umask_set) (void)umask((mode_t)cfg->umask_val);
#endif
    /* Likewise every listen address: a host that does not resolve is a
     * croak here, never a bind of INADDR_ANY in its place. */
    {
        int i;
        char err[128];
        for (i = 0; i < cfg->nlspecs; i++) {
            hm_listener_spec *s = &cfg->lspecs[i];
            s->fd = -1;         /* calloc'd, and 0 is a descriptor */
            s->udp_fd = -1;
            if (s->path) {
                if (cfg->reuseport)
                    croak("Hyperman->run: reuseport cannot rebind a path listener "
                          "per worker (each rebind would unlink the other's socket)");
                if (hm_resolve_unix(s->path, &s->sa, &s->salen, err, sizeof(err)) < 0)
                    croak("Hyperman->run: listen path '%s': %s", s->path, err);
            }
            else if (hm_resolve(s->host, s->port, SOCK_STREAM, &s->sa, &s->salen,
                                err, sizeof(err)) < 0)
                croak("Hyperman->run: listen host '%s': %s",
                      s->host ? s->host : "", err);
        }
    }
#ifndef _WIN32
    hm_adopt_env(aTHX_ cfg);        /* a re-exec successor: the old sockets */
#endif
    hm_adopt_fds(aTHX_ cfg);

    if (!hm_empty_input)
        hm_empty_input = hm_new_input(aTHX_ "", 0);   /* inherited by workers */
    if (workers < 1)                       /* default: one per CPU */
        workers = hm_os_ncpu();               /* always 1 on Windows */
    cfg->nworkers = workers;

    /* The Shared::Arena, created HERE - before any worker forks - so every
     * worker inherits the one mapping, and from Perl, so Hyperman->arena is
     * the real object with every tenant on it. The denylist and the rate
     * counters are two maps in it; static `deny` entries are loaded now, in
     * the parent, so they are live from the first accepted connection.
     * Single-worker (dev) mode maps it too, so the ABI behaves the same
     * whether or not a supervisor forks. */
    hm_sa_open(aTHX_ cfg->arena_name, cfg->arena_name_len,
               cfg->arena_size ? cfg->arena_size
                               : hm_sa_bytes(cfg->deny_cap, cfg->rate_cap,
                                             cfg->bus_slots, cfg->bus_slot_size));
    {
        const char *which = NULL;
        int err = 0;
        if (hm_sa_abuse_open(cfg->deny_cap, cfg->rate_cap, &which, &err) < 0)
            warn("Hyperman: could not carve the \"%s\" map in the arena "
                 "(Shared::Arena error %d): the denylist and the rate "
                 "counters fail open; raise arena_size", which, err);
    }

    /* QUIC's stateless-reset and (later) Retry secret, generated HERE - once,
     * before any worker forks - for the same reason the TLS ticket key is:
     * under SO_REUSEPORT a client's token can land on a different worker than
     * the one that issued it, and a per-worker secret would make every such
     * token unverifiable. Inherited across the fork, so every worker agrees. */
    hm_quic_secret_init();

    /* The message bus, a ring in the same arena and for the same reason: a
     * ring created after the fork would be one ring per worker, which is a
     * bus that delivers to nobody. Opened even in single-worker mode, so the
     * ABI behaves the same whether or not a supervisor forks. */
    {
        int err = 0;
        if (hm_sa_bus_open(cfg->bus_slots, cfg->bus_slot_size, &err) < 0)
            warn("Hyperman: could not carve the bus ring in the arena "
                 "(Shared::Arena error %d): the bus is local-only; raise "
                 "arena_size", err);
    }
    /* ... and the wakeup descriptors, HERE for the same reason as the ring
     * and one more: a pipe created inside a worker is invisible to its
     * siblings, so a bus built after the fork delivers to some workers and
     * not others.
     *
     * TWO GENERATIONS' WORTH, plus the supervisor's own. A HUP or USR2 forks
     * the replacement pool while the old one is still draining, so both exist
     * at once and both want descriptors - and sized for one generation the
     * fresh workers found every slot held and came up deaf, which outlives the
     * recycle that caused it. Shared::Arena caps this at its own ceiling. */
    (void)hm_sa_bus_wake_init((uint32_t)(workers > 0 ? 2 * workers + 1 : 2));
    /* The worker scoreboard, one row per worker plus one, and the
     * distinct-client sketch when asked for. Neither is on a hot path by
     * default: the board is mirrored by a timer, the sketch is opt-in. */
    {
        int err = 0;
        /* Two generations' worth, like the wakers: a re-exec successor's
         * pool shares this board with the pool it replaces until that one
         * has drained, and a row is not reclaimable while its worker lives. */
        if (hm_sa_board_open((uint32_t)(workers > 0 ? 2 * workers + 2 : 2), &err) < 0)
            warn("Hyperman: could not carve the worker scoreboard in the arena "
                 "(Shared::Arena error %d): pool_stats is empty; raise "
                 "arena_size", err);
        if (cfg->distinct_clients && hm_sa_hll_open(&err) < 0)
            warn("Hyperman: could not carve the distinct-client sketch in the "
                 "arena (Shared::Arena error %d): distinct_clients is off", err);
    }
    if (cfg->deny && SvROK(cfg->deny) && SvTYPE(SvRV(cfg->deny)) == SVt_PVAV) {
        AV *av = (AV *)SvRV(cfg->deny);
        SSize_t i, n = av_len(av) + 1;
        for (i = 0; i < n; i++) {
            SV **e = av_fetch(av, i, 0);
            if (e && *e && SvOK(*e)) hm_sa_deny_add(SvPV_nolen(*e), 0);
        }
    }

#ifndef SO_REUSEPORT
    if (cfg->reuseport) croak("Hyperman: SO_REUSEPORT not supported on this platform");
#endif
#ifndef __linux__
    if (cfg->affinity) croak("Hyperman: CPU affinity is Linux-only");
#endif

    /* Per listener: build the TLS context before forking, so a bad cert/key
     * fails fast and the (read-only) SSL_CTX is shared copy-on-write across
     * workers. Then bind - in the parent, unless reuseport (each worker binds
     * its own). Under reuseport the parent still binds one probe fd so an
     * unusable address (e.g. a privileged port without root) fails fast. */
    {
        int i;
        for (i = 0; i < cfg->nlspecs; i++) {
            hm_listener_spec *s = &cfg->lspecs[i];
            if (s->tls_cert && s->tls_key) {
                s->tls_ctx = hm_tls_ctx_build(aTHX_ s->tls_cert, s->tls_key,
                                              s->tls_ca, s->tls_verify,
                                              s->tls_sni, s->http2, NULL, 0);
                if (!s->tls_ctx)
                    croak("Hyperman: TLS: could not initialise from cert '%s' "
                          "/ key '%s'", s->tls_cert, s->tls_key);
            }
        }
    }

    /* The access-log format, compiled once here so an unknown directive is a
     * croak before the fork and a line is a walk over an array after it. The
     * tokens are process-lifetime: every worker reads the parent's copy. */
    if (cfg->log_format && strcmp(cfg->log_format, "json") == 0) {
        hm_logfmt_json(aTHX_ &cfg->logtok, &cfg->nlogtok);
        cfg->log_json = 1;
        cfg->log_wants_t0 = 1;
    } else {
        char err[64];
        if (hm_logfmt_compile(aTHX_ cfg->log_format ? cfg->log_format : HM_LOG_COMBINED,
                              &cfg->logtok, &cfg->nlogtok, &cfg->log_wants_t0,
                              err, sizeof(err)) < 0)
            croak("Hyperman->run: access_log_format: unknown directive %s", err);
    }
    /* The error log, likewise opened once and inherited. */
    if (cfg->err_path && cfg->err_fd < 0) {
        cfg->err_fd = hm_os_open_file(cfg->err_path,
                           O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0644);
        if (cfg->err_fd < 0)
            croak("Hyperman: open error_log '%s': %s", cfg->err_path, strerror(errno));
    }
    if (cfg->err_fd >= 0) hm_err_fd = cfg->err_fd;

    /* Open the access-log file once in the parent so all workers share (and
     * O_APPEND to) the same fd - whole-line writes stay atomic between them. */
    if (cfg->log_path && cfg->log_fd < 0) {
        cfg->log_fd = hm_os_open_file(cfg->log_path,
                           O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0644);
        if (cfg->log_fd < 0)
            croak("Hyperman: open access_log '%s': %s",
                  cfg->log_path, strerror(errno));
    }

    {
        int i;
        for (i = 0; i < cfg->nlspecs; i++) {
            hm_listener_spec *s = &cfg->lspecs[i];
            if (s->fd >= 0) continue;           /* adopted from LISTEN_FDS */
#ifndef _WIN32
            /* `mode` through the umask around the bind, not a chmod of the
             * path afterwards: in a directory others can write to, the path
             * could be swapped for a symlink between the two and root would
             * chmod whatever it pointed at. A bind creates the socket with
             * 0777 less the umask, so the mask is the mode's complement. */
            mode_t saved_umask = 0;
            int have_saved = 0;
            if (s->path && s->mode_set) {
                saved_umask = umask((mode_t)(~(unsigned)s->mode & 0777u));
                have_saved = 1;
            }
#endif
            s->fd = hm_make_listener((struct sockaddr *)&s->sa, s->salen, cfg->reuseport);
#ifndef _WIN32
            if (have_saved) (void)umask(saved_umask);
#endif
            if (s->fd < 0)
                croak("Hyperman: bind %s%s:%d: %s", s->path ? s->path : "",
                      s->path ? "" : s->host, s->port, strerror(errno));
            s->udp_fd = -1;
            if (s->http3) {
                s->udp_fd = hm_make_udp_listener((struct sockaddr *)&s->sa, s->salen, cfg->reuseport);
                if (s->udp_fd < 0)
                    croak("Hyperman: bind udp %s:%d: %s", s->host, s->port,
                          strerror(errno));
            }
            if (cfg->reuseport) {                                    /* probe only */
                hm_os_close(s->fd); s->fd = -1;
                if (s->udp_fd >= 0) { hm_os_close(s->udp_fd); s->udp_fd = -1; }
            }
        }
    }

    /* Everything that needed root is held: the ports, the logs, the arena.
     * Record who we are, then stop being root, before the first fork so
     * every worker is born as the configured user. Under reuseport the
     * workers rebind their ports themselves, which needs the privilege for a
     * port below 1024; that pairing is refused rather than half-honoured. */
    if (cfg->reuseport && cfg->drop_uid_set) {
        int i;
        for (i = 0; i < cfg->nlspecs; i++)
            if (cfg->lspecs[i].port < 1024)
                croak("Hyperman->run: user => with reuseport => 1 cannot rebind "
                      "port %d as the dropped user; drop reuseport or use a "
                      "port above 1023", cfg->lspecs[i].port);
    }
    hm_write_pidfile(aTHX_ cfg);
    hm_drop_privileges(aTHX_ cfg);

    if (workers == 1) {
        /* dev mode: run the worker in this process, no supervisor. reuseport
         * needs a real bound fd here since there is no per-worker rebind. */
        if (cfg->reuseport) {
            int i;
            for (i = 0; i < cfg->nlspecs; i++) {
                hm_listener_spec *s = &cfg->lspecs[i];
                s->fd = hm_make_listener((struct sockaddr *)&s->sa, s->salen, 1);
                if (s->fd < 0)
                    croak("Hyperman: bind %s:%d: %s", s->host, s->port,
                          strerror(errno));
                if (s->http3) {
                    s->udp_fd = hm_make_udp_listener((struct sockaddr *)&s->sa, s->salen, 1);
                    if (s->udp_fd < 0)
                        croak("Hyperman: bind udp %s:%d: %s", s->host, s->port,
                              strerror(errno));
                }
            }
        }
        hm_worker(aTHX_ cfg, NULL);
        hm_unlink_pidfile(cfg->pid_path);
        return;
    }

#ifdef _WIN32
    /* Not a silent downgrade: an application sized for a pool is told, at
     * boot, in the log it already reads. */
    croak("Hyperman: a worker pool needs fork(2); Windows runs a single "
          "worker (workers => 1).");
#else
    {
        int i;
        int backoff = 0;
        struct sigaction sa;

        for (i = 0; i < workers; i++) {
            pid_t pid = hm_spawn(aTHX_ cfg, i);
            if (pid < 0) {
                /* take the pool down with us: croaking out of run() leaves
                 * nobody supervising the workers already forked, and they
                 * would keep the listening socket alive forever. */
                int j;
                for (j = 0; j < hm_nchildren; j++) kill(hm_children[j], SIGTERM);
                croak("Hyperman: fork: %s", strerror(errno));
            }
            hm_child_start[hm_nchildren] = time(NULL);
            hm_child_widx[hm_nchildren]  = i;
            hm_children[hm_nchildren++] = pid;
        }

        memset(&sa, 0, sizeof(sa));
        sa.sa_handler = hm_sup_sig;      /* no SA_RESTART: waitpid must EINTR */
        sigemptyset(&sa.sa_mask);
        sigaction(SIGTERM, &sa, NULL);
        sigaction(SIGINT,  &sa, NULL);
        sigaction(SIGHUP,  &sa, NULL);
        sigaction(SIGUSR1, &sa, NULL);
        sigaction(SIGUSR2, &sa, NULL);
        /* SIGCHLD must be delivered so sigsuspend() below wakes when a worker
         * exits; hm_sup_sig() treats it as a no-op wakeup. */
        sigaction(SIGCHLD, &sa, NULL);
        /* ALRM is the supervisor's only clock: a look at the scoreboard every
         * few seconds for a worker that is alive and not moving.
         *
         * An alarm the CALLER already had pending is not ours to cancel. A
         * watchdog that says `alarm(120); $server->run` has asked to be killed
         * at that deadline, and taking SIGALRM over - a handler that only sets
         * a flag, re-armed every tick - silently turns that into a promise
         * nothing keeps, leaving an unsupervised pool running for ever.
         *
         * So the pending deadline is read out and carried on the tick, which
         * is armed for it even with no board to watch. Letting the signal
         * through unhandled instead would kill the supervisor where it stood
         * and leave the workers holding the listening socket with nobody to
         * shut them down - a watchdog that takes half the pool with it is not
         * one. The tick is three seconds, so the deadline is met to within
         * that, and what follows is the death the caller asked for. */
        hm_sup_alarm_at = 0;
        {
            unsigned pending = alarm(0);
            if (pending) hm_sup_alarm_at = time(NULL) + (time_t)pending;
            if (hm_sa_board_live() || hm_sup_alarm_at) {
                sigaction(SIGALRM, &sa, NULL);
                alarm(HM_SUP_TICK_SECS);
            }
        }

        /* A successor: the pool is forked and serving on the adopted sockets;
         * the supervisor that handed them over can drain now. AFTER the
         * handlers above: this waits up to ten seconds, and the pid file
         * already names this process, so a deploy script retrying its
         * `kill -USR2 $(cat pidfile)` into that window hit SIG_DFL and
         * killed the successor, orphaning its workers on the sockets. */
        hm_reexec_takeover(hm_children, hm_nchildren);

        /* Race-free supervision: keep the acted-on signals blocked, check the
         * flags, reap without blocking, then sigsuspend() to wait for the next
         * signal atomically. A plain blocking waitpid() has a lost-wakeup
         * window - a signal arriving between a flag check and re-entering
         * waitpid() goes unserviced until some child happens to exit, which
         * for USR1 (stats dump) on an otherwise idle pool could be never. */
        {
            sigset_t block, orig;
            sigemptyset(&block);
            sigaddset(&block, SIGTERM); sigaddset(&block, SIGINT);
            sigaddset(&block, SIGHUP);  sigaddset(&block, SIGUSR1);
            sigaddset(&block, SIGUSR2); sigaddset(&block, SIGCHLD);
            sigaddset(&block, SIGALRM);
            sigprocmask(SIG_BLOCK, &block, &orig);

            for (;;) {
                int   st;
                pid_t pid;

                if (hm_sup_term) {
                    for (i = 0; i < hm_nchildren; i++)
                        kill(hm_children[i], SIGTERM);
                    /* workers self-bound their drain with shutdown_grace.
                     * Waited for BY PID: a re-exec successor is a child of
                     * this process too, and it does not exit - it is the
                     * one that asked us to; waitpid(-1) would wait for it
                     * for ever. */
                    for (i = 0; i < hm_nchildren; i++) {
                        while (waitpid(hm_children[i], &st, 0) < 0 && errno == EINTR) { }
                    }
                    break;
                }

                if (hm_sup_usr1) {
                    hm_sup_usr1 = 0;
                    /* With a scoreboard the supervisor has every worker's
                     * numbers already: one aggregate line and one per row,
                     * a dead row marked. Without one, each worker prints its
                     * own line as it always did. */
                    if (hm_sa_board_live()) hm_sa_board_print(stderr);
                    else
                        for (i = 0; i < hm_nchildren; i++)
                            kill(hm_children[i], SIGUSR1);
                }

                if (hm_sup_tick) {
                    hm_sup_tick = 0;
                    /* The caller's own deadline, if it had one: take the pool
                     * down and then let SIGALRM do what it would have done. */
                    if (hm_sup_alarm_at && time(NULL) >= hm_sup_alarm_at) {
                        hm_sup_alarmed = 1;
                        hm_sup_term    = 1;
                        continue;
                    }
                    hm_sup_watch_board();
                    alarm(HM_SUP_TICK_SECS);
                }

                if (hm_sup_usr2) {
                    hm_sup_usr2 = 0;
                    hm_reexec(aTHX_ cfg, &orig);
                }

                if (hm_sup_hup) {
                    pid_t  old[1024];
                    time_t old_start[1024];
                    int    old_widx[1024];
                    int nold = hm_nchildren, nfresh = 0;
                    memcpy(old, hm_children, nold * sizeof(pid_t));
                    memcpy(old_start, hm_child_start, nold * sizeof(time_t));
                    memcpy(old_widx, hm_child_widx, nold * sizeof(int));
                    hm_sup_hup = 0;
                    hm_nchildren = 0;
                    for (i = 0; i < workers; i++) {
                        /* a failed fork must never reach hm_children: -1 there
                         * turns the TERM sweep below into kill(-1, SIGTERM),
                         * which signals every process this uid owns. */
                        pid_t pid = hm_spawn(aTHX_ cfg, i);
                        if (pid < 0) continue;
                        hm_child_start[hm_nchildren] = time(NULL);
                        hm_child_widx[hm_nchildren]  = i;
                        hm_children[hm_nchildren++]  = pid;
                        nfresh++;
                    }
                    /* retire one old worker per fresh one. Short of processes
                     * the survivors stay in the live set, so a HUP under fork
                     * pressure shrinks the pool instead of emptying it. */
                    for (i = 0; i < nold; i++) {
                        if (i < nfresh) {
                            kill(old[i], SIGTERM);   /* drain gracefully */
                        } else {
                            hm_child_start[hm_nchildren] = old_start[i];
                            hm_child_widx[hm_nchildren]  = old_widx[i];
                            hm_children[hm_nchildren++]  = old[i];
                        }
                    }
                }

                /* reap every exited child without blocking, respawning the
                 * live set. Recycled workers exiting are expected; only a
                 * death in the live set is respawned. Abnormal quick deaths
                 * (crash loops) back off exponentially; clean exits respawn
                 * immediately. */
                while ((pid = waitpid(-1, &st, WNOHANG)) > 0) {
                    int live = 0;
                    int dead_widx = 0;
                    time_t started = 0;
                    if (pid == hm_resup_pid) {
                        /* It never took over (a successor that did would have
                         * TERMed us first). Nothing here changed, so nothing
                         * is undone: this pool serves on. Except the pid
                         * file, which it may have written before it died,
                         * and which would then name a dead process for the
                         * next `kill -USR2 $(cat pidfile)`: re-asserted. */
                        hm_errlog("Hyperman: re-exec successor %d exited (%s %d) "
                                  "before taking over; still serving", (int)pid,
                                  WIFEXITED(st) ? "status" : "signal",
                                  WIFEXITED(st) ? WEXITSTATUS(st) : WTERMSIG(st));
                        hm_resup_pid = 0;
                        (void)hm_write_pidfile_(aTHX_ cfg, 0);
                        continue;
                    }
                    for (i = 0; i < hm_nchildren; i++) {
                        if (hm_children[i] == pid) {
                            started   = hm_child_start[i];
                            dead_widx = hm_child_widx[i];
                            hm_nchildren--;
                            hm_children[i]    = hm_children[hm_nchildren];
                            hm_child_start[i] = hm_child_start[hm_nchildren];
                            hm_child_widx[i]  = hm_child_widx[hm_nchildren];
                            live = 1;
                            break;
                        }
                    }
                    if (!live) continue;

                    {
                        int crashed = !(WIFEXITED(st) && WEXITSTATUS(st) == 0);
                        if (crashed && time(NULL) - started < 5) {
                            backoff = backoff ? backoff * 2 : 1;
                            if (backoff > 30) backoff = 30;
                            /* keep TERM responsive during crash-loop backoff */
                            sigprocmask(SIG_SETMASK, &orig, NULL);
                            sleep((unsigned)backoff);
                            sigprocmask(SIG_BLOCK, &block, NULL);
                        } else if (!crashed || time(NULL) - started >= 5) {
                            backoff = 0;
                        }
                    }
                    {
                        /* The replacement takes the DEAD worker's index, not
                         * the position it is about to occupy here.
                         *
                         * Those are different numbers, because the array above
                         * is kept packed by swapping the last entry down: kill
                         * the worker at position 0 of three and the survivor
                         * from position 2 moves into it, so the next free
                         * position is 2 - an index a LIVE worker still holds.
                         * Spawned with that, the replacement asked for a waker
                         * slot its own sibling owned, was refused, and ran
                         * deaf to the bus for the rest of its life while the
                         * slot its predecessor had died holding went spare. It
                         * was pinned to that sibling's CPU too.
                         *
                         * So identity comes from the child that left, and only
                         * the bookkeeping position is reused. */
                        int slot = hm_nchildren;
                        pid_t fresh = hm_spawn(aTHX_ cfg, dead_widx);
                        if (fresh > 0) {            /* never record a -1 */
                            hm_child_start[slot] = time(NULL);
                            hm_child_widx[slot]  = dead_widx;
                            hm_children[slot]    = fresh;
                            hm_nchildren = slot + 1;
                        }
                    }
                    if (hm_sup_term) break;   /* TERM arrived during backoff */
                }

                if (hm_sup_term) continue;    /* serviced at the top of the loop */

                /* atomically unblock and sleep until the next signal */
                sigsuspend(&orig);
            }

            sigprocmask(SIG_SETMASK, &orig, NULL);
            hm_unlink_pidfile(cfg->pid_path);

            /* The pool is down because the caller's own alarm came due, so
             * finish the job the alarm was set to do. */
            if (hm_sup_alarmed) {
                signal(SIGALRM, SIG_DFL);
                raise(SIGALRM);
            }
        }
    }
#endif /* !_WIN32 */
}

/* loop object <-> pointer */
static hm_loop *hm_loop_from_sv(pTHX_ SV *sv) {
    if (!(sv && SvROK(sv) && sv_derived_from(sv, "Hyperman::Loop")))
        croak("not a Hyperman::Loop");
    return INT2PTR(hm_loop *, SvIV(SvRV(sv)));
}

/* The Perl wrapper for a loop. ONE blessed referent per loop, however many
 * times this is asked: Hyperman::Loop->new, psgix.loop, Hyperman->loop and
 * the ABI's sv_of_loop used to each mint their own, and the first wrapper
 * whose refcount hit zero freed a Perl-owned loop under every other one.
 * The loop keeps a WEAK copy, so holding the wrapper is what keeps the loop,
 * and the loop holding its own wrapper keeps nothing alive. */
static SV *hm_loop_to_sv(pTHX_ hm_loop *loop) {
    SV *rv;
    if (loop->self_sv && SvROK(loop->self_sv)) return newSVsv(loop->self_sv);
    rv = newRV_noinc(newSViv(PTR2IV(loop)));
    sv_bless(rv, gv_stashpv("Hyperman::Loop", GV_ADD));
    if (loop->self_sv) SvREFCNT_dec(loop->self_sv);   /* a cleared weak ref */
    loop->self_sv = newSVsv(rv);
    sv_rvweaken(loop->self_sv);
    return rv;
}

/* install_await: $Hyperman::Future::AWAIT pumps this loop */
XS_INTERNAL(hm_xs_await_cb);
XS_INTERNAL(hm_xs_await_cb) {
    dXSARGS;
    hm_clos *cl = hm_clos_of(aTHX_ cv);
    if (!cl || items < 1) XSRETURN_EMPTY;
    hm_loop_run(aTHX_ hm_loop_from_sv(aTHX_ cl->a), ST(0));
    XSRETURN_EMPTY;
}

/* A run() option that is a count: a non-negative number, never a string
 * that is not one ("10M" read as 10) and never a negative ("-1" read as
 * UV_MAX, which for max_body was an unbounded request buffer). */
static UV hm_opt_count(pTHX_ SV *val, const char *key) {
    if (!SvOK(val) || SvROK(val)
        || (!SvIOK(val) && !SvNOK(val) && !looks_like_number(val))
        || SvNV(val) < 0)
        croak("Hyperman->run: %s must be a non-negative number, not '%s'",
              key, SvOK(val) ? SvPV_nolen(val) : "undef");
    return SvUV(val);
}

#endif /* HM_CORE_H */


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.