Re: better snprintf replacement, anyone?

Casper Dik (casper@HOLLAND.SUN.COM)
Wed, 23 Jul 1997 15:28:57 +0200

>A while back I threw together these short routines, which have worked quite
>well in practice. The return value conventions probably don't correspond to
>the BSD snprintf(), because I didn't have any documentation available.
>The principal virtue is that they produce results identical to the native
>printf() implementation. They were not written with security in mind, though.
>A poor implementation of tmpfile() could open up numerous security holes.

Typical system V implementations (as well as older BSD implementations) have
an unsafe tmpfile(). This is true for Solaris (before 2.6 when we fixed it)
and in IRIX (last time I looked). Perhaps we can check and compile a list?

> If the string exceeds the buffer length, errno is set to ENOMEM
> and the negative string length is returned. The first len-1
> characters are placed in the buffer.
> (Add one for the terminating '\0' when allocating a buffer!)

This is also a problem as many implementations expect snprintf() to
always return a positive count; either bytes required (possible > n)

That's one of the more difficult things of implementing a new function;
what willbe teh exact semantics? (the 2.5/2.5.1 __* function has slightly
different semantcis than the public 2.6 *snprintf())

Here's one I did and it also uses temporary files, but it uses
"mkstemp()" which should be save.

BTW, mkstemp() always uses open(,, 0600) so you get private tmpfiles;
tmpfile() is required to use the umask() in setting the mode. Depending
on how this is implemented, there's a window in which a tmpfile() can be opened
by other processes.

A naive reimplementation of tmpfile() using mkstemp() fails the standard
conformance tests. (s = mkstemp(template); unlink(template); return
fdopen(s,"w+)). Solaris 2.6 does use mkstemp() but does an fchmod() after
unlink()ing the file so there's no race condition either.
Standard are stupid sometimes.

/*
* snprintf() quicky using temporary files.
* Not stress tested, but it seems to work.
*
* Returns the number of bytes that would have been output by printf.
* Does not check whether a negative value if passed in for n (as it's unsigned);
* some implementations cast n to int and than compare with 0.
*
* Casper Dik (Casper.Dik@Holland.Sun.COM)
*/

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

int snprintf(char * buf, size_t n, const char *fmt, ...)
{
va_list ap;
int ret;

va_start(ap, fmt);
ret = vsnprintf(buf, n, fmt, ap);
va_end(ap);
return ret;
}

static char template[] = "/tmp/snprintfXXXXXX";

int vsnprintf(char *buf, size_t n, const char *fmt, va_list ap)
{
char templ[sizeof(template)];
int s;
int ret, cnt = 0, nread;
FILE *fp;

strcpy(templ,template);

s = mkstemp(templ);

if (s < 0)
return -1;

(void) unlink(templ);

fp = fdopen(s, "w+");

if (fp == NULL) {
close(s);
return -1;
}

ret = vfprintf(fp, fmt, ap);

if (ret < 0 || fseek(fp, 0, SEEK_SET) < 0) {
fclose(fp);
return -1;
}

while (cnt < n && (nread = fread(buf + cnt, 1, n - cnt, fp)) > 0)
cnt += nread;

buf[cnt-1] = '\0'; /* cnt is atmost n */

fclose(fp);
return ret;
}