Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

A variadic function – a function declared with a parameter list ending with ellipsis (...) – can 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 in that they accept a varying number of arguments of differing types. Variadic functions are flexible, but they are also hazardous. The compiler can't verify that a given call to a variadic function passes an appropriate number of arguments or that those arguments have appropriate types. Consequently, 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 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.

The best way to avoid calling variadic functions is the avoid defining them. However, declaring them does no harm.

Do not define C-style variadic functions. Issues with C-style variadic functions can be avoided by using variadic functions defined with function parameter packs for situations where a variable number of arguments should be passed to a function. Additionally, function currying can be used to build object state piecemeal, such as the standard output stream does with its std::cout::operator<<() overloads.

Note that the declaration of C-style variadic functions is not harmful, and can be useful in unevaluated contexts. When When a function call expression appears in some contexts, notably an unevaluated context, such as the argument in a sizeof expression, the compiler performs overload resolution is performed to determine the result type of the call, but the object code doesn't execute the call at runtime. In such cases, the compiler uses only the function's declaration, not its definition.Some does not require a function definition. Some template metaprogramming techniques that employ "substitution failure is not an error" (SFINAE) use variadic functions to implement compile-time type queries, as in:

Code Block
languagecpp
template <typename Ty>
class has_foo_function {
  typedef char Trueyes[1];
typedef struct {typedef char ano[2];

 } False;

template <typename T>
True isPtr(T *);

False isPtrInner>
  static yes& test(Inner *I, decltype(I->foo()) * = nullptr);

  template <typename>
  static no& test(...);


#define is_ptr(e) (sizeof(isPtr(epublic:
  static const bool value = sizeof(test<Ty>(nullptr)) == sizeof(True))yes);
};

In this example, is_ptr(e) returns true if expression e has a pointer type. is_ptr(e) calls variadic function isPtr(...), but the call takes place in a sizeof expression. Consequently, isPtr(...) must be declared, but it need not, and should not, be defined.

Non-compliant Code Example

the value of value is determined based on 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.

Noncompliant Code Example

This noncompliant code example uses a C-style variadic function to add a series of integers together, until the value 0 is found. Calling this function without passing the value 0 as an argument results in undefined behavior. Further, passing any type other than an int also results in undefined behaviorThis example uses a variadic function to concatenate an arbitrary number of null-terminated character sequences (NTCS) in a single NTCS. Each call to the function must use a null pointer value to mark the end of the argument list.

Code Block
bgColor#FFCCCC
langcpp
#include <cstdarg>

charint *concatenate(char const *sAdd(int First, int Second, ...) {
  int  {R = First + Second;  
  va_list va;
 // code to actually concatenate the strings
    }

char *separator = /* some reasonable value */;

char *t = concatenate("hello", separator, "world", NULL);

Calling this function without the trailing null pointer, or with an argument of any type other than "pointer to possibly-CV-qualified char" yields undefined behavior:

Code Block
bgColor#FFCCCC
langcpp
char *u = concatenate("hello", separator, "world"); // undefined behavior

char *v = concatenate("hello", ' ', "world", NULL); // undefined behavior

Compliant Solution

 va_start(va, First);
  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 an integer, it results in the code being ill-formed, instead of undefined behavior.

Code Block
bgColor#ccccff
langcpp
#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...);
}

Note, this compliant solution makes use of std::enable_if to ensure that any non-integral argument values result 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-init-list. Since narrowing conversions are not allowed in a braced-init-list, the type safety is preserved despite the std::enable_if not involving any of the variadic arguments.Rather than use a variadic function, you can use a chain of binary operations:

Code Block
bgColor#ccccff
langcpp
#include <string>

string separator = /* some reasonable value */;

string s = "hello" + separator + "world";
 <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;
}

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

DCL31-CPP

highHigh

probableProbable

lowMedium

P18P12

L1

Automated Detection

Tool

Version

Checker

Description

 PRQA QA-C++

 
Include Page
PRQA QA-C++_v
PRQA QA-C++_v

2012
2625

 

Related Vulnerabilities

Search for other vulnerabilities resulting from the violation of this rule on the CERT website.

Related Guidelines

 

 

Bibliography

...

[ISO/IEC 14882-2014]5.2.2, "Function call"
14.5.3, "Variadic templates" 

 TODO