13.4 Using Variable Arguments Properly

13.4.1 Problem

You need a way to protect a function that accepts a variable number of arguments from reading more arguments than were passed to the function.

13.4.2 Solution

Our solution for dealing with a variable number of arguments is actually two solutions. The interface for both solutions is identical, however. Instead of calling va_arg( ), you should call spc_next_varg( ), listed later in this section. Note, however, that the signature for the two functions is different. The code:

my_int_arg = va_arg(ap, int);

becomes:

spc_next_varg(ap, int, my_int_arg);

The biggest difference from using variable argument functions is how you need to make the calls when using this solution. If you can guarantee that your code will be compiled only by GCC and will always be running on an x86 processor (or another processor to which you can port the first solution), you can make calls to the function using spc_next_varg( ) in the normal way. Otherwise, you will need to use the VARARG_CALL_x macros, where x is the number of arguments that you will be passing to the function, including both fixed and variable.

#include <stdarg.h>
#include <stdio.h>
   
#if defined(_ _GNUC_ _) && defined(i386)
/* NOTE: This is valid only using GCC on an x86 machine */
   
#define spc_next_varg(ap, type, var)                               \
  do {                                                             \
    unsigned int _ _frame;                                          \
    _ _frame = *(unsigned int *)_ _builtin_frame_address(0);         \
    if ((unsigned int)(ap) =  = _ _frame - 16) {                      \
      fprintf(stderr, "spc_next_varg(  ) called too many times!\n"); \
      abort(  );                                                     \
    }                                                              \
    (var) = va_arg((ap), (type));                                  \
  } while (0)
   
#define VARARG_CALL_1(func, a1)                             \
  func((a1))
#define VARARG_CALL_2(func, a1, a2)                         \
  func((a1), (a2))
#define VARARG_CALL_3(func, a1, a2, a3)                     \
  func((a1), (a2), (a3))
#define VARARG_CALL_4(func, a1, a2, a3, a4)                 \
  func((a1), (a2), (a3), (a4))
#define VARARG_CALL_5(func, a1, a2, a3, a4, a5)             \
  func((a1), (a2), (a3), (a4), (a5))
#define VARARG_CALL_6(func, a1, a2, a3, a4, a5, a6)         \
  func((a1), (a2), (a3), (a4), (a5), (a6))
#define VARARG_CALL_7(func, a1, a2, a3, a4, a5, a6, a7)     \
  func((a1), (a2), (a3), (a4), (a5), (a6), (a7))
#define VARARG_CALL_8(func, a1, a2, a3, a4, a5, a6, a7, a8) \
  func((a1), (a2), (a3), (a4), (a5), (a6), (a7), (a8))
   
#else
/* NOTE: This should work on any machine with any compiler */
   
#define VARARG_MAGIC    0xDEADBEEF
   
#define spc_next_varg(ap, type, var)                               \
  do {                                                             \
    (var) = va_arg((ap), (type));                                  \
    if ((int)(var) =  = VARARG_MAGIC) {                              \
      fprintf(stderr, "spc_next_varg(  ) called too many times!\n"); \
      abort(  );                                                     \
    }                                                              \
  } while (0)
   
#define VARARG_CALL_1(func, a1)                             \
  func((a1), VARARG_MAGIC)
#define VARARG_CALL_2(func, a1, a2)                         \
  func((a1), (a2), VARARG_MAGIC)
#define VARARG_CALL_3(func, a1, a2, a3)                     \
  func((a1), (a2), (a3), VARARG_MAGIC)
#define VARARG_CALL_4(func, a1, a2, a3, a4)                 \
  func((a1), (a2), (a3), (a4), VARARG_MAGIC)
#define VARARG_CALL_5(func, a1, a2, a3, a4, a5)             \
  func((a1), (a2), (a3), (a4), (a5), VARARG_MAGIC)
#define VARARG_CALL_6(func, a1, a2, a3, a4, a5, a6)         \
  func((a1), (a2), (a3), (a4), (a5), (a6), VARARG_MAGIC)
