about summary refs log tree commit homepage
path: root/ext/mwrap/httpd.h
diff options
context:
space:
mode:
Diffstat (limited to 'ext/mwrap/httpd.h')
-rw-r--r--ext/mwrap/httpd.h1349
1 files changed, 1349 insertions, 0 deletions
diff --git a/ext/mwrap/httpd.h b/ext/mwrap/httpd.h
new file mode 100644
index 0000000..03aef9f
--- /dev/null
+++ b/ext/mwrap/httpd.h
@@ -0,0 +1,1349 @@
+/*
+ * Copyright (C) mwrap hackers <mwrap-perl@80x24.org>
+ * License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+ *
+ * Single-threaded multiplexing HTTP/1.x AF_UNIX server.
+ * Not using epoll|kqueue here since we don't want to be wasting another
+ * FD for a few clients.
+ *
+ * stdio (via open_memstream) is used for all vector management,
+ * thus everything is a `FILE *'
+ *
+ * Buffering is naive: write in full to a memstream to get an accurate
+ * Content-Length, then write out the header and sendmsg it off.
+ * I'm avoiding a streaming + lazy buffering design based on fopencookie(3)
+ * since that adds more complexity and uses icache.
+ * Supporting gzip would be nice, but linking zlib is not an option since
+ * there's a risk of conflicts if the application links against a different
+ * zlib version.  posix_spawn+gzip isn't an option, either, since we don't
+ * want to generate intrusive SIGCHLD.
+ */
+#ifndef _DEFAULT_SOURCE
+#        define _DEFAULT_SOURCE
+#endif
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <poll.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <errno.h>
+#include <string.h>
+#include <math.h>
+#include <urcu/list.h>
+#include "picohttpparser.h"
+#include "picohttpparser_c.h"
+#include <pthread.h>
+#include <stdbool.h>
+#define URL "https://80x24.org/mwrap-perl.git/about"
+#define TYPE_HTML "text/html; charset=UTF-8"
+#define TYPE_CSV "text/csv"
+#define TYPE_PLAIN "text/plain"
+
+enum mw_qev {
+        MW_QEV_IGNORE = 0,
+        MW_QEV_RD = POLLIN,
+        MW_QEV_WR = POLLOUT
+};
+
+struct mw_fbuf {
+        char *ptr;
+        size_t len;
+        FILE *fp;
+};
+
+struct mw_wbuf { /* for response headers + bodies */
+        struct iovec iov[2];
+        unsigned iov_nr;
+        unsigned iov_written;
+        char bytes[];
+};
+
+#define MW_RBUF_SIZE 8192
+#define MW_NR_NAME 8
+struct mw_h1req { /* HTTP/1.x request (TSD in common (fast) case) */
+        const char *method, *path, *qstr;
+        size_t method_len, path_len, qlen;
+        uint16_t rbuf_len; /* capped by MW_RBUF_SIZE */
+        int pret, minor_ver;
+        size_t nr_hdr;
+        struct phr_header hdr[MW_NR_NAME];
+        char rbuf[MW_RBUF_SIZE]; /* read(2) in to this */
+};
+
+struct mw_h1 { /* each HTTP/1.x client (heap) */
+        int fd;
+        short events; /* for poll */
+        unsigned prev_len:13; /* capped by MW_RBUF_SIZE */
+        unsigned has_input:1;
+        unsigned unused_:2;
+        struct mw_h1req *h1r; /* only for slow clients */
+        unsigned long in_len;
+        struct mw_wbuf *wbuf;
+        struct cds_list_head nd; /* <=> mw_h1d.conn */
+};
+
+struct mw_h1d { /* the daemon + listener, a singleton */
+        int lfd;
+        uint8_t alive; /* set by parent */
+        uint8_t running; /* cleared by child */
+        struct cds_list_head conn; /* <=> mw_h1.nd */
+        /* use open_memstream + fwrite to implement a growing pollfd array */
+        struct mw_fbuf pb; /* pollfd vector */
+        pthread_t tid;
+        struct mw_h1req *shared_h1r; /* shared by all fast clients */
+        size_t pid_len;
+        char pid_str[10];
+};
+
+union mw_sockaddr { /* cast-avoiding convenience :> */
+        struct sockaddr_un un;
+        struct sockaddr any;
+};
+
+static struct mw_h1d g_h1d = { .lfd = -1 };
+
+/* sortable snapshot version of struct src_loc */
+struct h1_src_loc {
+        double mean_life;
+        size_t bytes;
+        size_t allocations;
+        size_t frees;
+        size_t live;
+        size_t max_life;
+        off_t lname_len;
+        const struct src_loc *sl;
+        char *loc_name;
+};
+
+/* sort numeric stuff descending */
+#define CMP_FN(F) static int cmp_##F(const void *x, const void *y) \
+{ \
+        const struct h1_src_loc *a = x, *b = y; \
+        if (a->F < b->F) return 1; \
+        return (a->F > b->F) ? -1 : 0; \
+}
+CMP_FN(bytes)
+CMP_FN(allocations)
+CMP_FN(frees)
+CMP_FN(live)
+CMP_FN(max_life)
+CMP_FN(mean_life)
+#undef CMP_FN
+
+static int cmp_location(const void *x, const void *y)
+{
+        const struct h1_src_loc *a = x, *b = y;
+        return strcmp(a->loc_name, b->loc_name);
+}
+
+/* fields for /each/$MIN{,.csv} endpoints */
+struct h1_tbl {
+        const char *fname;
+        size_t flen;
+        int (*cmp)(const void *, const void *);
+} fields[] = {
+#define F(n) { #n, sizeof(#n) - 1, cmp_##n }
+        F(bytes),
+        F(allocations),
+        F(frees),
+        F(live),
+        F(mean_life),
+        F(max_life),
+        F(location)
+#undef F
+};
+
+static enum mw_qev h1_close(struct mw_h1 *h1)
+{
+        mwrap_assert(h1->fd >= 0);
+        cds_list_del(&h1->nd); /* drop from h1d->conn */
+        close(h1->fd);
+        free(h1->wbuf);
+        free(h1->h1r);
+        free(h1);
+        return MW_QEV_IGNORE;
+}
+
+static enum mw_qev h1_400(struct mw_h1 *h1)
+{
+        /* best-effort response, so no checking send() */
+        static const char r400[] = "HTTP/1.1 400 Bad Request\r\n"
+                "Content-Type: text/html\r\n"
+                "Content-Length: 12\r\n"
+                "Connection: close\r\n\r\n" "Bad Request\n";
+        (void)send(h1->fd, r400, sizeof(r400) - 1, MSG_NOSIGNAL);
+        return h1_close(h1);
+}
+
+static enum mw_qev h1_send_flush(struct mw_h1 *h1)
+{
+        struct mw_wbuf *wbuf = h1->wbuf;
+        struct msghdr mh = { 0 };
+
+        free(h1->h1r);
+        h1->h1r = NULL;
+
+        mh.msg_iov = wbuf->iov + wbuf->iov_written;
+        mh.msg_iovlen = wbuf->iov_nr;
+        do {
+                ssize_t w = sendmsg(h1->fd, &mh, MSG_NOSIGNAL);
+                if (w < 0)
+                        return errno == EAGAIN ? MW_QEV_WR : h1_close(h1);
+                if (w == 0)
+                        return h1_close(h1);
+                while (w > 0) {
+                        if ((size_t)w >= mh.msg_iov->iov_len) {
+                                w -= mh.msg_iov->iov_len;
+                                ++mh.msg_iov;
+                                --mh.msg_iovlen;
+                                ++wbuf->iov_written;
+                                --wbuf->iov_nr;
+                        } else {
+                                uintptr_t x = (uintptr_t)mh.msg_iov->iov_base;
+                                mh.msg_iov->iov_base = (void *)(x + w);
+                                mh.msg_iov->iov_len -= w;
+                                w = 0;
+                        }
+                }
+        } while (mh.msg_iovlen);
+        return h1_close(h1);
+}
+
+static FILE *fbuf_init(struct mw_fbuf *fb)
+{
+        fb->ptr = NULL;
+        fb->fp = open_memstream(&fb->ptr, &fb->len);
+        if (!fb->fp) fprintf(stderr, "open_memstream: %m\n");
+        return fb->fp;
+}
+
+static FILE *wbuf_init(struct mw_fbuf *fb)
+{
+        static const struct mw_wbuf pad;
+        if (fbuf_init(fb)) /* pad space is populated before h1_send_flush */
+                fwrite(&pad, 1, sizeof(pad), fb->fp);
+        return fb->fp;
+}
+
+static int fbuf_close(struct mw_fbuf *fb)
+{
+        int e = ferror(fb->fp) | fclose(fb->fp);
+        fb->fp = NULL;
+        if (e) fprintf(stderr, "ferror|fclose: %m\n");
+        return e;
+}
+
+/* supported by modern gcc + clang */
+#define AUTO_CLOFREE __attribute__((__cleanup__(cleanup_clofree)))
+static void cleanup_clofree(void *ptr)
+{
+        struct mw_fbuf *fb = ptr;
+        if (fb->fp) fclose(fb->fp);
+        free(fb->ptr);
+}
+
+static enum mw_qev h1_res_oneshot(struct mw_h1 *h1, const char *buf, size_t len)
+{
+        struct mw_fbuf fb;
+
+        if (!wbuf_init(&fb))
+                return h1_close(h1);
+
+        fwrite(buf, 1, len, fb.fp);
+        if (fbuf_close(&fb))
+                return h1_close(h1);
+
+        /* fill in the zero padding we added at wbuf_init */
+        mwrap_assert(!h1->wbuf);
+        struct mw_wbuf *wbuf = h1->wbuf = (struct mw_wbuf *)fb.ptr;
+        wbuf->iov_nr = 1;
+        wbuf->iov[0].iov_len = fb.len - sizeof(*wbuf);
+        wbuf->iov[0].iov_base = wbuf->bytes;
+        return h1_send_flush(h1);
+}
+
+#define FPUTS(STR, fp) fwrite(STR, sizeof(STR) - 1, 1, fp)
+static enum mw_qev h1_200(struct mw_h1 *h1, struct mw_fbuf *fb, const char *ct)
+{
+        /*
+         * the HTTP header goes at the END of the body buffer,
+         * we'll rely on iovecs via sendmsg(2) to reorder and clamp it
+         */
+        off_t clen = ftello(fb->fp);
+        if (clen < 0) {
+                fprintf(stderr, "ftello: %m\n");
+                fbuf_close(fb);
+                return h1_close(h1);
+        }
+        clen -= sizeof(struct mw_wbuf);
+        mwrap_assert(clen >= 0);
+        FPUTS("HTTP/1.1 200 OK\r\n"
+                "Connection: close\r\n"
+                "Expires: Fri, 01 Jan 1980 00:00:00 GMT\r\n"
+                "Pragma: no-cache\r\n"
+                "Cache-Control: no-cache, max-age=0, must-revalidate\r\n"
+                "Content-Type: ", fb->fp);
+        fprintf(fb->fp, "%s\r\nContent-Length: %zu\r\n\r\n", ct, (size_t)clen);
+
+        if (fbuf_close(fb))
+                return h1_close(h1);
+
+        /* fill in the zero-padding we added at wbuf_init */
+        mwrap_assert(!h1->wbuf);
+        struct mw_wbuf *wbuf = h1->wbuf = (struct mw_wbuf *)fb->ptr;
+        wbuf->iov_nr = 2;
+        wbuf->iov[0].iov_len = fb->len - ((size_t)clen + sizeof(*wbuf));
+        wbuf->iov[0].iov_base = wbuf->bytes + (size_t)clen;
+        wbuf->iov[1].iov_len = clen;
+        wbuf->iov[1].iov_base = wbuf->bytes;
+        return h1_send_flush(h1);
+}
+
+static enum mw_qev h1_404(struct mw_h1 *h1)
+{
+        static const char r404[] = "HTTP/1.1 404 Not Found\r\n"
+                "Content-Type: text/html\r\n"
+                "Connection: close\r\n"
+                "Content-Length: 10\r\n\r\n" "Not Found\n";
+        return h1_res_oneshot(h1, r404, sizeof(r404) - 1);
+}
+
+#define NAME_EQ(h, NAME) name_eq(h, NAME, sizeof(NAME)-1)
+static int name_eq(const struct phr_header *h, const char *name, size_t len)
+{
+        return h->name_len == len && !strncasecmp(name, h->name, len);
+}
+
+static enum mw_qev h1_do_reset(struct mw_h1 *h1)
+{
+        static const char r200[] = "HTTP/1.1 200 OK\r\n"
+                "Content-Type: text/plain\r\n"
+                "Connection: close\r\n"
+                "Content-Length: 6\r\n\r\n" "reset\n";
+        mwrap_reset();
+        return h1_res_oneshot(h1, r200, sizeof(r200) - 1);
+}
+
+static enum mw_qev h1_do_trim(struct mw_h1 *h1)
+{
+        static const char r200[] = "HTTP/1.1 200 OK\r\n"
+                "Content-Type: text/plain\r\n"
+                "Connection: close\r\n"
+                "Content-Length: 9\r\n\r\n" "trimming\n";
+        malloc_trim(0);
+        return h1_res_oneshot(h1, r200, sizeof(r200) - 1);
+}
+
+static enum mw_qev h1_do_ctl_finish(struct mw_h1 *h1)
+{
+        struct mw_fbuf plain;
+        FILE *fp = wbuf_init(&plain);
+        if (!fp) return h1_close(h1);
+        fprintf(fp, "MWRAP=bt:%u\n", (unsigned)CMM_LOAD_SHARED(bt_req_depth));
+        return h1_200(h1, &plain, TYPE_PLAIN);
+}
+
+#define PATH_SKIP(h1r, pfx) path_skip(h1r, pfx, sizeof(pfx) - 1)
+static const char *path_skip(struct mw_h1req *h1r, const char *pfx, size_t len)
+{
+        if (h1r->path_len > len && !memcmp(pfx, h1r->path, len))
+                return h1r->path + len;
+        return NULL;
+}
+
+static void write_html(FILE *fp, const char *s, size_t len)
+{
+        for (; len--; ++s) {
+                switch (*s) {
+                case '&': FPUTS("&amp;", fp); break;
+                case '<': FPUTS("&lt;", fp); break;
+                case '>': FPUTS("&gt;", fp); break;
+                case '"': FPUTS("&quot;", fp); break;
+                case '\'': FPUTS("&#39;", fp); break;
+                case '\n': FPUTS("<br>", fp); break;
+                default: fputc(*s, fp);
+                }
+        }
+}
+
+/*
+ * quotes multi-line backtraces for CSV (and `\' and `"' in case
+ * we encounter nasty file names).
+ */
+static void write_q_csv(FILE *fp, const char *s, size_t len)
+{
+        fputc('"', fp);
+        for (; len--; ++s) {
+                switch (*s) {
+                case '\n': fputs("\\n", fp); break;
+                case '\\': fputs("\\\\", fp); break;
+                case '"': fputs("\\\"", fp); break;
+                default: fputc(*s, fp);
+                }
+        }
+        fputc('"', fp);
+}
+
+
+/* URI-safe base-64 (RFC 4648) */
+static void write_b64_url(FILE *fp, const uint8_t *in, size_t len)
+{
+        static const uint8_t b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+                        "abcdefghijklmnopqrstuvwxyz" "0123456789-_";
+        uint8_t o[4];
+        while (len > 3) {
+                o[0] = b64[in[0] >> 2];
+                o[1] = b64[((in[0] << 4) | (in[1] >> 4)) & 0x3f];
+                o[2] = b64[((in[1] << 2) | (in[2] >> 6)) & 0x3f];
+                o[3] = b64[in[2] & 0x3f];
+                fwrite(o, sizeof(o), 1, fp);
+                len -= 3;
+                in += 3;
+        }
+        if (len) {
+                size_t i = 2;
+
+                o[0] = b64[in[0] >> 2];
+                o[1] = b64[((in[0] << 4) | (--len ? (in[1] >> 4) : 0)) & 0x3f];
+                if (len)
+                        o[i++] = b64[((in[1] << 2) |
+                                        (--len ? in[2] >> 6 : 0)) & 0x3f];
+                if (len)
+                        o[i++] = b64[in[2] & 0x3f];
+                fwrite(o, i, 1, fp);
+        }
+}
+
+/* unescapes @s in-place and adjusts @len */
+static bool b64_url_decode(const void *ptr, size_t *len)
+{
+        union { const void *in; uint8_t *out; } deconst;
+        const uint8_t *in = ptr;
+        uint8_t u = 0;
+
+        deconst.in = ptr;
+        uint8_t *out = deconst.out;
+
+        for (size_t i = 0; i < *len; ++i) {
+                uint8_t c = in[i];
+
+                switch (c) {
+                case 'A' ... 'Z': c -= 'A'; break;
+                case 'a' ... 'z': c -= ('a' - 26); break;
+                case '0' ... '9': c -= ('0' - 52); break;
+                case '-': c = 62; break;
+                case '_': c = 63; break;
+                default: return false;
+                }
+
+                mwrap_assert(c <= 63);
+                switch (i % 4) {
+                case 0: u = c << 2; break;
+                case 1:
+                        *out++ = u | c >> 4;
+                        u = c << 4;
+                        break;
+                case 2:
+                        *out++ = u | c >> 2;
+                        u = c << 6;
+                        break;
+                case 3: *out++ = u | c;
+                }
+        }
+        *len = out - in;
+        return true;
+}
+
+/* keep this consistent with Mwrap.xs location_string */
+static off_t write_loc_name(FILE *fp, const struct src_loc *l)
+{
+        off_t beg = ftello(fp);
+
+        if (beg < 0) {
+                fprintf(stderr, "ftello: %m\n");
+                return beg;
+        }
+        if (l->f) {
+                fputs(l->f->fn, fp);
+                if (l->lineno == U24_MAX)
+                        FPUTS(":-", fp);
+                else
+                        fprintf(fp, ":%u", l->lineno);
+        }
+        if (l->bt_len) {
+                AUTO_FREE char **s = bt_syms(l->bt, l->bt_len);
+                if (!s) return -1;
+                if (l->f) fputc('\n', fp);
+
+                /* omit local " [RETURN_ADDRESS]" if doing deep backtraces */
+                for (uint32_t i = 0; i < l->bt_len; ++i) {
+                        char *c = memrchr(s[i], '[', strlen(s[i]));
+                        if (c && c > (s[i] + 2) && c[-1] == ' ')
+                                c[-1] = '\0';
+                }
+
+                fputs(s[0], fp);
+                for (uint32_t i = 1; i < l->bt_len; ++i) {
+                        fputc('\n', fp);
+                        fputs(s[i], fp);
+                }
+        }
+        off_t end = ftello(fp);
+        if (end < 0) {
+                fprintf(stderr, "ftello: %m\n");
+                return end;
+        }
+        return end - beg;
+}
+
+static struct h1_src_loc *accumulate(unsigned long min, size_t *hslc, FILE *lp)
+{
+        struct mw_fbuf fb;
+        if (!fbuf_init(&fb)) return NULL;
+        rcu_read_lock();
+        struct cds_lfht *t = CMM_LOAD_SHARED(totals);
+        struct cds_lfht_iter iter;
+        struct src_loc *l;
+        if (t) cds_lfht_for_each_entry(t, &iter, l, hnode) {
+                size_t freed = uatomic_read(&l->freed_bytes);
+                size_t total = uatomic_read(&l->total);
+                struct h1_src_loc hsl;
+
+                if (total < min) continue;
+                hsl.bytes = total - freed;
+                hsl.allocations = uatomic_read(&l->allocations);
+                hsl.frees = uatomic_read(&l->frees);
+                hsl.live = hsl.allocations - hsl.frees;
+                hsl.mean_life = hsl.frees ?
+                        ((double)uatomic_read(&l->age_total) /
+                                (double)hsl.frees) :
+                        HUGE_VAL;
+                hsl.max_life = uatomic_read(&l->max_lifespan);
+                hsl.sl = l;
+                hsl.lname_len = write_loc_name(lp, l);
+                fwrite(&hsl, sizeof(hsl), 1, fb.fp);
+        }
+        rcu_read_unlock();
+
+        struct h1_src_loc *hslv;
+        if (fbuf_close(&fb)) {
+                hslv = NULL;
+        } else {
+                *hslc = fb.len / sizeof(*hslv);
+                mwrap_assert((fb.len % sizeof(*hslv)) == 0);
+                hslv = (struct h1_src_loc *)fb.ptr;
+        }
+        return hslv;
+}
+
+static void show_stats(FILE *fp)
+{
+        size_t dec = uatomic_read(&total_bytes_dec);
+        size_t inc = uatomic_read(&total_bytes_inc);
+        fprintf(fp, "<p>Current age: %zu (live: %zu) "
+                "/ files: %zu / locations: %zu",
+                inc , inc - dec,
+                uatomic_read(&nr_file), uatomic_read(&nr_src_loc));
+}
+
+/* /$PID/at/$LOCATION endpoint */
+static enum mw_qev each_at(struct mw_h1 *h1, struct mw_h1req *h1r)
+{
+        const char *loc = h1r->path + sizeof("/at/") - 1;
+        size_t len = h1r->path_len - (sizeof("/at/") - 1);
+        size_t min = 0;
+
+        if (!b64_url_decode(loc, &len) || len >= PATH_MAX)
+                return h1_400(h1);
+
+        struct src_loc *l = mwrap_get_bin(loc, len);
+
+        if (!l) return h1_404(h1);
+
+        AUTO_CLOFREE struct mw_fbuf lb;
+        if (!fbuf_init(&lb)) return h1_close(h1);
+        if (write_loc_name(lb.fp, l) < 0) return h1_close(h1);
+        if (fbuf_close(&lb))
+                return h1_close(h1);
+
+        struct mw_fbuf html;
+        FILE *fp = wbuf_init(&html);
+        if (!fp) return h1_close(h1);
+        FPUTS("<html><head><title>", fp);
+        write_html(fp, lb.ptr, lb.len);
+        FPUTS("</title></head><body><p>live allocations at:", fp);
+        if (l->bt_len > 1 || (l->bt_len == 1 && l->f)) FPUTS("<br/>", fp);
+        else fputc(' ', fp);
+        write_html(fp, lb.ptr, lb.len);
+
+        show_stats(fp);
+        FPUTS("<table><tr><th>size</th><th>generation</th>"
+                "<th>address</th></tr>", fp);
+
+        rcu_read_lock();
+        struct alloc_hdr *h;
+        cds_list_for_each_entry_rcu(h, &l->allocs, anode) {
+                size_t size = uatomic_read(&h->size);
+                if (size > min)
+                        fprintf(fp, "<tr><td>%zu</td><td>%zu</td><td>%p</td>\n",
+                                size, h->as.live.gen, h->real);
+        }
+        rcu_read_unlock();
+        FPUTS("</table></body></html>", fp);
+        return h1_200(h1, &html, TYPE_HTML);
+}
+
+/* /$PID/each/$MIN endpoint */
+static enum mw_qev each_gt(struct mw_h1 *h1, struct mw_h1req *h1r,
+                                unsigned long min, bool csv)
+{
+        static const char default_sort[] = "bytes";
+        const char *sort = default_sort;
+        size_t sort_len = sizeof(default_sort) - 1;
+
+        if (h1r->qstr && h1r->qlen > 5 && !memcmp(h1r->qstr, "sort=", 5)) {
+                sort = h1r->qstr + 5;
+                sort_len = h1r->qlen - 5;
+        }
+
+        size_t hslc;
+        AUTO_CLOFREE struct mw_fbuf lb;
+        if (!fbuf_init(&lb)) return h1_close(h1);
+        AUTO_FREE struct h1_src_loc *hslv = accumulate(min, &hslc, lb.fp);
+        if (!hslv)
+                return h1_close(h1);
+
+        if (fbuf_close(&lb))
+                return h1_close(h1);
+
+        char *n = lb.ptr;
+        for (size_t i = 0; i < hslc; ++i) {
+                hslv[i].loc_name = n;
+                n += hslv[i].lname_len;
+                if (hslv[i].lname_len < 0)
+                        return h1_close(h1);
+        }
+
+        struct mw_fbuf bdy;
+        FILE *fp = wbuf_init(&bdy);
+        if (!fp) return h1_close(h1);
+
+        if (!csv) {
+                unsigned depth = (unsigned)CMM_LOAD_SHARED(bt_req_depth);
+                fprintf(fp, "<html><head><title>mwrap each &gt;%lu"
+                        "</title></head><body><p>mwrap each &gt;%lu "
+                        "(change `%lu' in URL to adjust filtering) - "
+                        "MWRAP=bt:%u", min, min, min, depth);
+                show_stats(fp);
+                /* need borders to distinguish multi-level traces */
+                if (depth)
+                        FPUTS("<table\nborder=1><tr>", fp);
+                else /* save screen space if only tracing one line */
+                        FPUTS("<table><tr>", fp);
+        }
+
+        int (*cmp)(const void *, const void *) = NULL;
+        if (csv) {
+                for (size_t i = 0; i < CAA_ARRAY_SIZE(fields); i++) {
+                        const char *fn = fields[i].fname;
+                        if (i)
+                                fputc(',', fp);
+                        fputs(fn, fp);
+                        if (fields[i].flen == sort_len &&
+                                        !memcmp(fn, sort, sort_len))
+                                cmp = fields[i].cmp;
+                }
+                fputc('\n', fp);
+        } else {
+                for (size_t i = 0; i < CAA_ARRAY_SIZE(fields); i++) {
+                        const char *fn = fields[i].fname;
+                        FPUTS("<th>", fp);
+                        if (fields[i].flen == sort_len &&
+                                        !memcmp(fn, sort, sort_len)) {
+                                cmp = fields[i].cmp;
+                                fprintf(fp, "<b>%s</b>", fields[i].fname);
+                        } else {
+                                fprintf(fp, "<a\nhref=\"./%lu?sort=%s\">%s</a>",
+                                        min, fn, fn);
+                        }
+                        FPUTS("</th>", fp);
+                }
+        }
+        if (!csv)
+                FPUTS("</tr>", fp);
+        if (cmp)
+                qsort(hslv, hslc, sizeof(*hslv), cmp);
+        else if (!csv)
+                FPUTS("<tr><td>sort= not understood</td></tr>", fp);
+        if (csv) {
+                for (size_t i = 0; i < hslc; i++) {
+                        struct h1_src_loc *hsl = &hslv[i];
+
+                        fprintf(fp, "%zu,%zu,%zu,%zu,%0.3f,%zu,",
+                                hsl->bytes, hsl->allocations, hsl->frees,
+                                hsl->live, hsl->mean_life, hsl->max_life);
+                        write_q_csv(fp, hsl->loc_name, hsl->lname_len);
+                        fputc('\n', fp);
+                }
+        } else {
+                for (size_t i = 0; i < hslc; i++) {
+                        struct h1_src_loc *hsl = &hslv[i];
+
+                        fprintf(fp, "<tr><td>%zu</td><td>%zu</td><td>%zu</td>"
+                                "<td>%zu</td><td>%0.3f</td><td>%zu</td>",
+                                hsl->bytes, hsl->allocations, hsl->frees,
+                                hsl->live, hsl->mean_life, hsl->max_life);
+                        FPUTS("<td><a\nhref=\"../at/", fp);
+
+                        write_b64_url(fp, src_loc_hash_tip(hsl->sl),
+                                        src_loc_hash_len(hsl->sl));
+
+                        FPUTS("\">", fp);
+                        write_html(fp, hsl->loc_name, hsl->lname_len);
+                        FPUTS("</a></td></tr>", fp);
+                }
+                FPUTS("</table></body></html>", fp);
+        }
+        return h1_200(h1, &bdy, csv ? TYPE_CSV : TYPE_HTML);
+}
+
+/* /$PID/ root endpoint */
+static enum mw_qev pid_root(struct mw_h1 *h1, struct mw_h1req *h1r)
+{
+        struct mw_fbuf html;
+        FILE *fp = wbuf_init(&html);
+        if (!fp) return h1_close(h1);
+#define default_min "2000"
+
+        int pid = (int)getpid();
+        fprintf(fp, "<html><head><title>mwrap PID:%d</title></head><body>"
+                "<pre>mwrap PID:%d", pid, pid);
+        show_stats(fp);
+        FPUTS("\n\n<a\nhref=\"each/" default_min "\">allocations &gt;"
+                default_min " bytes</a>""</pre><pre\nid=help>"
+"To get source file and line info for native backtraces, consult your\n"
+"distro for -dbg, -dbgsym, or -debug packages.\n"
+"And/or rebuild your code with debug flags (e.g. `-ggdb3' if using gcc)\n"
+"and don't strip the resulting binaries.\n"
+"You should see locations from the backtrace_symbols(3) function\n"
+"in the form of FILENAME(+OFFSET) or FILENAME(SYMBOL+OFFSET)\n"
+"(e.g. /usr/lib/foo.so(+0xdead) or /usr/lib/foo.so(func+(0xbeef))\n"
+"\n"
+"Any version of addr2line should decode FILENAME(+OFFSET) locations:\n"
+"\n"
+"        addr2line -e FILENAME OFFSET\n"
+"\n"
+"SYMBOL+OFFSET requires addr2line from GNU binutils 2.39+ (Aug 2022):\n"
+"\n"
+"        addr2line -e FILENAME SYMBOL+OFFSET\n", fp);
+
+        FPUTS("\n<a\nhref=\"" URL "\">" URL "</a></pre></body></html>", fp);
+        return h1_200(h1, &html, TYPE_HTML);
+#undef default_min
+}
+
+/* @e is not NUL-terminated */
+static bool sfx_eq(const char *e, const char *sfx)
+{
+        for (const char *m = sfx; *m; m++, e++)
+                if (*e != *m)
+                        return false;
+        return true;
+}
+
+static enum mw_qev h1_dispatch(struct mw_h1 *h1, struct mw_h1req *h1r)
+{
+        if (h1r->method_len == 3 && !memcmp(h1r->method, "GET", 3)) {
+                const char *c;
+
+                if ((c = PATH_SKIP(h1r, "/each/"))) {
+                        errno = 0;
+                        char *e;
+                        unsigned long min = strtoul(c, &e, 10);
+                        if (!errno) {
+                                if (*e == ' ' || *e == '?')
+                                        return each_gt(h1, h1r, min, false);
+                                if (sfx_eq(e, ".csv") &&
+                                                (e[4] == ' ' || e[4] == '?'))
+                                        return each_gt(h1, h1r, min, true);
+                        }
+                } else if ((PATH_SKIP(h1r, "/at/"))) {
+                        return each_at(h1, h1r);
+                } else if (h1r->path_len == 1 && h1r->path[0] == '/') {
+                        return pid_root(h1, h1r);
+                }
+        } else if (h1r->method_len == 4 && !memcmp(h1r->method, "POST", 4)) {
+                if (h1r->path_len == 6 && !memcmp(h1r->path, "/reset", 6))
+                        return h1_do_reset(h1);
+                if (h1r->path_len == 5 && !memcmp(h1r->path, "/trim", 5))
+                        return h1_do_trim(h1);
+                if (h1r->path_len == 4 && !memcmp(h1r->path, "/ctl", 4))
+                        return h1_do_ctl_finish(h1);
+        }
+        return h1_404(h1);
+}
+
+static void
+prep_trickle(struct mw_h1 *h1, struct mw_h1req *h1r, struct mw_h1d *h1d)
+{
+        if (h1->h1r) return; /* already trickling */
+        h1->h1r = h1r;
+        mwrap_assert(h1d->shared_h1r == h1r);
+        h1d->shared_h1r = NULL;
+}
+
+/*
+ * nothing in the PSGI app actually reads input, but clients tend
+ * to send something in the body of POST requests anyways, so we
+ * just drain it
+ */
+static enum mw_qev h1_drain_input(struct mw_h1 *h1, struct mw_h1req *h1r,
+        struct mw_h1d *h1d)
+{
+        if (h1r) { /* initial */
+                ssize_t overread = h1r->rbuf_len - h1r->pret;
+                mwrap_assert(overread >= 0);
+                if ((size_t)overread <= h1->in_len)
+                        h1->in_len -= overread;
+                else /* pipelining not supported */
+                        return h1_400(h1);
+        } else { /* continue dealing with a trickle */
+                h1r = h1->h1r;
+                mwrap_assert(h1r);
+        }
+        while (h1->in_len > 0) {
+                char ibuf[BUFSIZ];
+                size_t len = h1->in_len;
+                ssize_t r;
+
+                mwrap_assert(h1->has_input);
+                if (len > sizeof(ibuf))
+                        len = sizeof(ibuf);
+
+                r = read(h1->fd, ibuf, len);
+                if (r > 0) { /* just discard the input */
+                        h1->in_len -= r;
+                } else if (r == 0) {
+                        return h1_close(h1);
+                } else {
+                        switch (errno) {
+                        case EAGAIN:
+                                prep_trickle(h1, h1r, h1d);
+                                return MW_QEV_RD;
+                        case ECONNRESET: /* common */
+                        case ENOTCONN:
+                                return h1_close(h1);
+                        default: /* ENOMEM, ENOBUFS, ... */
+                                assert(errno != EBADF);
+                                fprintf(stderr, "read: %m\n");
+                                return h1_close(h1);
+                        }
+                }
+        }
+        h1->has_input = 0; /* all done with input */
+        return h1_dispatch(h1, h1r);
+}
+
+static bool valid_end(const char *end)
+{
+        switch (*end) {
+        case '\r': case ' ': case '\t': case '\n': return true;
+        default: return false;
+        }
+}
+
+/* no error reporting, too much code */
+static void ctl_set(struct mw_h1 *h1, long n)
+{
+        if (n >= 0) {
+                if (n > MWRAP_BT_MAX)
+                        n = MWRAP_BT_MAX;
+                CMM_STORE_SHARED(bt_req_depth, (uint32_t)n);
+        }
+}
+
+static enum mw_qev h1_parse_harder(struct mw_h1 *h1, struct mw_h1req *h1r,
+        struct mw_h1d *h1d)
+{
+        enum { HDR_IGN, HDR_XENC, HDR_CLEN } cur = HDR_IGN;
+        char *end;
+        struct phr_header *hdr = h1r->hdr;
+        long depth = -1;
+
+        h1->prev_len = 0;
+        h1->has_input = 0;
+        h1->in_len = 0;
+
+        for (hdr = h1r->hdr; h1r->nr_hdr--; hdr++) {
+                if (NAME_EQ(hdr, "Transfer-Encoding"))
+                        cur = HDR_XENC;
+                else if (NAME_EQ(hdr, "Content-Length"))
+                        cur = HDR_CLEN;
+                else if (NAME_EQ(hdr, "Trailer"))
+                        return h1_400(h1);
+                else if (hdr->name) {
+                        cur = HDR_IGN;
+                        /*
+                         * don't want to increase code to deal with POST
+                         * request bodies, so let pico handle parameters in
+                         * HTTP request headers, instead.
+                         */
+                        if (NAME_EQ(hdr, "X-Mwrap-BT-Depth")) {
+                                errno = 0;
+                                depth = strtol(hdr->value, &end, 10);
+                                if (errno || !valid_end(end))
+                                        depth = -1;
+                        }
+                }
+
+                /* else: continuation line */
+                if (!hdr->value_len)
+                        continue;
+                switch (cur) {
+                case HDR_XENC:
+                        return h1_400(h1);
+                case HDR_CLEN:
+                        if (h1->has_input) return h1_400(h1);
+                        h1->has_input = 1;
+                        errno = 0;
+                        h1->in_len = strtoul(hdr->value, &end, 10);
+                        if (errno || !valid_end(end))
+                                return h1_400(h1);
+                        break;
+                case HDR_IGN:
+                        break;
+                }
+        }
+        if (h1r->path_len < (g_h1d.pid_len + 2))
+                return h1_404(h1);
+
+        /* skip "/$PID" prefix */
+        if (*h1r->path == '/' &&
+                        !memcmp(h1r->path+1, g_h1d.pid_str, g_h1d.pid_len) &&
+                        h1r->path[1 + g_h1d.pid_len] == '/') {
+                h1r->path += 1 + g_h1d.pid_len;
+                h1r->path_len -= 1 + g_h1d.pid_len;
+        } else {
+                return h1_404(h1);
+        }
+
+        /*
+         * special case for /ctl, since I don't feel like parsing queries
+         * in the request body (ensure no query string, too)
+         */
+        if (h1r->method_len == 4 && !memcmp(h1r->method, "POST", 4)) {
+                if (h1r->path_len == 4 && !memcmp(h1r->path, "/ctl", 4))
+                        ctl_set(h1, depth);
+        }
+
+        /* break off QUERY_STRING */
+        h1r->qstr = memchr(h1r->path, '?', h1r->path_len);
+        if (h1r->qstr) {
+                ++h1r->qstr; /* ignore '?' */
+                h1r->qlen = h1r->path + h1r->path_len - h1r->qstr;
+                h1r->path_len -= (h1r->qlen + 1);
+        }
+        return h1_drain_input(h1, h1r, h1d);
+}
+
+static enum mw_qev h1_event_step(struct mw_h1 *h1, struct mw_h1d *h1d)
+{
+        struct mw_h1req *h1r;
+
+        /*
+         * simple rule to avoid trivial DoS in HTTP/1.x: never process a
+         * new request until you've written out your previous response
+         * (and this is why I'm too stupid to do HTTP/2)
+         */
+        if (h1->wbuf)
+                return h1_send_flush(h1);
+
+        if (h1->has_input)
+                return h1_drain_input(h1, NULL, h1d);
+        /*
+         * The majority of requests can be served using per-daemon rbuf,
+         * no need for per-client allocations unless a client trickles
+         */
+        h1r = h1->h1r ? h1->h1r : h1d->shared_h1r;
+        if (!h1r) {
+                h1r = h1d->shared_h1r = malloc(sizeof(*h1r));
+                if (!h1r) {
+                        fprintf(stderr, "h1r malloc: %m\n");
+                        return h1_close(h1);
+                }
+        }
+        for (;;) {
+                size_t n = MW_RBUF_SIZE - h1->prev_len;
+                ssize_t r = read(h1->fd, &h1r->rbuf[h1->prev_len], n);
+
+                if (r > 0) {
+                        h1r->rbuf_len = h1->prev_len + r;
+                        h1r->nr_hdr = MW_NR_NAME;
+                        h1r->pret = phr_parse_request(h1r->rbuf, h1r->rbuf_len,
+                                                &h1r->method, &h1r->method_len,
+                                                &h1r->path, &h1r->path_len,
+                                                &h1r->minor_ver, h1r->hdr,
+                                                &h1r->nr_hdr, h1->prev_len);
+                        if (h1r->pret > 0)
+                                return h1_parse_harder(h1, h1r, h1d);
+                        if (h1r->pret == -1)
+                                return h1_400(h1); /* parser error */
+
+                        mwrap_assert(h1r->pret == -2); /* incomplete */
+                        mwrap_assert(h1r->rbuf_len <= MW_RBUF_SIZE &&
+                                "bad math");
+
+                        /* this should be 413 or 414, don't need the bloat */
+                        if (h1r->rbuf_len == MW_RBUF_SIZE)
+                                return h1_400(h1);
+                        mwrap_assert(h1r->rbuf_len < MW_RBUF_SIZE);
+                        h1->prev_len = h1r->rbuf_len;
+                        /* loop again */
+                } else if (r == 0) {
+                        return h1_close(h1);
+                } else { /* r < 0 */
+                        switch (errno) {
+                        case EAGAIN: /* likely, detach to per-client buffer */
+                                if (h1->prev_len)
+                                        prep_trickle(h1, h1r, h1d);
+                                return MW_QEV_RD;
+                        case ECONNRESET: /* common */
+                        case ENOTCONN:
+                                return h1_close(h1);
+                        default: /* ENOMEM, ENOBUFS, ... */
+                                assert(errno != EBADF);
+                                fprintf(stderr, "read: %m\n");
+                                return h1_close(h1);
+                        }
+                }
+        }
+
+        return MW_QEV_RD;
+}
+
+static int poll_add(struct mw_h1d *h1d, int fd, short events)
+{
+        struct pollfd pfd;
+
+        if (!h1d->pb.fp && !fbuf_init(&h1d->pb))
+                return -1;
+        pfd.fd = fd;
+        pfd.events = events;
+        fwrite(&pfd, 1, sizeof(pfd), h1d->pb.fp);
+        return 0; /* success */
+}
+
+static struct pollfd *poll_detach(struct mw_h1d *h1d, nfds_t *nfds)
+{
+        struct pollfd *pfd = NULL; /* our return value */
+
+        /* not sure how to best recover from ENOMEM errors in stdio */
+        if (h1d->pb.fp) {
+                if (fbuf_close(&h1d->pb)) {
+                        exit(EXIT_FAILURE);
+                } else {
+                        mwrap_assert(h1d->pb.len % sizeof(*pfd) == 0);
+                        pfd = (struct pollfd *)h1d->pb.ptr;
+                        *nfds = h1d->pb.len / sizeof(*pfd);
+                }
+        }
+
+        /* prepare a new poll buffer the next loop */
+        memset(&h1d->pb, 0, sizeof(h1d->pb));
+
+        return pfd;
+}
+
+static void non_fatal_pause(const char *fail_fn)
+{
+        fprintf(stderr, "%s: %m (non-fatal, pausing mwrap-httpd)\n", fail_fn);
+        poll(NULL, 0, 1000);
+}
+
+static void h1d_event_step(struct mw_h1d *h1d)
+{
+        union mw_sockaddr sa;
+        const char *fail_fn = NULL;
+
+        while (!fail_fn) {
+                socklen_t len = (socklen_t)sizeof(sa);
+                int fd = accept4(h1d->lfd, &sa.any, &len,
+                                SOCK_NONBLOCK|SOCK_CLOEXEC);
+
+                if (fd >= 0) {
+                        struct mw_h1 *h1 = calloc(1, sizeof(*h1));
+
+                        if (h1) {
+                                h1->fd = fd;
+                                h1->events = POLLIN;
+                                cds_list_add_tail(&h1->nd, &h1d->conn);
+                        } else {
+                                int err = errno;
+                                fail_fn = "malloc";
+                                close(fd);
+                                errno = err;
+                        }
+                } else {
+                        switch (errno) {
+                        case EAGAIN: /* likely */
+                                return;
+                        case ECONNABORTED: /* common w/ TCP */
+                                continue;
+                        case EMFILE:
+                        case ENFILE:
+                        case ENOBUFS:
+                        case ENOMEM:
+                        case EPERM:
+                                fail_fn = "accept4";
+                                break;
+                        /*
+                         * EINVAL, EBADF, ENOTSOCK, EOPNOTSUPP are all fatal
+                         * bugs.  The last 3 would be wayward closes in the
+                         * application being traced
+                         */
+                        default:
+                                fprintf(stderr,
+                                        "accept4: %m (fatal in mwrap-httpd)\n");
+                                abort();
+                        }
+                }
+        }
+        /* hope other cleanup work gets done by other threads: */
+        non_fatal_pause(fail_fn);
+}
+
+static void h1d_unlink(struct mw_h1d *h1d, bool do_close)
+{
+        union mw_sockaddr sa;
+        socklen_t len = (socklen_t)sizeof(sa);
+
+        if (h1d->lfd < 0 || !h1d->pid_len)
+                return;
+        if (getsockname(h1d->lfd, &sa.any, &len) < 0) {
+                fprintf(stderr, "getsockname: %m\n");
+                return;
+        }
+        if (do_close) { /* only safe to close if thread isn't running */
+                (void)close(h1d->lfd);
+                h1d->lfd = -1;
+        }
+
+        char p[sizeof(h1d->pid_str)];
+        int rc = snprintf(p, sizeof(p), "%d", (int)getpid());
+
+        if (rc == (int)h1d->pid_len && !memcmp(p, h1d->pid_str, rc))
+                if (unlink(sa.un.sun_path) && errno != ENOENT)
+                        fprintf(stderr, "unlink(%s): %m\n", sa.un.sun_path);
+        h1d->pid_len = 0;
+}
+
+/* @env is getenv("MWRAP") */
+static int h1d_init(struct mw_h1d *h1d, const char *menv)
+{
+        union mw_sockaddr sa = { .un = { .sun_family = AF_UNIX } };
+#if defined(HAS_SOCKADDR_SA_LEN) || defined(HAVE_STRUCT_SOCKADDR_UN_SUN_LEN)
+        sa.un.sun_len = (unsigned char)sizeof(struct sockaddr_un);
+#endif
+        const char *env = strstr(menv, "socket_dir:");
+        if (!env) return 1;
+        if (env != menv && env[-1] != ',')
+                return 1;
+        env += sizeof("socket_dir");
+        if (!*env) return 1;
+        const char *end = strchr(env, ',');
+        size_t len = end ? (size_t)(end - env) : strlen(env);
+        if (len == 0)
+                return fprintf(stderr, "socket_dir: cannot be empty\n");
+        if (len >= sizeof(sa.un.sun_path))
+                return fprintf(stderr, "socket_dir:%s too long(%zu)\n",
+                                env, len);
+
+        char *p = mempcpy(sa.un.sun_path, env, len);
+        if (p[-1] != '/')
+                *p++ = '/';
+        struct stat sb;
+        if (stat(sa.un.sun_path, &sb) < 0) {
+                if (errno != ENOENT)
+                        return fprintf(stderr, "stat(%s): %m\n",
+                                        sa.un.sun_path);
+                if (mkdir(sa.un.sun_path, 0700) < 0)
+                        return fprintf(stderr, "mkdir(%s): %m\n",
+                                        sa.un.sun_path);
+        } else if (!S_ISDIR(sb.st_mode)) {
+                return fprintf(stderr, "socket_dir:%s is not a directory\n",
+                                sa.un.sun_path);
+        }
+        len = sizeof(sa.un.sun_path) - (p - sa.un.sun_path);
+        int rc = snprintf(p, len, "%d.sock", (int)getpid());
+        if (rc >= (int)len)
+                return fprintf(stderr,
+                        "socket_dir too long rc=%d > len=%zu\n", rc, len);
+        if (rc < 0)
+                return fprintf(stderr, "we suck at snprintf: %m\n");
+        h1d->pid_len = rc - sizeof(".sock") + 1;
+        memcpy(h1d->pid_str, p, h1d->pid_len);
+        if (unlink(sa.un.sun_path) && errno != ENOENT)
+                return fprintf(stderr, "unlink(%s): %m\n", sa.un.sun_path);
+        h1d->lfd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
+        if (h1d->lfd < 0)
+                return fprintf(stderr, "socket: %m\n");
+        if (bind(h1d->lfd, &sa.any, (socklen_t)sizeof(sa)) < 0) {
+                fprintf(stderr, "bind: %m\n");
+                goto close_fail;
+        }
+        if (listen(h1d->lfd, 1024) < 0) {
+                fprintf(stderr, "listen: %m\n");
+                goto close_fail;
+        }
+        h1d->alive = 1; /* runs in parent, before pthread_create */
+        h1d->running = 1;
+        CDS_INIT_LIST_HEAD(&h1d->conn);
+        return 0;
+close_fail:
+        h1d_unlink(h1d, true);
+        return 1;
+}
+
+/*
+ * epoll|kqueue would make this O(n) function unnecessary, but our (n) is
+ * expected to be tiny (<10): no need to waste kernel memory on epoll|kqueue
+ */
+static struct mw_h1 *h1_lookup(const struct mw_h1d *h1d, int fd)
+{
+        struct mw_h1 *h1 = NULL;
+
+        cds_list_for_each_entry(h1, &h1d->conn, nd)
+                if (h1->fd == fd)
+                        break;
+        mwrap_assert(h1 && h1->fd == fd && "bad FD");
+        return h1;
+}
+
+static void *h1d_run(void *x) /* pthread_create cb */
+{
+        struct mw_h1d *h1d = x;
+        nfds_t i, nfds;
+        int rc;
+        struct mw_h1 *h1, *nxt;
+        enum mw_qev ev;
+        locating = 1; /* don't report our own memory use */
+
+        for (; uatomic_read(&h1d->alive); ) {
+                while (poll_add(h1d, h1d->lfd, POLLIN))
+                        non_fatal_pause("poll_add(lfd)");
+                cds_list_for_each_entry_safe(h1, nxt, &h1d->conn, nd)
+                        if (poll_add(h1d, h1->fd, h1->events))
+                                h1_close(h1);
+                AUTO_FREE struct pollfd *pfd = poll_detach(h1d, &nfds);
+                rc = pfd ? poll(pfd, nfds, -1) : -1;
+
+                if (rc < 0) {
+                        switch (errno) {
+                        case EINTR: break; /* shouldn't happen, actually */
+                        case ENOMEM: /* may be common */
+                        case EINVAL: /* RLIMIT_NOFILE hit */
+                                non_fatal_pause("poll");
+                                break; /* to forloop where rc<0 */
+                        default: /* EFAULT is a fatal bug */
+                                fprintf(stderr,
+                                        "poll: %m (fatal in mwrap-httpd)\n");
+                                abort();
+                        }
+                } else {
+                        for (i = 0; i < nfds &&
+                                        uatomic_read(&h1d->alive); i++) {
+                                if (!pfd[i].revents)
+                                        continue;
+                                if (pfd[i].fd == h1d->lfd) {
+                                        h1d_event_step(h1d);
+                                } else {
+                                        h1 = h1_lookup(h1d, pfd[i].fd);
+                                        ev = h1_event_step(h1, h1d);
+                                        if (ev == MW_QEV_IGNORE)
+                                                continue;
+                                        h1->events = ev;
+                                }
+                        }
+                }
+        }
+        uatomic_set(&h1d->running, 0);
+        free(poll_detach(h1d, &nfds));
+        cds_list_for_each_entry_safe(h1, nxt, &h1d->conn, nd)
+                h1_close(h1);
+        return NULL;
+}
+
+static void h1d_atexit(void)
+{
+        h1d_unlink(&g_h1d, false);
+}
+
+static void h1d_stop_join(struct mw_h1d *h1d)
+{
+        union mw_sockaddr sa;
+        socklen_t len = (socklen_t)sizeof(sa);
+        int e, sfd;
+        void *ret;
+#define ERR ": (stopping mwrap-httpd before fork): "
+
+        mwrap_assert(uatomic_read(&h1d->alive) == 0);
+        while (getsockname(h1d->lfd, &sa.any, &len) < 0) {
+                non_fatal_pause("getsockname"ERR);
+                if (!uatomic_read(&h1d->running))
+                        goto join_thread;
+        }
+retry_socket:
+        while ((sfd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0)) < 0) {
+                non_fatal_pause("socket"ERR);
+                if (!uatomic_read(&h1d->running))
+                        goto join_thread;
+        }
+        if (connect(sfd, &sa.any, len) < 0) {
+                int e = errno;
+                close(sfd);
+                errno = e;
+                non_fatal_pause("connect"ERR);
+                if (!uatomic_read(&h1d->running))
+                        goto join_thread;
+                goto retry_socket;
+        }
+#undef ERR
+        (void)close(sfd);
+join_thread:
+        e = pthread_join(h1d->tid, &ret);
+        if (e) { /* EDEADLK, EINVAL, ESRCH are all fatal bugs */
+                fprintf(stderr, "BUG? pthread_join: %s\n", strerror(e));
+                abort();
+        }
+        h1d_unlink(h1d, true);
+}
+
+static void h1d_atfork_prepare(void)
+{
+        if (uatomic_cmpxchg(&g_h1d.alive, 1, 0))
+                h1d_stop_join(&g_h1d);
+}
+
+static void h1d_start(void) /* may be called as pthread_atfork child cb */
+{
+        if (mwrap_env && !h1d_init(&g_h1d, mwrap_env) && g_h1d.alive) {
+                int rc = pthread_create(&g_h1d.tid, NULL, h1d_run, &g_h1d);
+                if (rc) { /* non-fatal */
+                        fprintf(stderr, "pthread_create: %s\n", strerror(rc));
+                        g_h1d.alive = 0;
+                        g_h1d.running = 0;
+                        h1d_unlink(&g_h1d, true);
+                }
+        }
+}
+
+/* must be called with global_mtx held */
+static void h1d_atfork_parent(void)
+{
+        if (g_h1d.lfd < 0)
+                h1d_start();
+}