Memory Safety
UVHTTP's headline differentiator is not peak single-host throughput (nginx and h2o win there) — it is verified memory safety in a lightweight, embeddable, 32-bit-capable C library. For long-running production services and embedded devices that cannot tolerate a slow memory leak or a use-after-free after weeks of uptime, this is the property that matters, and it is the one most comparable lightweight C HTTP libraries do not provide.
This document is the engineering evidence.
The guarantee
The full 101-test suite is verified clean under both:
| Sanitizer | What it catches | Result |
|---|---|---|
| AddressSanitizer (with leak detection) | heap use-after-free, heap-buffer-overflow, stack-buffer-overflow, memory leaks | 101/101 pass, zero findings |
| UndefinedBehaviorSanitizer | signed integer overflow, invalid shifts, null dereference, misalignment, out-of-bounds | 101/101 pass, zero findings |
ASan and UBSan cannot be combined in a single build, so they run as two separate configurations. Both must be green.
Reproduce it yourself (one command)
make verify-memory-safetyThis configures and builds the suite under ASan and UBSan, runs the full 101-test suite under each, and exits non-zero if any test fails or any sanitizer reports a finding. A clean run prints:
==> PASS: full suite clean under ASan and UBSan (101/101 each).The equivalent manual commands:
# AddressSanitizer (leaks, use-after-free, overflows)
cmake -B build_asan -DCMAKE_BUILD_TYPE=Debug -DENABLE_ASAN=ON
cmake --build build_asan -j$(nproc)
(cd build_asan && ctest --output-on-failure)
# UndefinedBehaviorSanitizer
cmake -B build_ubsan -DCMAKE_BUILD_TYPE=Debug -DENABLE_UBSAN=ON
cmake --build build_ubsan -j$(nproc)
(cd build_ubsan && ctest --output-on-failure)Sanitizer builds keep debug symbols (no -s strip) so any finding produces a resolvable, source-level stack trace.
Continuous verification
Memory safety is a non-regressing invariant, not a one-time check:
Coverage: 86% line / 99% function coverage on project code (excl. deps/).
- PR CI gate (
.github/workflows/ci-pr.yml): theasan-gatejob runs the suite under ASan on every pull request — memory safety cannot regress intomain. - Nightly CI (
.github/workflows/ci-nightly.yml): thetest-memory(ASan with leak detection) andtest-ubsanjobs run the full suite under each sanitizer every night. - Nightly fuzzing (
.github/workflows/ci-fuzz.yml): a libFuzzer harness (test/fuzz/fuzz_router.c) drives the router with millions of arbitrary inputs under ASan — finding bugs the fixed test inputs do not reach (it already found one; see below). - Build-system guard: sanitizer/Debug builds are never stripped, so CI findings are always debuggable.
Fuzzing
Sanitizers cover the code paths the test suite exercises. Fuzzing covers the paths it doesn't. UVHTTP ships a libFuzzer harness for the router (test/fuzz/fuzz_router.c), run nightly in CI.
# Build (clang + libFuzzer + ASan)
cmake -B build_fuzz -DCMAKE_BUILD_TYPE=Debug -DENABLE_ASAN=ON \
-DBUILD_TESTING=OFF -DBUILD_EXAMPLES=OFF
cmake --build build_fuzz -j$(nproc) --target uvhttp
clang -g -O1 -fsanitize=fuzzer,address -fno-omit-frame-pointer \
-Iinclude -Ideps/llhttp/include -Ideps/uthash/src \
-Ideps/mbedtls/include -Ideps/cjson -Ideps/libuv/include \
test/fuzz/fuzz_router.c build_fuzz/dist/lib/libuvhttp.a \
-Wl,--start-group \
deps/llhttp/build/libllhttp.a deps/cjson/build/libcjson.a \
deps/mbedtls/build/library/libmbedtls.a deps/mbedtls/build/library/libmbedx509.a \
deps/mbedtls/build/library/libmbedcrypto.a deps/libuv/build/libuv.a \
-Wl,--end-group -lpthread -lm -ldl -o fuzz_router
# Run
./fuzz_router -max_total_time=60 -max_len=256A clean run exits 0 with no ERROR: line. The harness already found the add_route_method heap-buffer-overflow listed in the table below (a post-migration non-parameter route registration that the unit tests never performed in that order) — which is exactly the point of fuzzing.
Bug classes fixed to reach this guarantee
The suite did not start clean. Reaching zero sanitizer findings required fixing genuine memory-safety defects in production library code — bugs that passed the normal test suite but corrupted memory under sanitizers. Representative fixes (full list in the Changelog):
| Bug class | Location | Detail |
|---|---|---|
| heap-buffer-overflow | uvhttp_router.c add_route_method | Found by libFuzzer. After a parameter route triggered trie migration (resetting array_capacity to 0), a later non-parameter route fell through to add_array_route, whose 0*2 capacity computed a zero-size realloc that strncpy overflowed. The normal test suite never hit this route-registration order. |
| heap-use-after-free | uvhttp_router.c find_or_create_child | cached pointer into node pool dangled after realloc grew the pool |
| heap-use-after-free | uvhttp_server.c uvhttp_server_free | freed flag read from already-freed memory on double-free |
| heap-use-after-free | uvhttp_connection.c | post-close read; close_pending accounting broken on re-entry |
| heap-buffer-overflow | uvhttp_response.c build_response_headers | snprintf return accumulation let pos exceed the buffer, underflowing the bound |
| stack-buffer-underflow | uvhttp_websocket.c uvhttp_ws_close | write to out-of-scope stack connection |
| memory leak | uvhttp_connection.c restart_read | request body pointer nulled without freeing |
| memory leak | uvhttp_server.c on_connection 503 path | temp_client (uv_tcp_t) never closed/freed |
| memory leak | uvhttp_server.c ws_disable_connection_management | manager struct (with embedded uv_timer_ts) freed before close callbacks fired |
| memory leak | uvhttp_error_helpers.c | uv_strerror on non-libuv errno triggered an unfreeable libuv uv__strdup |
| undefined behavior | uvhttp_connection.c switch_to_websocket | re-entry clobbered CLOSING state, breaking close accounting (caught as a UBSan-time hang) |
Each was root-caused (not masked) and fixed at the source.
Why this matters (and why most lightweight C libraries lack it)
A C HTTP library that "passes its tests" can still leak a few kilobytes per connection, dereference freed memory on a rare close race, or build an unterminated string into a response. None of these fail a functional test. They surface as:
- Slow RSS growth over days/weeks of uptime (embedded devices cannot restart).
- Intermittent crashes under load patterns the test suite never hit.
- Security-reachable corruption (buffer overflows in header/body parsing).
Sanitizers catch these before deployment. Most lightweight C HTTP libraries do not advertise sanitizer-clean status — not because they are clean, but because they have not run the check. UVHTTP runs it nightly and treats any finding as a release blocker.
Defense in depth
Beyond sanitizers, the codebase applies:
-Werrorwith-Wall -Wextra -Wformat=2 -Wformat-security: zero warnings compile gate.-fstack-protector-strong,-fno-common, full RELRO (-Wl,-z,relro,now).- HTTP response-splitting guards (
contains_control_chars) on all header values before emission. - Input validation: URL length/path-traversal, header size/format, body size limits.
- Idempotent resource cleanup:
*_cleanup/*_free_resourcespaths null pointers after freeing and guard on afreedflag to be robust against double-free at the API boundary.
When a sanitizer finding is reported
- Reproduce with
make verify-memory-safety(or the specific sanitizer build). - Root-cause from the source-level stack trace (sanitizer builds are unstripped).
- Fix at the source — do not suppress.
- Confirm both ASan and UBSan are green before merging.
Report security-relevant findings via the process in SECURITY.md.