Why Passing NULL to Variadic Functions Triggers Undefined Behavior (varFuncNullUB)
Understanding the varFuncNullUB Warning in C
If you have worked with variadic functions in C or C++—functions that accept a variable number of arguments such as printf() or custom functions using stdarg.h—you might have encountered static analyzer warnings like varFuncNullUB when passing NULL as a sentinel argument. A common example looks like this:
EXAMPLE(line, 0, "typedef", "unsigned", "int", NULL);Your static analyzer flags this line with the following message:
warning: Passing NULL after the last typed argument to a variadic function leads to undefined behaviour. [varFuncNullUB]
In this guide, we will unpack why passing uncast NULL causes undefined behavior, why explicitly casting it to (const char *)NULL resolves the issue, and how variadic argument promotion works under the hood.
1. Why Does Passing Plain NULL Trigger Undefined Behavior?
To understand the root cause of this warning, you must look at how the NULL macro is defined and how C handles argument passing for variadic parameters.
How NULL is Defined
In C, standard headers define NULL as an implementation-defined null pointer constant. It is typically defined as one of the following:
00L((void *)0)
In C++, NULL is almost always defined simply as 0 or 0L because implicitly casting void* to other pointer types is not allowed without an explicit cast.
Default Argument Promotions
When you pass arguments to the variadic portion of a function (the ... parameters), the compiler does not have explicit parameter types to convert arguments into. Instead, C applies default argument promotions:
- Integral types smaller than
int(such ascharorshort) are promoted tointorunsigned int. floattypes are promoted todouble.- Pointers and standard types like
intorlongremain as their promoted types.
If NULL is defined as integer zero (0), the compiler passes an int parameter on the call stack or in CPU registers. However, inside your variadic function, you extract arguments using va_arg(args, const char *), expecting a pointer parameter.
Type and Size Mismatches (Especially on 64-bit Systems)
On standard 64-bit systems (x86-64 LP64 architecture):
- An
intis 32 bits (4 bytes). - A pointer (
const char *) is 64 bits (8 bytes).
When va_arg(args, const char *) executes, it attempts to read 8 bytes from the stack or register state. If you passed an uncast 0 (a 4-byte int), va_arg reads 4 bytes of your 0 value plus 4 bytes of adjacent uninitialized stack garbage. This results in a non-null invalid pointer address, causing memory corruption or crashes—a classic case of Undefined Behavior.
2. Is Casting NULL to (const char *) the Correct Solution?
Yes, absolutely. Explicitly casting NULL to the expected pointer type is both standard-compliant and portable.
EXAMPLE(line, 0, "typedef", "unsigned", "int", (const char *)NULL);Or using the target pointer type:
EXAMPLE(line, 0, "typedef", "unsigned", "int", (char *)NULL);3. Why Does the Cast Make a Difference?
When you add an explicit typecast like (const char *)NULL, you instruct the compiler about the precise type of the expression being passed to the variadic function:
- The compiler evaluates
(const char *)NULLas a null pointer of typeconst char *. - It allocates the full width required for a pointer argument (e.g., 64 bits on a 64-bit architecture).
- When
va_arg(args, const char *)fetches the argument inside the function, the binary alignment, size, and type match perfectly.
Best Practices for Variadic Functions and Sentinels
- Always cast sentinel NULL values: Functions like POSIX
execl()or custom variadic functions that useNULLas a terminator should always receive explicitly cast pointers (e.g.,(char *)NULL). - Use
nullptrin C++11 and modern C (C23): Modern standards introduce thenullptrkeyword (typestd::nullptr_t/nullptr_t), which eliminates ambiguity between integer zero and pointer zero. - Leverage Compiler Attributes: If you are using GCC or Clang, you can decorate your custom variadic functions with
__attribute__((sentinel))to have the compiler automatically verify that the function call ends with a null pointer.
Summary
Passing uncast NULL to variadic functions is risky because NULL may expand to an integer literal 0, passing a smaller type than the expected pointer. Explicitly casting to (const char *)NULL guarantees proper argument sizing on the stack, satisfying static code analyzers and preventing undefined behavior.