YUME Internet freedom by FixCraft
On this page

Developer diagnostics

Operators check a configuration with --validate and yume-doctor, which operations describes, and the native guide covers typed runtime diagnostics and evidence. This page is for developers: the timing helpers, the benchmark probes, the warning set, sanitizers and fuzzing.

Timing helpers

src/common/timing.hpp holds the shared timing types. Stopwatch times one synchronous operation, SampleAccumulator batches counts and nanoseconds, and IntervalTimer tracks an asynchronous span. YUME_TIMING_SINK hands an event to a sink, and in builds without diagnostics it removes the event together with the expression that builds its details.

One CMake option, YUME_DEV_DIAGNOSTICS, decides whether the timing helpers and the benchmark probes exist, whatever the build type:

YUME_DEV_DIAGNOSTICS Timing helpers and probes
OFF (the default) compiled out, no clock reads
ON compiled in, off until a caller or YUME_BENCH_PROBE_FILE turns them on

The option defines YUME_ENABLE_DEV_DIAGNOSTICS for the whole build, never for one target. Both headers declare inline functions and types whose shape depends on it, so two parts of one program built with different values would disagree about those definitions. CI’s sanitizer jobs and the developer lanes build with it on, its Release jobs, fuzzing and the reachability check with it off, so both states compile.

Today only the HTTP/2 carrier uses the helpers. With set_timing_enabled(true) it records feed, flush, WebSocket encode and decode time next to its credit and window counters, and only its tests turn that on. yume and yumed have no timing switch.

./ezbuild.sh builds Release without diagnostics. ./ezbuild.sh --dev builds an optimized RelWithDebInfo with YUME_DEV_DIAGNOSTICS=ON. ./ezbuild.sh --native tunes for the current CPU, so that binary must not be copied to an older or different CPU. Fast-math stays off in every mode.

New timing work should use these types instead of pairs of steady_clock::now() calls or per-file switches. Keep detail strings inside YUME_TIMING_SINK. Guard a diagnostic timer that an asynchronous callback captures with #if YUME_ENABLE_DEV_DIAGNOSTICS, so an ordinary build’s closure carries no diagnostic member. Never log keys, nonces, plaintext, authentication material, secret paths or full peer-controlled payloads.

Benchmark probes

src/common/bench_probe.hpp counts and times key rotations for performance work: why each rotation started (its epoch’s bytes, records or age, or the endpoint’s timer or a peer’s REKEY_INIT), the security provider’s INIT, acceptance and ACK steps, the time from INIT to the verified ACK, the records that waited for a new epoch and for how long, and the epoch’s bytes when a rotation started and when the sender switched.

The probes share the timing helpers’ switch. Only -DYUME_DEV_DIAGNOSTICS=ON compiles them. In every other build each probe is an empty type or call, and yume_bench_probe_test checks that at compile time. A probe build records only when YUME_BENCH_PROBE_FILE names a file prefix at start. It then appends one JSON line of totals each second, and a last one at exit, to PREFIX.PID: counts, sums, maxima and power-of-two histograms of nanoseconds or bytes, never keys, payload or addresses. If the recorder cannot open that file, build its name or start its writer thread, the probes stay off and the program runs as usual.

cmake -S . -B build-probe -DCMAKE_BUILD_TYPE=Release -DYUME_DEV_DIAGNOSTICS=ON \
  -DYUME_BUILD_TESTING=ON
cmake --build build-probe -j"$(nproc)" --target yume yumed
YUME_BENCH_PROBE_FILE=/tmp/rotations build-probe/bin/yumed --config yumed.json

A diagnostics build is a benchmark subject of its own. Code layout alone moved a Release build’s single-session rate by about 5 % against the same source with probes, so compare diagnostics builds with diagnostics builds.

Compiler warnings

Option Default Effect
YUME_WARNINGS ON Project warning set on first-party targets
YUME_WARNINGS_AS_ERRORS OFF (ON in CI) Promote those warnings to errors

