The C standard library macro va_start()
defines several semantic restrictions on the type of the value of its second parameter. The C Standard, subclause 7.16.1.4, paragraph 4 [ISO/IEC 9899:2011], states:
...
- You must not pass a reference type as the second argument to
va_start()
. - Passing an object of a nontrivially copyable type class type which has a nontrivial copy constructor, nontrivial move constructor, or nontrivial destructor 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 object of array type still produces undefined behavior in C++ because an array type as a function parameter requires use of a reference, which is prohibited. Additionally, passing an object of a type that undergoes default argument promotions still produces undefined behavior in C++ as well.
Noncompliant Code Example
In this noncompliant code example, the object passed to va_start()
will undergo a default argument promotion, which results in undefined behavior:
Code Block | ||||
---|---|---|---|---|
| ||||
#include <cstdarg> void f(float a, ...) { va_list list; va_start(list, a); // ... va_end(list); } |
Compliant Solution
In this compliant solution, f()
accepts a double
instead of a float
:
Code Block | ||||
---|---|---|---|---|
| ||||
#include <cstdarg>
void f(double a, ...) {
va_list list;
va_start(list, a);
// ...
va_end(list);
}
|
Noncompliant Code Example
In this noncompliant code example, a reference type is passed as the second argument to va_start()
:
Code Block | ||||
---|---|---|---|---|
| ||||
#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:
Code Block | ||||
---|---|---|---|---|
| ||||
#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:
Code Block | ||||
---|---|---|---|---|
| ||||
#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:
Code Block | ||||
---|---|---|---|---|
| ||||
#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 an object of an unsupported 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 |
| -Wvarargs | Does not catch all instances of this rule, such as the violation in 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] | Subclause 7.16.1.4, "The va_start Macro" |
[ISO/IEC 14882-2014] | Subclause 18.10, "Other Runtime Support" |
...