Skip to content

Commit a57a6fa

Browse files
committed
Make snprintf.c follow the C99 standard for snprintf's result value.
C99 says that the result should be the number of bytes that would have been emitted given a large enough buffer, not the number we actually were able to put in the buffer. It's time to make our substitute implementation comply with that. Not doing so results in inefficiency in buffer-enlargement cases, and also poses a portability hazard for third-party code that might expect C99-compliant snprintf behavior within Postgres. In passing, remove useless tests for str == NULL; neither C99 nor predecessor standards ever allowed that except when count == 0, so I see no reason to expend cycles on making that a non-crash case for this implementation. Also, don't waste a byte in pg_vfprintf's local I/O buffer; this might have performance benefits by allowing aligned writes during flushbuffer calls. Back-patch of commit 805889d. There was some concern about this possibly breaking code that assumes pre-C99 behavior, but there is much more risk (and reality, in our own code) of code that assumes C99 behavior and hence fails to detect buffer overrun without this. Discussion: https://postgr.es/m/17245.1534289329@sss.pgh.pa.us
1 parent 3531365 commit a57a6fa

File tree

1 file changed

+63
-31
lines changed

1 file changed

+63
-31
lines changed

src/port/snprintf.c

Lines changed: 63 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Copyright (c) 1983, 1995, 1996 Eric P. Allman
33
* Copyright (c) 1988, 1993
44
* The Regents of the University of California. All rights reserved.
5+
* Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
56
*
67
* Redistribution and use in source and binary forms, with or without
78
* modification, are permitted provided that the following conditions
@@ -52,8 +53,8 @@
5253
* SNPRINTF, VSNPRINTF and friends
5354
*
5455
* These versions have been grabbed off the net. They have been
55-
* cleaned up to compile properly and support for most of the Single Unix
56-
* Specification has been added. Remaining unimplemented features are:
56+
* cleaned up to compile properly and support for most of the C99
57+
* specification has been added. Remaining unimplemented features are:
5758
*
5859
* 1. No locale support: the radix character is always '.' and the '
5960
* (single quote) format flag is ignored.
@@ -67,25 +68,24 @@
6768
* 5. Space and '#' flags are not implemented.
6869
*
6970
*
70-
* The result values of these functions are not the same across different
71-
* platforms. This implementation is compatible with the Single Unix Spec:
71+
* Historically the result values of sprintf/snprintf varied across platforms.
72+
* This implementation now follows the C99 standard:
7273
*
73-
* 1. -1 is returned only if processing is abandoned due to an invalid
74-
* parameter, such as incorrect format string. (Although not required by
75-
* the spec, this happens only when no characters have yet been transmitted
76-
* to the destination.)
74+
* 1. -1 is returned if an error is detected in the format string, or if
75+
* a write to the target stream fails (as reported by fwrite). Note that
76+
* overrunning snprintf's target buffer is *not* an error.
7777
*
78-
* 2. For snprintf and sprintf, 0 is returned if str == NULL or count == 0;
79-
* no data has been stored.
78+
* 2. For successful writes to streams, the actual number of bytes written
79+
* to the stream is returned.
8080
*
81-
* 3. Otherwise, the number of bytes actually transmitted to the destination
82-
* is returned (excluding the trailing '\0' for snprintf and sprintf).
81+
* 3. For successful sprintf/snprintf, the number of bytes that would have
82+
* been written to an infinite-size buffer (excluding the trailing '\0')
83+
* is returned. snprintf will truncate its output to fit in the buffer
84+
* (ensuring a trailing '\0' unless count == 0), but this is not reflected
85+
* in the function result.
8386
*
84-
* For snprintf with nonzero count, the result cannot be more than count-1
85-
* (a trailing '\0' is always stored); it is not possible to distinguish
86-
* buffer overrun from exact fit. This is unlike some implementations that
87-
* return the number of bytes that would have been needed for the complete
88-
* result string.
87+
* snprintf buffer overrun can be detected by checking for function result
88+
* greater than or equal to the supplied count.
8989
*/
9090