#define VARARG_CALL_7(func, a1, a2, a3, a4, a5, a6, a7)     \
  func((a1), (a2), (a3), (a4), (a5), (a6), (a7), VARARG_MAGIC)
#define VARARG_CALL_8(func, a1, a2, a3, a4, a5, a6, a7, a8) \
  func((a1), (a2), (a3), (a4), (a5), (a6), (a7), (a8), VARARG_MAGIC)
   
#endif

13.4.3 Discussion

Both C and C++ allow the definition of functions that take a variable number of arguments. The header file stdarg.h defines three macros,[1] va_start( ), va_arg( ), and va_end( ), that can be used to obtain the arguments in the variable argument list. First, you must call the macro va_start( ), possibly followed by an arbitrary number of calls to va_arg( ), and finally, you must call va_end( ).

[1] The ANSI C standard dictates that va_start( ), va_arg( ), and va_end( ) must be macros. However, it does not place any requirements on their expansion. Some implementations may simply expand the macros to built-in function calls (GCC does this). Others may be expressions performing pointer arithmetic (Microsoft Visual C++ does this). Others still may provide some completely different kind of implementation for the macros.

A function that takes a variable number of arguments does not know the number of arguments present or the type of each argument in the argument list; the function must therefore have some other way of knowing how many arguments should be present, so as to not make too many calls to va_arg( ). In fact, the ANSI C standard does not define the behavior that occurs should va_arg( ) be called too many times. Often, the behavior is to keep returning data from the stack until a hardware exception occurs, which will crash your program, of course.

Calling va_arg( ) too many times can have disastrous effects. In Recipe 13.2, we discussed format string attacks against the printf family of functions. One particularly dangerous format specifier is %n, which causes the number of bytes written so far to the output destination (whether it's a string via sprintf( ), or a file via fprintf( )) to be written into the next argument in the variable argument list. For example:

int x;
   
printf("hello, world%n\n", &x);

In this example code, the integer value 12 would be written into the variable x. Imagine what would happen if no argument were present after the format string, and the return address were the next thing on the stack: an attacker could overwrite the return address, possibly resulting in arbitrary code execution.

There is no easy way to protect the printf family of functions against this type of attack, except to properly sanitize input that could eventually make its way down into a call to one of the printf family of functions. However, it is possible to protect variable argument functions that you write against possible mistakes that would leave the code vulnerable to such an attack.

The first solution we've presented is compiler- and processor-specific because it makes use of a GCC-specific built-in function, _ _builtin_frame_address( ), and of knowledge of how the stack is organized on an x86 based processor to determine where the arguments pushed by the caller end. With a small amount of effort, this solution can likely be ported to some other processors as well, but the non-x86 systems on which we have tested do not work (in particular, this trick does not work on Apple Mac G3 or G4 systems). This solution also requires that you do not compile your program using the optimization option to omit the frame pointer, -fomit-frame-pointer, because it depends on having the frame pointer available.

The second solution we have presented should work with any compiler and processor combination. It works by adding an extra argument passed to the function that is a "magic" value. When spc_next_varg( ) gets the next argument by calling va_arg( ) itself, it checks to see whether the value of the argument matches the "magic" value. The need to add this extra "magic" argument is the reason for the VARARG_CALL_x macros. We have chosen a magic value of 0xDEADBEEF here, but if a legitimate argument with that value might be used, it can easily be changed to something else. Certainly, the code provided here could also be easily modified to allow different "magic" values to be used for different function calls.

Finally, note that both implementations of spc_next_varg( ) print an error message to stderr and call abort( ) to terminate the program immediately. Handling this error condition differently in your own program may take the form of throwing an exception if you are using the code in C++, or calling a special handler function. Anything except allowing the function to proceed can be done here. The error should not necessarily be treated as fatal, but it certainly is serious.

13.4.4 See Also

Recipe 3.2