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.