Is GCC mishandling a pointer to a va_list passed to a function?

Jonathan Leffler picture Jonathan Leffler · Nov 8, 2011 · Viewed 10.5k times · Source

The question 'Pass va_list or pointer to va_list?' has an answer which quotes the standard (ISO/IEC 9899:1999 - §7.15 'Variable arguments <stdarg.h>, footnote 212) as explicitly saying that:

It is permitted to create a pointer to a va_list and pass that pointer to another function, in which case the original function may make further use of the original list after the other function returns.

I'm compiling some code which can be exemplified by the following (the real code is very considerably more complex, with the original functions doing a lot more work than shown here).

vap.c

#include <stdarg.h>
#include <stdio.h>

static void test_ptr(const char *fmt, va_list *argp)
{
    int x;
    x = va_arg(*argp, int);
    printf(fmt, x);
}

static void test_val(const char *fmt, va_list args)
{
    test_ptr(fmt, &args);
}

static void test(const char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);   /* First use */
    test_val(fmt, args);
    va_end(args);
    va_start(args, fmt);   /* Second use */
    test_ptr(fmt, &args);
    va_end(args);
}

int main(void)
{
    test("%d", 3);
    return 0;
}

Error messages

When I compile it (on RHEL5 with GCC 4.1.2 or 4.5.1), I get the following error messages. Notice how much more informative the 4.5.1 error message is - the GCC team is to be congratulated on the improvement!

$ gcc --version
gcc (GCC) 4.5.1
Copyright (C) 2010 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ /usr/bin/gcc --version
gcc (GCC) 4.1.2 20080704 (Red Hat 4.1.2-44)
Copyright (C) 2006 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ gcc -c vap.c
vap.c: In function ‘test_val’:
vap.c:13:5: warning: passing argument 2 of ‘test_ptr’ from incompatible pointer type
vap.c:4:13: note: expected ‘struct __va_list_tag (*)[1]’ but argument is of type ‘struct __va_list_tag **’
$ /usr/bin/gcc -c vap.c
vap.c: In function ‘test_val’:
vap.c:13: warning: passing argument 2 of ‘test_ptr’ from incompatible pointer type
$ 

I get the same messages on MacOS X Lion with GCC/LLVM 4.2.1 and with GCC 4.6.1:

$ /usr/bin/gcc --version
i686-apple-darwin11-llvm-gcc-4.2 (GCC) 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2335.15.00)
Copyright (C) 2007 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ gcc --version
gcc (GCC) 4.6.1
Copyright (C) 2011 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$

Questions

  • Can someone articulate why the test_val() function cannot pass the va_list passed as an argument to test_ptr(), whereas the test() function (which created the va_list) can?

  • Is GCC correct to complain about the indirect passing of the pointer in test_val()?

On both cases, I can see an answer fuzzily, but I can't describe it succinctly. I think that the code in test_val() is abusing the va_list and it is good that the code won't compile - but I'd like to be sure before I go fixing it.


Update 2012-03-30

I went to deal with the problematic code this week. Before making changes, I went to find where the miscreant functions are used — and they aren't! So, I solved my compilation error problem by removing the functions (4 externally visible but unused ones, plus 2 static ones which contained the problematic code). That was much simpler than having to work out how to deal with the mess. (This also explains why there was never any evidence of a run-time problem caused by the code.)

Answer

Christoph picture Christoph · Nov 8, 2011

This is a known problem. On some architectures (in particular x86-64), va_list needs to be more complex than a simple pointer to the stack, for example because some arguments might be passed in registers or out-of-band in some other way (see this answer for the definition of va_list on x86-64).

On such architectures, it is common to make va_list an array type so that parameters of type va_list will be adjusted to pointer types, and instead of the whole structure, only a single pointer needs to be passed.

This should not violate the C standard, which only says that va_list must be a complete object type and even explicitly accounts for the fact that passing a va_list argument might not actually clone the necessary state: va_list objects have indeterminate value if they are passed as arguments and consumed in the called function.

But even if making va_list an array type is legal, it still leads to the problems you experienced: As parameters of type va_list have the 'wrong' type, eg struct __va_list_tag * instead of struct __va_list_tag [1], it will blow up in cases where the difference between arrays and pointers matter.

The real problem is not the type mismatch gcc warns about, but the by-pointer instead of by-value argument passing semantics: &args in test_val() points to the intermediate pointer variable instead of the va_list object; ignoring the warning means that you'll invoke va_arg() in test_ptr() on the pointer variable, which should return garbage (or segfault if you're lucky) and corrupt the stack.

One workaround is to wrap your va_list in a structure and pass that around instead. Another solution I've seen in the wild, even here on SO, is to use va_copy to create a local copy of the argument and then pass a pointer to that:

static void test_val(const char *fmt, va_list args)
{
    va_list args_copy;
    va_copy(args_copy, args);
    test_ptr(fmt, &args_copy);
    va_end(args_copy); 
}

This should work in practice, but technically it might or might not be undefined behaviour, depending on your interpretation of the standard:

If va_copy() is implemented as a macro, no parameter adjustments are performed, and it might matter that args is not of type va_list. However, as it is unspecified whether va_copy() is a macro or a function, one might argue that it at least could be a function and parameter adjustments are implicitly assumed in the prototype given for the macro. It might be a good idea to ask the officials for clarification or even file a defect report.

You could also use your build system to deal with the issue by defining a configuration flag like HAVE_VA_LIST_AS_ARRAY so you can do the right thing for your particular architecture:

#ifdef HAVE_VA_LIST_AS_ARRAY
#define MAKE_POINTER_FROM_VA_LIST_ARG(arg) ((va_list *)(arg))
#else
#define MAKE_POINTER_FROM_VA_LIST_ARG(arg) (&(arg))
#endif

static void test_val(const char *fmt, va_list args)
{
    test_ptr(fmt, MAKE_POINTER_FROM_VA_LIST_ARG(args));
}