Using undefined behaviour sanitizer to catch errors.

Ubsan Turns Silent Assumptions Into Loud Failures

I spent three weeks in a high-frequency trading shop chasing a ghost that only appeared when the production servers hit a specific thermal threshold. It wasn’t a logic error or a race condition in the traditional sense; it was a subtle violation of the integer promotion rules that the compiler had exploited to “optimize” a branch out of existence. Most tutorials treat the undefined behaviour sanitizer as some optional luxury for academic perfectionists, but after seeing a multi-million dollar execution engine stutter because of a signed overflow, I realized it’s actually a survival tool.

I’m not here to give you a lecture on the formal semantics of the ISO standard or a tutorial on how to flip a compiler flag. Instead, I’m going to show you how to integrate an undefined behaviour sanitizer into a real-world CI pipeline so it actually catches the bugs that matter. We will focus on the specific, nasty edge cases where the sanitizer shines and, more importantly, how to filter out the noise so you aren’t drowning in false positives while trying to ship code.

Table of Contents

Detecting C Undefined Behavior Before the Crash

Detecting C Undefined Behavior Before the Crash

You can’t debug what you can’t see. The problem with most UB is that it doesn’t trigger a crash immediately; it just silently corrupts a register or skips a branch, leaving you to chase a ghost three modules away. Relying on manual inspection or even a debugger is a fool’s errand when the logic is technically valid according to the syntax but illegal according to the standard. This is where compiler instrumentation tools become your first line of defense. Instead of hoping the hardware catches a fault, you force the compiler to inject checks directly into the binary.

When I’m setting up a CI pipeline, I lean heavily on the LLVM sanitizer suite. It’s not just about catching a null pointer; it’s about identifying the subtle, logical violations that occur during execution. By integrating specific flags into your build process, you shift from reactive firefighting to proactive runtime error detection. You aren’t just looking for a segfault anymore; you are hunting for the exact moment your code stops respecting the contract of the language. It turns “it works on my machine” into something much more rigorous.

The Hidden Cost of Ignoring Runtime Error Detection

The Hidden Cost of Ignoring Runtime Error Detection

The cost of ignoring runtime error detection isn’t just a crash; it’s the insidious way a program continues to run while silently corrupting its own state. In my time writing low-latency code, I’ve seen “impossible” logic branches execute simply because a signed integer overflow allowed the compiler to optimize away a critical safety check. When you rely solely on static analysis, you’re only seeing what the code looks like. You aren’t seeing what the machine actually does when the data hits the metal.

This is where the LLVM sanitizer suite becomes indispensable. Without proper compiler instrumentation tools, you are essentially flying blind, hoping that your test coverage is perfect—it never is. The real danger lies in the delta between your local dev environment and production. A bug that manifests as a minor calculation error on your machine can become a catastrophic security vulnerability under specific load conditions. If you aren’t actively debugging memory safety issues and logic violations during your CI pipeline, you aren’t actually testing your software; you’re just waiting for the inevitable production post-mortem.

Field Notes: How to Actually Use UBSan Without Losing Your Mind

  • Don’t run it in production. UBSan adds overhead—not as much as Valgrind, but enough that your latency-sensitive loops will feel the drag. Use it in your CI pipeline or your local test suite, where the goal is finding the bug, not hitting your performance targets.
  • Pair it with AddressSanitizer (ASan). UBSan is great at catching logical nonsense like integer overflows or misaligned pointers, but it doesn’t track memory ownership. If you aren’t running them together, you’re only seeing half the disaster.
  • Treat every report as a critical failure. In C++, “undefined” doesn’t mean “it might crash”; it means the compiler is legally allowed to optimize your logic right out of existence. If UBSan flags a signed integer overflow, don’t just “fix” the symptom—fix the assumption that the math was safe.
  • Use the `-fno-sanitize-recover` flag during testing. By default, UBSan often just prints a warning and keeps going. That’s useless for automated testing. Force the program to abort on the first sign of trouble so your test runner actually fails.
  • Watch out for false positives in third-party headers. You’ll eventually run into a legacy library that does something clever (and wrong) with bitwise operations. Use a suppression file to silence the noise so you can focus on the code you actually own.

The Bottom Line

Stop treating Undefined Behavior as a theoretical concern; if you aren’t using UBSan, you are essentially gambling that your compiler’s optimizer won’t decide to delete your logic entirely.

Integration into your CI/CD pipeline isn’t optional overhead—it’s the only way to catch the subtle, non-crashing violations that turn into production nightmares weeks after deployment.

Real-world C++ mastery isn’t just about knowing the syntax; it’s about using the right tooling to enforce the rules the language specification actually demands.

The Reality Check

At the end of the day, UBSan isn’t a luxury; it’s a fundamental part of a sane development workflow. We’ve looked at how it surfaces the subtle, non-crashing violations that static analysis misses and why ignoring these runtime signals is essentially inviting technical debt into your codebase. You can keep relying on luck and hoping your test suite hits that one specific branch where the integer overflow occurs, but that isn’t engineering—it’s gambling. Integrating these checks into your CI pipeline ensures that you aren’t just shipping code that works, but code that actually adheres to the language specification under pressure.

C++ is a powerful tool, but it is a blunt instrument if you don’t respect its boundaries. My time in high-frequency trading taught me that the most expensive bugs aren’t the ones that crash your process immediately, but the ones that silently corrupt your state while the system continues to run. Use UBSan to bridge the gap between what you think your code is doing and what the machine is actually doing. Stop treating undefined behavior as an abstract concept and start treating it as a documented failure of your logic. That is how you write software that lasts.

About Ruaridh Kensington-Oyelaran

C++ rewards people who know what the compiler is allowed to do. I write about the rules that bite, the ones nobody mentions until you have already shipped the bug.

More From Author

Transform for element wise work.

Transform Says What You Mean Better Than a for Loop

Debug versus release builds undefined behavior.

A Bug That Only Appears in Release Is Usually Undefined Behaviour