The C standard library macro va_start()
defines several semantic restrictions on the value of its second parameter. The C Standard, subclause 7.16.1.4, paragraph 4 [ISO/IEC 9899:2011], states:
The parameter parmN is the identifier of the rightmost parameter in the variable parameter list in the function definition (the one just before the
...
). If the parameter parmN is declared with theregister
storage class, with a function or array type, or with a type that is not compatible with the type that results after application of the default argument promotions, the behavior is undefined.
These restrictions are superseded by the C++ Standard, [support.runtime], paragraph 3 [ISO/IEC 14882-2014], which states:
The restrictions that ISO C places on the second parameter to the
va_start()
macro in header<stdarg.h>
are different in this International Standard. The parameterparmN
is the identifier of the rightmost parameter
in the variable parameter list of the function definition (the one just before the...
). If the parameterparmN
is of a reference type, or of a type that is not compatible with the type that results when passing an argument for which there is no parameter, the behavior is undefined.
The primary differences between the semantic requirements are as follows:
- You must not pass a reference type as the second argument to
va_start()
. - Passing a nontrivially copyable type as the second argument to
va_start
is conditionally supported with implementation-defined semantics ([expr.call] paragraph 7). - You may pass a parameter declared with the
register
keyword ([dcl.stc] paragraph 3) or a parameter with a function type.
Passing an array type still produces undefined behavior in C++ because an array type as a function parameter requires use of a reference, which is prohibited.
Noncompliant Code Example
In this noncompliant code example, a reference type is passed as the second argument to va_start()
:
#include <cstdarg> #include <iostream> void f(int &a, ...) { va_list list; va_start(list, a); if (a) { std::cout << a << ", " << va_arg(list, int); a = 100; // Assign something to a for the caller } va_end(list); }
Compliant Solution
Instead of passing a reference type to f()
, this compliant solution passes a pointer type:
#include <cstdarg> #include <iostream> void f(int *a, ...) { va_list list; va_start(list, a); if (a && *a) { std::cout << a << ", " << va_arg(list, int); *a = 100; // Assign something to *a for the caller } va_end(list); }
Noncompliant Code Example
In this noncompliant code example, a nontrivially copyable type is passed as the second argument to va_start()
, which is conditionally supported depending on the implementation:
#include <cstdarg> #include <iostream> #include <string> void f(std::string s, ...) { va_list list; va_start(list, s); std::cout << s << ", " << va_arg(list, int); va_end(list); }
Compliant Solution
This compliant solution passes a const char *
instead of a std::string
, which has well-defined behavior on all implementations:
#include <cstdarg> #include <iostream> void f(const char *s, ...) { va_list list; va_start(list, a); std::cout << (s ? s : "") << ", " << va_arg(list, int); va_end(list); }
Risk Assessment
Passing a reference type or nontrivially copyable type as the second argument to va_start()
can result in undefined behavior that might be exploited to cause data integrity violations.
Rule | Severity | Likelihood | Remediation Cost | Priority | Level |
---|---|---|---|---|---|
EXP58-CPP | Medium | Unlikely | Medium | P4 | L3 |
Automated Detection
Tool | Version | Checker | Description |
---|---|---|---|
Clang | 3.9 | -Wvarargs | Does not catch all instances of this rule, such as the second noncompliant code example |
Related Vulnerabilities
Search for vulnerabilities resulting from the violation of this rule on the CERT website.
Bibliography
[ISO/IEC 9899:2011] | 7.16.1.4, "The va_start macro" |
[ISO/IEC 14882-2014] | 18.10, "Other Runtime Support" |