The set is -Wall -Wextra -Wformat-security -Wvla -Wnon-virtual-dtor on GCC and Clang and /W4 /permissive- on MSVC, applied through yume_apply_warnings() in src/CMakeLists.txt. Most first-party targets get it from yume_apply_perf_opts(). The shared library yume_abi calls yume_apply_warnings() directly, so its optimization settings stay a separate decision from its warnings.

A configure-time audit at the end of src/CMakeLists.txt fails the build if any target that compiles first-party code never received the set. Add an exempt target to YUME_WARNING_EXEMPT_TARGETS with a reason.

The CI test lanes build with -Werror at different optimization levels on purpose. Warnings whose analysis depends on optimization, such as -Wformat-truncation, fire in only some configurations, so gating one lane would leave part of the set unenforced.

Bundled dependencies such as BaseFWX are separate targets or SYSTEM includes and never receive the set, so any warning printed here is about code this tree owns and can fix. Do not silence one with a blanket -Wno-. Fix it or add a narrowly scoped, commented suppression.

The deliberately disabled flags are -Wconversion, -Wsign-conversion, -Wshadow, -Wcast-qual and -Wold-style-cast. Each needs its own cleanup pass instead of a suppression and is enabled when that work is done.

Two diagnostics are demoted from errors rather than disabled, so they still print. Both are limitations of a specific toolchain reported against a system or third-party header, where an in-source pragma does not apply:

Demoted Where Why
-Wstringop-overread GCC 11 only GCC 11 propagates an exact string length proved at the call site into libstdc++’s std::string move assignment, then warns about the short-string branch a string of that length can never take. GCC 12 and later do not emit it.
-Wtsan GCC, ThreadSanitizer lane GCC reports that Boost.Asio’s atomic fences are not modeled by TSan.

Neither is permission to ignore the warning elsewhere. The release toolchain is GCC 13 on ubuntu-24.04, and a source build with GCC 11 still prints a real overread in its log.

Sanitizers

cmake -S . -B build-asan -DCMAKE_BUILD_TYPE=Debug \
  -DYUME_BUILD_TESTING=ON -DYUME_SANITIZE=address+undefined
cmake --build build-asan -j"$(nproc)"
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
  ctest --test-dir build-asan --output-on-failure

YUME_SANITIZE accepts none (the default), address, undefined, address+undefined or thread. It instruments every source-built target in this tree, including bundled BaseFWX sources when the modules are on. Prebuilt vendor archives and system libraries are not instrumented.

The option forces YUME_LTO=OFF, because link-time optimization inlines and reorders across exactly the boundaries a sanitizer reports against. It refuses to combine with YUME_STATIC, since the sanitizer runtime must stay dynamic, and with compilers other than GCC and Clang. UBSan builds add -fno-sanitize-recover=all so a violation aborts instead of printing and continuing, which is what makes it usable as a CI gate.

address and thread are mutually exclusive by construction. Run them as separate configurations. Never distribute a sanitized binary.

Fuzzing

cmake -S . -B build-fuzz -DCMAKE_BUILD_TYPE=RelWithDebInfo \
  -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
  -DYUME_BUILD_TESTING=ON -DYUME_BUILD_FUZZERS=ON
cmake --build build-fuzz -j"$(nproc)" \
  --target yume_fuzz_ytp1_protocol yume_fuzz_ytp1_auth yume_fuzz_config_v1 \
  yume_fuzz_circuit1 yume_fuzz_h2_admission yume_fuzz_cover_site \
  yume_fuzz_websocket
bash tests/fuzz/run_fuzzers.sh build-fuzz/bin 600 fuzz-out

