Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you call vsnprintf() safely?

Tags:

c

printf

glibc

I'm porting some very old (> 10y) C code to modern Linuxes. I'm getting segmentation faults within a custom-written vsnprintf() wrapper (apparently its task is to detect duplicate output strings and intern them):

char* strVPrintf(const String fmt, va_list ap)
{
  /* Guess we need no more than 50 bytes. */
  int n, size = 50;
  char* p = (char*)memMalloc(size), q;

  while (1) {
    /* Try to print in the allocated space. */
    n = vsnprintf(p, size, fmt, ap);
    /* If that worked, return the string. */
    if (n > -1 && n < size) {
      break;
    }
    /* Else try again with more space. */
    if (n > -1)                /* glibc 2.1 */
      size = n + 1;            /* precisely what is needed */
    else                   /* glibc 2.0 */
      size *= 2;               /* twice the old size */
    p = memRealloc(p, size);
  }

  q =  strRegister(p);
  memFree(p);
  return q;
}

The author seems to have assumed that the standard vsnprintf() function returns the number of characters written, and simply returns a sentinel value if it doesn't receive enough space to format all args. This means that you can just guess a buffer size and increase it if necessary.

But on my system (Ubuntu 14.04, glibc 2.19) vnprintf causes a segmentation fault when called with too many arguments for the provided space. Did the semantics of the snprintf() family change that drastically in the meantime? And what is the modern way of ensuring you hand it enough buffer space?

like image 550
Kilian Foth Avatar asked Jun 13 '16 11:06

Kilian Foth


1 Answers

This is the correct way to use snprintf and vsnprintf on every operating system except SunOS 4 (which has been obsolete for 20 years), so your problem is somewhere else.

I'll make a pure guess and say that I'm almost certain that your problem is that you're passing the va_list ap into vsnprintf which consumes it and then you expect it to be reset on the next call. This is incorrect and has stopped working in gcc many years ago (because it only worked on certain architectures).

Change:

n = vsnprintf(p, size, fmt, ap);

To:

va_list apc;
va_copy(apc, ap);
n = vsnprintf(p, size, fmt, apc);
va_end(apc);

And see if that helps.

Here's a simple test to see what's going on:

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

void
foo(const char *fmt, va_list ap)
{
#ifdef BAD
    vprintf(fmt, ap);
#else
    va_list apc;
    va_copy(apc, ap);
    vprintf(fmt, apc);
    va_end(apc);
#endif
    vprintf(fmt, ap);
}

void
bar(const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    foo(fmt, ap);
    va_end(ap);
}

int
main(int argc, char **argv)
{
    bar("foo %s\n", "bar");
    return 0;
}

When run I get this:

$ cc -o foo foo.c && ./foo
foo bar
foo bar
$ cc -DBAD -o foo foo.c && ./foo
foo bar
foo ����
like image 73
Art Avatar answered Oct 06 '22 01:10

Art