Functions can be defined to accept more formal arguments at the call site than are specified by the parameter declaration clause. Such functions are called variadic functions because they can accept a variable number of arguments from a caller. C++ provides two mechanisms by which a variadic function can be defined: function parameter packs and use of a C-style ellipsis as the final parameter declaration.
Variadic functions are flexible because they accept a varying number of arguments of differing types. However, they can also be hazardous. A variadic function using a C-style ellipsis (hereafter called a C-style variadic function) has no mechanisms to check the type safety of arguments being passed to the function or to check that the number of arguments being passed matches the semantics of the function definition. Consequently, a runtime call to a C-style variadic function that passes inappropriate arguments yields undefined behavior. Such undefined behavior could be exploited to run arbitrary code.
Do not define C-style variadic functions. (The declaration of a C-style variadic function that is never defined is permitted, as it is not harmful and can be useful in unevaluated contexts.)
Issues with C-style variadic functions can be avoided by using variadic functions defined with function parameter packs for situations in which a variable number of arguments should be passed to a function. Additionally, function currying can be used as a replacement to variadic functions. For example, in contrast to C's printf()
family of functions, C++ output is implemented with the overloaded single-argument std::cout::operator<<()
operators.
Noncompliant Code Example
This noncompliant code example uses a C-style variadic function to add a series of integers together. The function reads arguments until the value 0
is found. Calling this function without passing the value 0
as an argument (after the first two arguments) results in undefined behavior. Furthermore, passing any type other than an int
also results in undefined behavior.
#include <cstdarg> int add(int first, int second, ...) { int r = first + second; va_list va; va_start(va, second); while (int v = va_arg(va, int)) { r += v; } va_end(va); return r; }
Compliant Solution (Recursive Pack Expansion)
In this compliant solution, a variadic function using a function parameter pack is used to implement the add()
function, allowing identical behavior for call sites. Unlike the C-style variadic function used in the noncompliant code example, this compliant solution does not result in undefined behavior if the list of parameters is not terminated with 0
. Additionally, if any of the values passed to the function are not integers, the code is ill-formed rather than producing undefined behavior.
#include <type_traits> template <typename Arg, typename std::enable_if<std::is_integral<Arg>::value>::type * = nullptr> int add(Arg f, Arg s) { return f + s; } template <typename Arg, typename... Ts, typename std::enable_if<std::is_integral<Arg>::value>::type * = nullptr> int add(Arg f, Ts... rest) { return f + add(rest...); }
This compliant solution makes use of std::enable_if
to ensure that any nonintegral argument value results in an ill-formed program.
Compliant Solution (Braced Initializer List Expansion)
An alternative compliant solution that does not require recursive expansion of the function parameter pack instead expands the function parameter pack into a list of values as part of a braced initializer list. Since narrowing conversions are not allowed in a braced initializer list, the type safety is preserved despite the std::enable_if
not involving any of the variadic arguments.
#include <type_traits> template <typename Arg, typename... Ts, typename std::enable_if<std::is_integral<Arg>::value>::type * = nullptr> int add(Arg i, Arg j, Ts... all) { int values[] = { j, all... }; int r = i; for (auto v : values) { r += v; } return r; }
Exceptions
DCL50-CPP-EX1: It is permissible to define a C-style variadic function if that function also has external C language linkage. For instance, the function may be a definition used in a C library API that is implemented in C++.
DCL50-CPP-EX2: As stated in the normative text, C-style variadic functions that are declared but never defined are permitted. For example, when a function call expression appears in an unevaluated context, such as the argument in a sizeof
expression, overload resolution is performed to determine the result type of the call but does not require a function definition. Some template metaprogramming techniques that employ SFINAE use variadic function declarations to implement compile-time type queries, as in the following example.
template <typename Ty> class has_foo_function { typedef char yes[1]; typedef char no[2]; template <typename Inner> static yes& test(Inner *I, decltype(I->foo()) * = nullptr); // Function is never defined. template <typename> static no& test(...); // Function is never defined. public: static const bool value = sizeof(test<Ty>(nullptr)) == sizeof(yes); };
In this example, the value of value
is determined on the basis of which overload of test()
is selected. The declaration of Inner *I
allows use of the variable I
within the decltype
specifier, which results in a pointer of some (possibly void
) type, with a default value of nullptr
. However, if there is no declaration of Inner::foo()
, the decltype
specifier will be ill-formed, and that variant of test()
will not be a candidate function for overload resolution due to SFINAE. The result is that the C-style variadic function variant of test()
will be the only function in the candidate set. Both test()
functions are declared but never defined because their definitions are not required for use within an unevaluated expression context.
Risk Assessment
Incorrectly using a variadic function can result in abnormal program termination, unintended information disclosure, or execution of arbitrary code.
Rule | Severity | Likelihood | Remediation Cost | Priority | Level |
---|---|---|---|---|---|
DCL50-CPP | High | Probable | Medium | P12 | L1 |
Automated Detection
Tool | Version | Checker | Description |
---|---|---|---|
Astrée | 22.10 | function-ellipsis | Fully checked |
Axivion Bauhaus Suite | 7.2.0 | CertC++-DCL50 | |
Clang | 3.9 | cert-dcl50-cpp | Checked by clang-tidy . |
CodeSonar | 8.1p0 | LANG.STRUCT.ELLIPSIS | Ellipsis |
Helix QAC | 2024.3 | C++2012, C++2625 | |
Klocwork | 2024.3 | MISRA.FUNC.VARARG | |
LDRA tool suite | 9.7.1
| 41 S | Fully Implemented |
Parasoft C/C++test | 2023.1 | CERT_CPP-DCL50-a | Functions shall not be defined with a variable number of arguments |
Polyspace Bug Finder | R2024a | CERT C++: DCL50-CPP | Checks for function definition with ellipsis notation (rule fully covered) |
RuleChecker | 22.10 | function-ellipsis | Fully checked |
SonarQube C/C++ Plugin | 4.10 | FunctionEllipsis |
Related Vulnerabilities
Search for other vulnerabilities resulting from the violation of this rule on the CERT website.
Bibliography
[ISO/IEC 14882-2014] | Subclause 5.2.2, "Function Call" Subclause 14.5.3, "Variadic Templates" |
16 Comments
Martin Sebor
Although I agree that the misuses pointed out in this rule can lead to serious bugs I don't think it's realistic to expect existing projects to abandon vararg functions and convert to the recommended solutions, especially those that make use of internationalization APIs like
gettext
. Forprintf
-like functions, GCC and compatible compilers provide theformat
andformat_arg
attributes that together with the-Wformat
family of options help detect and prevent the problems discussed here. I would suggest adding an exception allowing these types of functions (i.e., those that are checked by the implementation).Aaron Ballman
The
-Wformat
style of flags are not generalized to help with writing your own varargs function unless it uses the same format strings as printf() (and friends). What's more, that is not a portable solution because not all compilers support that kind of checking of format strings. So at best, this would be a very limited, nonportable exception. I'm not certain such an exception would really be useful – if your implementation has format string checks, and your varargs function happens to use the same format specifiers as printf(), I think it's better to simply document the function definition as not complying with this rule and provide rationale for why that's okay.Martin Sebor
My suggestion is to add an exception for functions that the compiler knows to check (like those decorated with GCC attribute
format
), not to others. Many portable C++ projects define vararg functions for error reporting (all those I work with, such as GCC and GDB). They make use of attributeformat
to help detect bugs when GCC or a compatible compiler is used to compile them. It's misleading for the coding standard to suggest they're subject to the type safety problems mentioned here when they are detected and prevented by the compiler they most commonly use. Few projects go to the effort of documenting their non-compliance with any given coding standard (I don't know of any). In my view, the greatest value of this coding standard is in educating engineers about what is unsafe and in offering viable alternatives that make it safer. Declaring that all vararg functions are necessarily unsafe is inaccurate and diminishes the standard's practical value. When the recommended alternatives are also not viable replacements for an essential feature like internationalization a rule against using the feature becomes pointless.Aaron Ballman
I'm not strongly opposed to this, but I am certainly uncomfortable with it.
(1) These attributes are compiler extensions that are not supported by all major compiler vendors (MSVC comes to mind immediately). We have no other exceptions in the C++ rules for specific compiler extensions. Adding exceptions for vendor-specific extensions isn't awful in and of itself (though I do worry about it being a slippery slope), but our rules focus on what you can do with ISO C++ and only stray into implementation details when it is really beneficial to do so.
(2) These attributes are really hard to write properly because the GNU-style attribute requires positional indexing rather than placing the attribute directly on the format string argument itself. I see people use these attributes wrong frequently, and they are very fragile when it comes to modifications to the function signature. I am not keen on suggesting to use this arcane, fragile construct to support type unsafe C-style variadic functions rather than write a type-safe variadic function instead.
(3) What makes these attributes special, compared to, say
_Printf_format_string_
and friends that Microsoft Visual Studio supports? Or do you think we should have exceptions for those attributes as well? What if another vendor (say, Embarcadero or Digital Mars) has yet another way to solve this? This is part of what I meant above by "slippery slope".With those concerns in mind, do you still think it's worth having an exception? If so, would you mind drafting such an exception? That way we have something more concrete to work with for discussion, as it's possible I may be worrying about things you're not actually recommending.
Martin Sebor
I agree that code should be safe and conforming first. I will usually be among the first to recommend against relying on non-portable extensions. But in cases where the only available alternatives are not viable for many projects or in common environments and where there are language extensions (like GCC attributes or MSVC source code annotations) that overcome the safety concerns I think the coding standard should mention them and make exceptions for using them. I don't think we need to have an exhaustive list of these extensions to introduce an exception. Mentioning as examples those we know are in widespread use (like GCC attribute
format
) should be good enough. More can be added as we learn about them.Trevor Saunders
So, while I personally prefer printf to C++ stream style output I'm pretty ok with this rule as is. Also worth noting I agree the gettext issue of translating output can be pretty important for some applications, and its worth noting because languages word around numbers and stuff differently you kind of need printf like rather than stream concatonation (I'm no expert just what I think I understand about the problem). There is actually a way out though, you can implement your printf like function using variadic templates and constexpr. I'm not aware of a compatable implementation yet, but there are a couple starts on github. Tom Tromey wrote one, and I believe another is called safe-printf? Anyway that seems like a valuable thing to have so its somewhere on my list of things to do in free time. Rust has more or less taken this approach too, there standard output routine is println!() which is actually a macro that takes arguments and does formatting in a safe way.
as a wording point this rule doesn't seem to say anything about calling variadic functions, just defining them. Maybe there's another rule I'm forgeting that prevents you from calling all the variadic functions you like if they are defined in C like say printf, open, or execl. Discouraging calling ones outside of libc that aren't printf wrappers is probably a good idea. Ones in libc might be trickier, I'm not sure there's a good reason to use execl, and we are debating printf, but it seems hard to argue calling open() shouldn't be allowed?
David Svoboda
This rule does not forbid calling variadic functions, just defining them. Feel free to call printf(), open(), and exec*() to your heart's content.
Trevor Saunders
yes, I was trying to point that out in the second paragraph with "as a wording point..." but I guess I confused that with asking the question should calling some set of variadic functions be discouraged. Obviously a rule discouraging calling certain functions wouldn't belong in a group of rules about defining things.
So while its a little off topic for this rule if you take the position that format attributes don't make it safe enough to define printf like functions then it would seem logically more consistant to say you shouldn't call variadic printf like functions defined for you. Of course that would be controversial so maybe not worth trying even if it is more consistant?
Sorry about being confusing and then off topic.
David Svoboda
No problem. It could be argued that defining your own printf-style function for a program that only was compiled with GCC might be safe because GCC provides strong(?) typechecking for such functions. One could make similar arguments for platforms with similar features...no doubt there are other such features, which was Martin's argument.
I would expect the main advantage to using printf() over iostreams is the automated translation abilities of libraries like gettext() are more compatible with printf() than iostreams.
Finally, we do have a general principle that these secure coding rules presume you are running an app on platforms which make no extra guarantees about undefined behavior (or offer no extra features such as GCC's format attribute). So while I many not wish to add a specific exception for GCC code defining its own printf variant, I would not claim such code violates the rule if I am made aware that the code is GCC-specific. This principle lets us avoid arguments like "this works fine on my platform!".
OldChi
How will an attacker exploit the C-style variatic function vulnerability? What will the attack vector be like?
Aaron Ballman
One example would be the undefined behavior you get when a format specifier does not match the actual type passed – there are quite a few CVEs like that.
OldChi
Thank you Aaron. I'll go to CVE website to look for more information.
Joseph C. Sible
There is no vulnerability if you use C-style variadic functions correctly. The reason you should avoid them is that it's very easy to use them incorrectly, and using them incorrectly leads to vulnerabilities like
printf
format string attacks. The compliant solutions are safer, as using them incorrectly will result in a compiler error instead of potentially exploitable undefined behavior.OldChi
Thank you Joseph. Your conclusion inspired me a lot.
Alireza
take a look on initializer_list<> as parameter in c++ function
Joseph C. Sible
Isn't this usually not very useful in places that people use variadic functions, because all of its contents are of the same type?