5.3.13 · D5Build Systems & Toolchain

Question bank — Undefined Behavior Sanitizer (UBSan)

1,673 words8 min readBack to topic

Format reminder: each line is Prompt ::: Answer. Cover the right side, forecast, reveal.


Two pictures before the traps

Before drilling, hold two images in your head. First, what "instrumentation" actually means — the compiler weaves a tiny check-then-report block around a risky operation, so the check travels inside your binary rather than watching from outside:

Figure — Undefined Behavior Sanitizer (UBSan)

Notice the red trap branch: a trap is the moment the check fails and control jumps to the reporting handler. Nothing prints on the normal (green) path — that's why UBSan is cheap.

Second, the cost comparison behind the numbers quoted in the T/F section. UBSan adds a few branches near arithmetic; AddressSanitizer (ASan) reroutes every memory access and shadows memory. These are rough community/documentation figures (Clang/GCC sanitizer docs) for typical workloads, not guarantees — your mileage varies by program:

Figure — Undefined Behavior Sanitizer (UBSan)

The takeaway is the shape, not the exact decimals: UBSan is the light one, ASan the heavy one, and this is exactly why they are complementary — dynamic tools you layer, not choose between.


True or false — justify

Recall True or false (cover the answers!)

UBSan can find every kind of undefined behavior in a program. ::: False. It only checks the UB categories it instruments AND only on code paths your tests actually run; strict-aliasing and some uninitialized reads slip past it. Unsigned integer overflow is undefined behavior, so UBSan reports it by default. ::: False. Unsigned overflow is well-defined — it wraps modulo . It is not UB, so plain -fsanitize=undefined says nothing about it. A program that passes all tests under UBSan with -fno-sanitize-recover=all is proven UB-free. ::: False. A clean run is evidence, not proof — it only certifies the exact input paths you exercised, not the untested branches. UBSan is a static analyzer that scans your source without running it. ::: False. UBSan is a dynamic (runtime) tool — see Static Analysis vs Dynamic Analysis. It inserts checks at compile time but only fires them while the program is actually executing. Compiling with -fsanitize=undefined but without -g still gives you correct file-and-line reports. ::: False. Without -g there's no debug info, so reports may lack readable file:line locations even though the check still fires. Turning on -O2 cannot change whether your UB "matters." ::: False. Higher Compiler Optimization Levels let the compiler assume UB never happens and delete safety code (e.g. a null check after a dereference), so the same UB can behave completely differently at -O2 vs -O0. UBSan and ASan detect the same class of bugs, so running one is enough. ::: False. UBSan targets logic/arithmetic/pointer UB; AddressSanitizer (ASan) targets memory errors (out-of-bounds, use-after-free). They overlap little and are meant to run together. Because UBSan inserts checks, it makes programs dramatically slower than ASan. ::: False. Per the sanitizer docs, UBSan adds cheap branches near arithmetic (~1.2–2× time, negligible memory), while ASan reroutes all memory access (~2× time, ~3× memory). UBSan is the lighter one — see figure s02.


Spot the error

Recall Find the flaw in the reasoning (cover the answers!)

