Skip to content

Commit 7a39725

Browse files
committed
Fix out-of-bound memory access for interval -> char conversion
Using Roman numbers (via "RM" or "rm") for a conversion to calculate a number of months has never considered the case of negative numbers, where a conversion could easily cause out-of-bound memory accesses. The conversions in themselves were not completely consistent either, as specifying 12 would result in NULL, but it should mean XII. This commit reworks the conversion calculation to have a more consistent behavior: - If the number of months and years is 0, return NULL. - If the number of months is positive, return the exact month number. - If the number of months is negative, do a backward calculation, with -1 meaning December, -2 November, etc. Reported-by: Theodor Arsenij Larionov-Trichkin Author: Julien Rouhaud Discussion: https://postgr.es/m/[email protected] backpatch-through: 9.6
1 parent 6277435 commit 7a39725

File tree

3 files changed

+95
-10
lines changed

3 files changed

+95
-10
lines changed

src/backend/utils/adt/formatting.c

+53-10
Original file line numberDiff line numberDiff line change
@@ -3207,18 +3207,61 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col
32073207
s += strlen(s);
32083208
break;
32093209
case DCH_RM:
3210-
if (!tm->tm_mon)
3211-
break;
3212-
sprintf(s, "%*s", S_FM(n->suffix) ? 0 : -4,
3213-
rm_months_upper[MONTHS_PER_YEAR - tm->tm_mon]);
3214-
s += strlen(s);
3215-
break;
3210+
/* FALLTHROUGH */
32163211
case DCH_rm:
3217-
if (!tm->tm_mon)
3212+
3213+
/*
3214+
* For intervals, values like '12 month' will be reduced to 0
3215+
* month and some years. These should be processed.
3216+
*/
3217+
if (!tm->tm_mon && !tm->tm_year)
32183218
break;
3219-
sprintf(s, "%*s", S_FM(n->suffix) ? 0 : -4,
3220-
rm_months_lower[MONTHS_PER_YEAR - tm->tm_mon]);
3221-
s += strlen(s);
3219+
else
3220+
{
3221+
int mon = 0;
3222+
const char *const *months;
3223+
3224+
if (n->key->id == DCH_RM)
3225+
months = rm_months_upper;
3226+
else
3227+
months = rm_months_lower;
3228+
3229+
/*
3230+
* Compute the position in the roman-numeral array. Note
3231+
* that the contents of the array are reversed, December
3232+
* being first and January last.
3233+
*/
3234+
if (tm->tm_mon == 0)
3235+
{
3236+
/*
3237+
* This case is special, and tracks the case of full
3238+
* interval years.
3239+
*/
3240+
mon = tm->tm_year >= 0 ? 0 : MONTHS_PER_YEAR - 1;
3241+
}
3242+
else if (tm->tm_mon < 0)
3243+
{
3244+
/*
3245+
* Negative case. In this case, the calculation is
3246+
* reversed, where -1 means December, -2 November,
3247+
* etc.
3248+
*/
3249+
mon = -1 * (tm->tm_mon + 1);
3250+
}
3251+
else
3252+
{
3253+
/*
3254+
* Common case, with a strictly positive value. The
3255+
* position in the array matches with the value of
3256+
* tm_mon.
3257+
*/
3258+
mon = MONTHS_PER_YEAR - tm->tm_mon;
3259+
}
3260+
3261+
sprintf(s, "%*s", S_FM(n->suffix) ? 0 : -4,
3262+
months[mon]);
3263+
s += strlen(s);
3264+
}
32223265
break;
32233266
case DCH_W:
32243267
sprintf(s, "%d", (tm->tm_mday - 1) / 7 + 1);

src/test/regress/expected/timestamp.out

+36
Original file line numberDiff line numberDiff line change
@@ -1961,6 +1961,42 @@ SELECT to_char(d, 'FF1 FF2 FF3 FF4 FF5 FF6 ff1 ff2 ff3 ff4 ff5 ff6 MS US')
19611961
7 78 789 7890 78901 789012 7 78 789 7890 78901 789012 789 789012
19621962
(4 rows)
19631963

1964+
-- Roman months, with upper and lower case.
1965+
SELECT i,
1966+
to_char(i * interval '1mon', 'rm'),
1967+
to_char(i * interval '1mon', 'RM')
1968+
FROM generate_series(-13, 13) i;
1969+
i | to_char | to_char
1970+
-----+---------+---------
1971+
-13 | xii | XII
1972+
-12 | i | I
1973+
-11 | ii | II
1974+
-10 | iii | III
1975+
-9 | iv | IV
1976+
-8 | v | V
1977+
-7 | vi | VI
1978+
-6 | vii | VII
1979+
-5 | viii | VIII
1980+
-4 | ix | IX
1981+
-3 | x | X
1982+
-2 | xi | XI
1983+
-1 | xii | XII
1984+
0 | |
1985+
1 | i | I
1986+
2 | ii | II
1987+
3 | iii | III
1988+
4 | iv | IV
1989+
5 | v | V
1990+
6 | vi | VI
1991+
7 | vii | VII
1992+
8 | viii | VIII
1993+
9 | ix | IX
1994+
10 | x | X
1995+
11 | xi | XI
1996+
12 | xii | XII
1997+
13 | i | I
1998+
(27 rows)
1999+
19642000
-- timestamp numeric fields constructor
19652001
SELECT make_timestamp(2014, 12, 28, 6, 30, 45.887);
19662002
make_timestamp

src/test/regress/sql/timestamp.sql

+6
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,12 @@ SELECT to_char(d, 'FF1 FF2 FF3 FF4 FF5 FF6 ff1 ff2 ff3 ff4 ff5 ff6 MS US')
353353
('2018-11-02 12:34:56.78901234')
354354
) d(d);
355355

356+
-- Roman months, with upper and lower case.
357+
SELECT i,
358+
to_char(i * interval '1mon', 'rm'),
359+
to_char(i * interval '1mon', 'RM')
360+
FROM generate_series(-13, 13) i;
361+
356362
-- timestamp numeric fields constructor
357363
SELECT make_timestamp(2014, 12, 28, 6, 30, 45.887);
358364
SELECT make_timestamp(-44, 3, 15, 12, 30, 15);

0 commit comments

Comments
 (0)