tests/fuzz/run_fuzzers.sh seeds the corpora, runs each harness for the given per-target budget with a bounded input size, and exits nonzero if libFuzzer writes any artifact. CI runs the same script with a short budget as a regression gate. A longer campaign is the same invocation with a larger budget and a corpus carried over from the previous run. tests/fuzz/make_seeds.py generates the seeds as code rather than checking in opaque binaries, and derives the configuration seeds from the schema-1 examples in config/, with one valid variant per schema section, and the circuit seeds from the circuit 1 vectors.

YUME_BUILD_FUZZERS builds libFuzzer harnesses for the parsers that consume input from outside a trust boundary, and requires Clang. It selects YUME_SANITIZE=address+undefined and YUME_LTO=OFF, with coverage and ASan/UBSan checks in the source-built parser libraries as well as the harnesses. Only the harness executables link libFuzzer’s main function. Other sanitizer selections need a separate build. Prebuilt dependencies stay outside this instrumentation, and the harnesses are never installed. Verify parser compile commands and object instrumentation when you keep qualification evidence.

Harness Parser Checks
yume_fuzz_ytp1_protocol YTP/1 frame, OPEN, destination and capability codecs in ytp/protocol.* Every accepted encoding is canonical and re-encodes to the same bytes
yume_fuzz_ytp1_auth AUTH TLV records and the key-schedule encoding in ytp/security.* Unknown critical fields, duplicate IDs, reordered TLVs and wrong suite values are refused, and unknown noncritical fields survive. Every record the encoder accepts decodes back to itself. Key-schedule validation, size and encoding agree, and a buffer one octet short is refused
yume_fuzz_config_v1 The schema-1 parser in config/v1/ Typed rejection is the expected failure, and any other exception is a finding. It opens no credential file
yume_fuzz_circuit1 Circuit 1 cells, handshakes, CREATE bodies, relay messages and BEGIN destinations in circuit/protocol.* Every accepted encoding is canonical and re-encodes to the same bytes
yume_fuzz_h2_admission The admission path, the :authority check against the TLS server name and the replay cache in admission/h2_admission.*, and the proof in providers/ytp1_h2_admission.* An accepted path is canonical and builds back to the same text. A built proof verifies, and fails with another exporter or any one character changed. The replay cache decides as a model of its contract does
yume_fuzz_cover_site The cover site’s answer to a method and request target in providers/cover_site.* Only GET and HEAD select a file, always one of the site’s own. GET and HEAD of a target agree, and HEAD has no body
yume_fuzz_websocket WebSocket framing in either role in stealth/websocket_codec.*, fed in pieces Payload handed back plus framing retired never exceeds the bytes fed. What one role encodes, as one frame, fragments or messages around a ping and a close, the other decodes to the same bytes

Corpora live outside the tree. A crashing input is evidence and does not belong in Git.

Verification

# Production: optimized, no timing code.
cmake -S . -B build-release -DCMAKE_BUILD_TYPE=Release \
  -DYUME_BUILD_TESTING=ON -DYUME_LTO=ON
cmake --build build-release -j"$(nproc)"
ctest --test-dir build-release --output-on-failure

# Developer: the same tests with the diagnostics compiled in.
cmake -S . -B build-dev -DCMAKE_BUILD_TYPE=RelWithDebInfo \
  -DYUME_BUILD_TESTING=ON -DYUME_DEV_DIAGNOSTICS=ON
cmake --build build-dev -j"$(nproc)"
ctest --test-dir build-dev --output-on-failure

yume_timing_test, yume_bench_probe_test and yume_h2_carrier_test assert at compile time that the diagnostics exist exactly when the option says they do, so either run above fails if the switch and the build disagree.

Native test executables keep their checks in optimized builds. Most define their own CHECK macro, which NDEBUG does not remove, and a test that calls assert() is compiled with -UNDEBUG. Keep it that way: a Release or RelWithDebInfo test run must not pass because CMake defined NDEBUG and compiled the checks away.

Benchmark comparisons must state whether YUME_DEV_DIAGNOSTICS was on. Do not compare an instrumented run with an uninstrumented one as if they were identical builds. Sanitized builds are never valid benchmark subjects.