Skip to content

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:

SanitizerWhat it catchesResult
AddressSanitizer (with leak detection)heap use-after-free, heap-buffer-overflow, stack-buffer-overflow, memory leaks101/101 pass, zero findings
UndefinedBehaviorSanitizersigned integer overflow, invalid shifts, null dereference, misalignment, out-of-bounds101/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) ​

bash
make verify-memory-safety

This 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:

bash
# 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): the asan-gate job runs the suite under ASan on every pull request — memory safety cannot regress into main.
  • Nightly CI (.github/workflows/ci-nightly.yml): the test-memory (ASan with leak detection) and test-ubsan jobs 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.

bash
# 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=256

A 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 classLocationDetail
heap-buffer-overflowuvhttp_router.c add_route_methodFound 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-freeuvhttp_router.c find_or_create_childcached pointer into node pool dangled after realloc grew the pool
heap-use-after-freeuvhttp_server.c uvhttp_server_freefreed flag read from already-freed memory on double-free
heap-use-after-freeuvhttp_connection.cpost-close read; close_pending accounting broken on re-entry
heap-buffer-overflowuvhttp_response.c build_response_headerssnprintf return accumulation let pos exceed the buffer, underflowing the bound
stack-buffer-underflowuvhttp_websocket.c uvhttp_ws_closewrite to out-of-scope stack connection
memory leakuvhttp_connection.c restart_readrequest body pointer nulled without freeing
memory leakuvhttp_server.c on_connection 503 pathtemp_client (uv_tcp_t) never closed/freed
memory leakuvhttp_server.c ws_disable_connection_managementmanager struct (with embedded uv_timer_ts) freed before close callbacks fired
memory leakuvhttp_error_helpers.cuv_strerror on non-libuv errno triggered an unfreeable libuv uv__strdup
undefined behavioruvhttp_connection.c switch_to_websocketre-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:

  • -Werror with -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_resources paths null pointers after freeing and guard on a freed flag to be robust against double-free at the API boundary.

When a sanitizer finding is reported ​

  1. Reproduce with make verify-memory-safety (or the specific sanitizer build).
  2. Root-cause from the source-level stack trace (sanitizer builds are unstripped).
  3. Fix at the source — do not suppress.
  4. Confirm both ASan and UBSan are green before merging.

Report security-relevant findings via the process in SECURITY.md.

Released under MIT License