Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.)

like image 907
Jonathan Leffler Avatar asked Nov 08 '11 07:11

Jonathan Leffler


People also ask

What does va_list mean?

va_list is a complete object type suitable for holding the information needed by the macros va_start, va_copy, va_arg, and va_end. If a va_list instance is created, passed to another function, and used via va_arg in that function, then any subsequent use in the calling function should be preceded by a call to va_end.

Can va_list be null?

NULL in general case is not a valid initializer for a va_list object. So, the answer to your question is: it is not possible.


2 Answers

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)); } 
like image 198
Christoph Avatar answered Sep 18 '22 19:09

Christoph


The problem is not specific to va_list. The following code results in a similar warning:

typedef char *test[1];  void x(test *a) { }  void y(test o) {     x(&o); } 

The problem stems from the way C handles function variables that are also arrays, probably due to the fact that arrays are passed as reference and not by value. The type of o is not the same as the type of a local variable of type test, in this case: char *** instead of char *(*)[1].

Returning to the original issue at hand, the easy way to work around it is to use a container struct:

struct va_list_wrapper {     va_list v; }; 

and there would be no typing problems passing a point to it.

like image 33
Dan Aloni Avatar answered Sep 22 '22 19:09

Dan Aloni