"if (ptr == NULL) return; comes right after int v = *ptr;, so I'm safe against null." ::: The dereference already ran; if ptr was null that was UB, and the compiler may delete the later null check because it "proved" ptr is non-null. Check before dereferencing. "n << 32 gave me 1 on my machine, so a shift-by-32 is fine." ::: On x86 the hardware masks the shift count with & 31, so n << 32 becomes n << 0 = 1 — a silently wrong answer. The standard still calls it UB; a different CPU or optimizer can produce anything. "int v = local; use(v); where local was declared but never assigned — it just holds garbage, no harm." ::: Reading an uninitialized variable is UB; the "garbage" can differ per run, and the compiler may assume the read never happens. This is an uninitialized-read trap (MSan territory; UBSan's -fsanitize=undefined does not cover it). "I added a positive and a negative int, so I don't need an overflow check." ::: Correct conclusion, wrong to skip the reasoning: adding opposite signs moves toward zero, so the magnitude can't exceed either input — it always fits. Same-sign additions are the ones that can overflow. "INT_MIN / -1 is just negation, division can't overflow." ::: It can. , which exceeds INT_MAX , so the result is unrepresentable — genuine signed overflow, and some CPUs even trap on it. "UBSan reported an overflow but the program kept running, so it recovered gracefully." ::: By default UBSan reports and continues, which can hide the real first cause. Add -fno-sanitize-recover=all so it aborts on the first violation. "I cast address 1 to a struct S* and only read a field, no write, so it's safe." ::: Reading through a misaligned pointer is itself UB — int needs 4-byte alignment and 1 isn't a multiple of 4. UBSan's alignment check fires regardless of read vs write.


Why questions

Recall Explain the

why (cover the answers!) Why does the C/C++ standard leave signed overflow undefined instead of just defining it to wrap? ::: To let compilers optimize freely (e.g. assume i + 1 > i) without inserting defensive checks everywhere — speed at the cost of a correctness trap. See Integer Overflow & Wraparound. Why can the overflow test be reduced to a single sign-bit comparison? ::: In Two's Complement Representation, same-sign addition that exceeds a limit flips the result's sign bit; comparing that one bit is exactly what the CPU's overflow flag computes — one instruction, no wide arithmetic. Why does UBSan need -fno-sanitize-recover=all to find the root cause? ::: Without it, UBSan reports each violation and keeps going, so a later cascade of errors can bury the first one; aborting immediately pins the true origin. Why is UBSan called "instrumentation" rather than an external checker? ::: Because the compiler weaves the check-then-report code directly into your binary at compile time (in GCC and Clang Toolchains), rather than analyzing or wrapping the program from outside — exactly the woven branch in figure s01. Why does "it ran fine" prove nothing about UB? ::: UB carries no guarantees, so a plausible-looking result is coincidence; a new optimizer, CPU, or optimization level can change or delete the behavior entirely. Why pair UBSan with static analysis instead of trusting it alone? ::: UBSan only sees executed paths (dynamic), while static analysis can flag unexecuted branches; together they cover far more of the program's possible states.


Edge cases

Recall Boundary and degenerate inputs (cover the answers!)

Is INT_MAX + 1 UB, and what does UBSan print? ::: Yes — same-sign addition exceeding INT_MAX. UBSan prints signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'. Is n << 0 (shift by zero) undefined? ::: No. The valid shift range is for a 32-bit int, and 0 is inside it, so it's perfectly defined and UBSan stays silent. Is n << -1 (negative shift count) defined? ::: No — a negative shift amount is UB; the standard's legal range starts at 0, so anything below zero is out of range and UBSan's shift check reports shift exponent -1 is negative. Is n << 31 on a 32-bit int inside the legal shift range? ::: Yes, 31 < 32, so the shift count is legal — though the resulting value may itself overflow depending on the type's signedness, a separate concern. What happens for INT_MIN / 1 (divide by positive one)? ::: Fine — the result is representable, no overflow. Only INT_MIN / -1 (and INT_MIN % -1) overflow. Does dividing by zero for integers get caught by UBSan? ::: Yes — integer division by zero is UB, and UBSan's division check reports it (distinct from the overflow case of INT_MIN / -1). Is reading an uninitialized int an edge case UBSan's default set catches? ::: No — uninitialized reads are UB but fall to MSan (MemorySanitizer), not -fsanitize=undefined. A clean UBSan run says nothing about them. Is a null pointer that you never dereference a problem for UBSan? ::: No. Merely holding a null pointer is legal; the UB (and the check) only occurs on dereference or member access. For unsigned arithmetic, what does UBSan report at the wrap point ? ::: Nothing by default — unsigned wraps cleanly to 0 and is well-defined, so there is no violation to report.