I recently helped someone troubleshoot a bug and saw an interesting case. The following program will have Segmentation Fault, can you see the problem?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdarg.h>

struct Context {
  char *error_;
};

void report_error(struct Context *ctx,
                  const char *format,
                  va_list args) {
  int len = vsnprintf(NULL, 0, format, args);  // XX
  free(ctx->error_);
  ctx->error_ = (char *)malloc(len + 1);
  if (ctx->error_) {
    vsnprintf(ctx->error_, len + 1, format, args);  // XX
  }
}

va_list Introduction

Variadic functions in C are implemented as va_list, va_start, va_arg and va_end defined by <stdarg.h>. Here is a simple example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdarg.h>

void vmyprintf(const char *format, va_list args) {
  while (*format) {
    char ch = *format++;
    switch (ch) {
      case '%':
        switch (*format++) {
          case 'd': {
            int i = va_arg(args, int);
            // ...
            break;
          }
          case 'f': {
            double d = va_arg(args, double);
            // ...
            break;
          }
          // ...
        }
        break;
      default:
        putchar(ch);
        break;
    }
  }
}

void myprintf(const char *format, ...) {
  va_list args;
  va_start(args, format);
  vmyprintf(format, args);
  va_end(args);
}

Among them, va_list is an “implementation-defined” data structure. Its function is equivalent to an iterator of function parameters. We must first initialize the start of the iterator with va_start, then read each parameter in turn with va_arg, and finally release all the resources required by va_list with va_end.

va_list itself can also be passed as an argument to other functions. Commonly used for printf functions starting with v, such as vprintf, vfprintf, vsnprintf, etc.

Problems

Go back to the code at the beginning of this article. The code wants to first call vsnprintf to calculate the amount of memory it needs, then allocate enough memory, and then call vsnprintf again to convert format and args into strings.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void report_error(struct Context *ctx,
                  const char *format,
                  va_list args) {
  int len = vsnprintf(NULL, 0, format, args);  // XX
  free(ctx->error_);
  ctx->error_ = (char *)malloc(len + 1);
  if (ctx->error_) {
    vsnprintf(ctx->error_, len + 1, format, args);  // XX
  }
}

The root of the problem is that the args iterator comes to the end of the vsnprintf function when it is first called. the C language standard has the following notation for the behavior of the vsnprintf function.

  1. As the functions vfprintf , vfscanf , vprintf , vscanf , vsnprintf , vsprintf , and vsscanf invoke the va_arg macro, the value of arg after the return is indeterminate. – C 11 (N1570), p. 327

In other words, if you read the function argument from the same va_list with va_arg after the vsnprintf return, the value of va_arg after the return is indeterminate. In my test environment, va_arg(args, const char *) returns the wrong address, which leads to a Segmenetation Fault.

How to fix it

Because we have to access the arguments twice, we should make a copy of va_list with va_copy before accessing the arguments for the first time. Example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void report_error(struct Context *ctx,
                  const char *format,
                  va_list args) {
  va_list args_copy;                                // ADDED
  va_copy(args_copy, args);                         // ADDED
  int len = vsnprintf(NULL, 0, format, args_copy);  // MODIFIED
  va_end(args_copy);                                // ADDED

  free(ctx->error_);
  ctx->error_ = (char *)malloc(len + 1);
  if (ctx->error_) {
    vsnprintf(ctx->error_, len + 1, format, args);
  }
}

Alternatively, if you can use the GNU extension function (compiled with the definition _GNU_SOURCE), we can call the vasprintf function directly. vasprintf will directly calculate the required memory space, configure the memory, and output the string.

1
2
3
4
5
6
7
8
9
#define _GNU_SOURCE
#include <stdio.h>

void report_error(struct Context *ctx,
                  const char *format,
                  va_list args) {
  free(ctx->error_);
  vasprintf(&ctx->error_, format, args);
}

However, the actual code I encountered was not configured with malloc memory, so I did not use this modification.