Skip to content

Commit 6540322

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/16953-f255a18f8c51f1d5@postgresql.org backpatch-through: 9.6
1 parent c777a1f commit 6540322

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
@@ -2896,18 +2896,61 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col
28962896
s += strlen(s);
28972897
break;
28982898
case DCH_RM:
2899-
if (!tm->tm_mon)
2900-
break;
2901-
sprintf(s, "%*s", S_FM(n->suffix) ? 0 : -4,
2902-
rm_months_upper[MONTHS_PER_YEAR - tm->tm_mon]);
2903-
s += strlen(s);
2904-
break;
2899+
/* FALLTHROUGH */
29052900
case DCH_rm:
2906-
if (!tm->tm_mon)
2901+
2902+
/*
2903+
* For intervals, values like '12 month' will be reduced to 0
2904+
* month and some years. These should be processed.
2905+
*/
2906+
if (!tm->tm_mon && !tm->tm_year)
29072907
break;
2908-
sprintf(s, "%*s", S_FM(n->suffix) ? 0 : -4,
2909-
rm_months_lower[MONTHS_PER_YEAR - tm->tm_mon]);
2910-
s += strlen(s);
2908+
else
2909+
{
2910+
int mon = 0;
2911+
const char *const *months;
2912+
2913+
if (n->key->id == DCH_RM)
2914+
months = rm_months_upper;
2915+
else
2916+
months = rm_months_lower;
2917+
2918+
/*
2919+
* Compute the position in the roman-numeral array. Note
2920+
* that the contents of the array are reversed, December
2921+
* being first and January last.
2922+
*/
2923+
if (tm->tm_mon == 0)
2924+
{
2925+
/*
2926+
* This case is special, and tracks the case of full
2927+
* interval years.
2928+
*/
2929+
mon = tm->tm_year >= 0 ? 0 : MONTHS_PER_YEAR - 1;
2930+
}
2931+
else if (tm->tm_mon < 0)
2932+
{
2933+
/*
2934+
* Negative case. In this case, the calculation is
2935+
* reversed, where -1 means December, -2 November,
2936+
* etc.
2937+
*/
2938+
mon = -1 * (tm->tm_mon + 1);
2939+
}
2940+
else
2941+
{
2942+
/*
2943+
* Common case, with a strictly positive value. The
2944+
* position in the array matches with the value of
2945+
* tm_mon.
2946+
*/
2947+
mon = MONTHS_PER_YEAR - tm->tm_mon;
2948+
}
2949+
2950+
sprintf(s, "%*s", S_FM(n->suffix) ? 0 : -4,
2951+
months[mon]);
2952+
s += strlen(s);
2953+
}
29112954
break;
29122955
case DCH_W:
29132956
sprintf(s, "%d", (tm->tm_mday - 1) / 7 + 1);

src/test/regress/expected/timestamp.out

+36
Original file line numberDiff line numberDiff line change
@@ -1701,6 +1701,42 @@ SELECT '' AS to_char_11, to_char(d1, 'FMIYYY FMIYY FMIY FMI FMIW FMIDDD FMID')
17011701
| 2001 1 1 1 1 1 1
17021702
(65 rows)
17031703

1704+
-- Roman months, with upper and lower case.
1705+
SELECT i,
1706+
to_char(i * interval '1mon', 'rm'),
1707+
to_char(i * interval '1mon', 'RM')
1708+
FROM generate_series(-13, 13) i;
1709+
i | to_char | to_char
1710+
-----+---------+---------
1711+
-13 | xii | XII
1712+
-12 | i | I
1713+
-11 | ii | II
1714+
-10 | iii | III
1715+
-9 | iv | IV
1716+
-8 | v | V
1717+
-7 | vi | VI
1718+
-6 | vii | VII
1719+
-5 | viii | VIII
1720+
-4 | ix | IX
1721+
-3 | x | X
1722+
-2 | xi | XI
1723+
-1 | xii | XII
1724+
0 | |
1725+
1 | i | I
1726+
2 | ii | II
1727+
3 | iii | III
1728+
4 | iv | IV
1729+
5 | v | V
1730+
6 | vi | VI
1731+
7 | vii | VII
1732+
8 | viii | VIII
1733+
9 | ix | IX
1734+
10 | x | X
1735+
11 | xi | XI
1736+
12 | xii | XII
1737+
13 | i | I
1738+
(27 rows)
1739+
17041740
-- timestamp numeric fields constructor
17051741
SELECT make_timestamp(2014,12,28,6,30,45.887);
17061742
make_timestamp

src/test/regress/sql/timestamp.sql

+6
Original file line numberDiff line numberDiff line change
@@ -235,5 +235,11 @@ SELECT '' AS to_char_10, to_char(d1, 'IYYY IYY IY I IW IDDD ID')
235235
SELECT '' AS to_char_11, to_char(d1, 'FMIYYY FMIYY FMIY FMI FMIW FMIDDD FMID')
236236
FROM TIMESTAMP_TBL;
237237

238+
-- Roman months, with upper and lower case.
239+
SELECT i,
240+
to_char(i * interval '1mon', 'rm'),
241+
to_char(i * interval '1mon', 'RM')
242+
FROM generate_series(-13, 13) i;
243+
238244
-- timestamp numeric fields constructor
239245
SELECT make_timestamp(2014,12,28,6,30,45.887);

0 commit comments

Comments
 (0)