9191
/**************************************************************
@@ -104,15 +104,27 @@
104104
#undef fprintf
105105
#undef printf
106106

107-
/* Info about where the formatted output is going */
107+
/*
108+
* Info about where the formatted output is going.
109+
*
110+
* dopr and subroutines will not write at/past bufend, but snprintf
111+
* reserves one byte, ensuring it may place the trailing '\0' there.
112+
*
113+
* In snprintf, we use nchars to count the number of bytes dropped on the
114+
* floor due to buffer overrun. The correct result of snprintf is thus
115+
* (bufptr - bufstart) + nchars. (This isn't as inconsistent as it might
116+
* seem: nchars is the number of emitted bytes that are not in the buffer now,
117+
* either because we sent them to the stream or because we couldn't fit them
118+
* into the buffer to begin with.)
119+
*/
108120
typedef struct
109121
{
110122
char *bufptr; /* next buffer output position */
111123
char *bufstart; /* first buffer element */
112-
char *bufend; /* last buffer element, or NULL */
124+
char *bufend; /* last+1 buffer element, or NULL */
113125
/* bufend == NULL is for sprintf, where we assume buf is big enough */
114126
FILE *stream; /* eventual output destination, or NULL */
115-
int nchars; /* # chars already sent to stream */
127+
int nchars; /* # chars sent to stream, or dropped */
116128
bool failed; /* call is a failure; errno is set */
117129
} PrintfTarget;
118130

@@ -150,17 +162,28 @@ int
150162
pg_vsnprintf(char *str, size_t count, const char *fmt, va_list args)
151163
{
152164
PrintfTarget target;
165+
char onebyte[1];
153166

154-
if (str == NULL || count == 0)
155-
return 0;
167+
/*
168+
* C99 allows the case str == NULL when count == 0. Rather than
169+
* special-casing this situation further down, we substitute a one-byte
170+
* local buffer. Callers cannot tell, since the function result doesn't
171+
* depend on count.
172+
*/
173+
if (count == 0)
174+
{
175+
str = onebyte;
176+
count = 1;
177+
}
156178
target.bufstart = target.bufptr = str;
157179
target.bufend = str + count - 1;
158180
target.stream = NULL;
159-
/* target.nchars is unused in this case */
181+
target.nchars = 0;
160182
target.failed = false;
161183
dopr(&target, fmt, args);
162184
*(target.bufptr) = '\0';
163-
return target.failed ? -1 : (target.bufptr - target.bufstart);
185+
return target.failed ? -1 : (target.bufptr - target.bufstart
186+
+ target.nchars);
164187
}
165188

166189
int
@@ -180,16 +203,15 @@ pg_vsprintf(char *str, const char *fmt, va_list args)
180203
{
181204
PrintfTarget target;
182205

183-
if (str == NULL)
184-
return 0;
185206
target.bufstart = target.bufptr = str;
186207
target.bufend = NULL;
187208
target.stream = NULL;
188-
/* target.nchars is unused in this case */
209+
target.nchars = 0; /* not really used in this case */
189210
target.failed = false;
190211
dopr(&target, fmt, args);
191212
*(target.bufptr) = '\0';
192-
return target.failed ? -1 : (target.bufptr - target.bufstart);
213+
return target.failed ? -1 : (target.bufptr - target.bufstart
214+
+ target.nchars);
193215
}
194216

195217
int
@@ -216,7 +238,7 @@ pg_vfprintf(FILE *stream, const char *fmt, va_list args)
216238
return -1;
217239
}
218240
target.bufstart = target.bufptr = buffer;
219-
target.bufend = buffer + sizeof(buffer) - 1;
241+
target.bufend = buffer + sizeof(buffer); /* use the whole buffer */
220242
target.stream = stream;
221243
target.nchars = 0;
222244
target.failed = false;
@@ -259,6 +281,10 @@ flushbuffer(PrintfTarget *target)
259281
{
260282
size_t nc = target->bufptr - target->bufstart;
261283

284+
/*
285+
* Don't write anything if we already failed; this is to ensure we
286+
* preserve the original failure's errno.
287+
*/
262288
if (!target->failed && nc > 0)
263289
{
264290
size_t written;
@@ -1020,7 +1046,10 @@ dostr(const char *str, int slen, PrintfTarget *target)
10201046
{
10211047
/* buffer full, can we dump to stream? */
10221048
if (target->stream == NULL)
1023-
return; /* no, lose the data */
1049+
{
1050+
target->nchars += slen; /* no, lose the data */
1051+
return;
1052+
}
10241053
flushbuffer(target);
10251054
continue;
10261055
}
@@ -1039,7 +1068,10 @@ dopr_outch(int c, PrintfTarget *target)
10391068
{
10401069
/* buffer full, can we dump to stream? */
10411070
if (target->stream == NULL)
1042-
return; /* no, lose the data */
1071+
{
1072+
target->nchars++; /* no, lose the data */
1073+
return;
1074+
}
10431075
flushbuffer(target);
10441076
}
10451077
*(target->bufptr++) = c;

0 commit comments

Comments
 (0)