Skip to content

Commit cd5dfea

Browse files
committed
Fix PGLC_localeconv() to handle errors better.
The code was intentionally not very careful about leaking strdup'd strings in case of an error. That was forgivable probably, but it also failed to notice strdup() failures, which could lead to subsequent null-pointer-dereference crashes, since many callers unsurprisingly didn't check for null pointers in the struct lconv fields. An even worse problem is that it could throw error while we were setlocale'd to a non-C locale, causing unwanted behavior in subsequent libc calls. Rewrite to ensure that we cannot throw elog(ERROR) until after we've restored the previous locale settings, or at least attempted to. (I'm sorely tempted to make restore failure be a FATAL error, but will refrain for the moment.) Having done that, it's not much more work to ensure that we clean up strdup'd storage on the way out, too. This code is substantially the same in all supported branches, so back-patch all the way. Michael Paquier and Tom Lane Discussion: <CAB7nPqRMbGqa_mesopcn4MPyTs34eqtVEK7ELYxvvV=oqS00YA@mail.gmail.com>
1 parent 71db302 commit cd5dfea

File tree

1 file changed

+164
-52
lines changed

1 file changed

+164
-52
lines changed

src/backend/utils/adt/pg_locale.c

Lines changed: 164 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -358,53 +358,93 @@ assign_locale_messages(const char *newval, void *extra)
358358

359359
/*
360360
* Frees the malloced content of a struct lconv. (But not the struct
361-
* itself.)
361+
* itself.) It's important that this not throw elog(ERROR).
362362
*/
363363
static void
364364
free_struct_lconv(struct lconv * s)
365365
{
366-
if (s->currency_symbol)
367-
free(s->currency_symbol);
368366
if (s->decimal_point)
369367
free(s->decimal_point);
370-
if (s->grouping)
371-
free(s->grouping);
372368
if (s->thousands_sep)
373369
free(s->thousands_sep);
370+
if (s->grouping)
371+
free(s->grouping);
374372
if (s->int_curr_symbol)
375373
free(s->int_curr_symbol);
374+
if (s->currency_symbol)
375+
free(s->currency_symbol);
376376
if (s->mon_decimal_point)
377377
free(s->mon_decimal_point);
378-
if (s->mon_grouping)
379-
free(s->mon_grouping);
380378
if (s->mon_thousands_sep)
381379
free(s->mon_thousands_sep);
382-
if (s->negative_sign)
383-
free(s->negative_sign);
380+
if (s->mon_grouping)
381+
free(s->mon_grouping);
384382
if (s->positive_sign)
385383
free(s->positive_sign);
384+
if (s->negative_sign)
385+
free(s->negative_sign);
386+
}
387+
388+
/*
389+
* Check that all fields of a struct lconv (or at least, the ones we care
390+
* about) are non-NULL. The field list must match free_struct_lconv().
391+
*/
392+
static bool
393+
struct_lconv_is_valid(struct lconv * s)
394+
{
395+
if (s->decimal_point == NULL)
396+
return false;
397+
if (s->thousands_sep == NULL)
398+
return false;
399+
if (s->grouping == NULL)
400+
return false;
401+
if (s->int_curr_symbol == NULL)
402+
return false;
403+
if (s->currency_symbol == NULL)
404+
return false;
405+
if (s->mon_decimal_point == NULL)
406+
return false;
407+
if (s->mon_thousands_sep == NULL)
408+
return false;
409+
if (s->mon_grouping == NULL)
410+
return false;
411+
if (s->positive_sign == NULL)
412+
return false;
413+
if (s->negative_sign == NULL)
414+
return false;
415+
return true;
386416
}
387417

388418

389419
/*
390-
* Return a strdup'ed string converted from the specified encoding to the
420+
* Convert the strdup'd string at *str from the specified encoding to the
391421
* database encoding.
392422
*/
393-
static char *
394-
db_encoding_strdup(int encoding, const char *str)
423+
static void
424+
db_encoding_convert(int encoding, char **str)
395425
{
396426
char *pstr;
397427
char *mstr;
398428

399429
/* convert the string to the database encoding */
400430
pstr = (char *) pg_do_encoding_conversion(
401-
(unsigned char *) str, strlen(str),
431+
(unsigned char *) *str, strlen(*str),
402432
encoding, GetDatabaseEncoding());
433+
if (pstr == *str)
434+
return; /* no conversion happened */
435+
436+
/* need it malloc'd not palloc'd */
403437
mstr = strdup(pstr);
404-
if (pstr != str)
405-
pfree(pstr);
438+
if (mstr == NULL)
439+
ereport(ERROR,
440+
(errcode(ERRCODE_OUT_OF_MEMORY),
441+
errmsg("out of memory")));
406442

407-
return mstr;
443+
/* replace old string */
444+
free(*str);
445+
*str = mstr;
446+
447+
pfree(pstr);
408448
}
409449

410450

@@ -418,13 +458,10 @@ PGLC_localeconv(void)
418458
static struct lconv CurrentLocaleConv;
419459
static bool CurrentLocaleConvAllocated = false;
420460
struct lconv *extlconv;
461+
struct lconv worklconv;
462+
bool trouble = false;
421463
char *save_lc_monetary;
422464
char *save_lc_numeric;
423-
char *decimal_point;
424-
char *grouping;
425-
char *thousands_sep;
426-
int encoding;
427-
428465
#ifdef WIN32
429466
char *save_lc_ctype;
430467
#endif
@@ -440,6 +477,20 @@ PGLC_localeconv(void)
440477
CurrentLocaleConvAllocated = false;
441478
}
442479

480+
/*
481+
* This is tricky because we really don't want to risk throwing error
482+
* while the locale is set to other than our usual settings. Therefore,
483+
* the process is: collect the usual settings, set locale to special
484+
* setting, copy relevant data into worklconv using strdup(), restore
485+
* normal settings, convert data to desired encoding, and finally stash
486+
* the collected data in CurrentLocaleConv. This makes it safe if we
487+
* throw an error during encoding conversion or run out of memory anywhere
488+
* in the process. All data pointed to by struct lconv members is
489+
* allocated with strdup, to avoid premature elog(ERROR) and to allow
490+
* using a single cleanup routine.
491+
*/
492+
memset(&worklconv, 0, sizeof(worklconv));
493+
443494
/* Save user's values of monetary and numeric locales */
444495
save_lc_monetary = setlocale(LC_MONETARY, NULL);
445496
if (save_lc_monetary)
@@ -455,7 +506,7 @@ PGLC_localeconv(void)
455506
* Ideally, monetary and numeric local symbols could be returned in any
456507
* server encoding. Unfortunately, the WIN32 API does not allow
457508
* setlocale() to return values in a codepage/CTYPE that uses more than
458-
* two bytes per character, like UTF-8:
509+
* two bytes per character, such as UTF-8:
459510
*
460511
* http://msdn.microsoft.com/en-us/library/x99tb11d.aspx
461512
*
@@ -465,7 +516,7 @@ PGLC_localeconv(void)
465516
*
466517
* Therefore, we set LC_CTYPE to match LC_NUMERIC or LC_MONETARY (which
467518
* cannot be UTF8), call localeconv(), and then convert from the
468-
* numeric/monitary LC_CTYPE to the server encoding. One example use of
519+
* numeric/monetary LC_CTYPE to the server encoding. One example use of
469520
* this is for the Euro symbol.
470521
*
471522
* Perhaps someday we will use GetLocaleInfoW() which returns values in
@@ -477,18 +528,20 @@ PGLC_localeconv(void)
477528
if (save_lc_ctype)
478529
save_lc_ctype = pstrdup(save_lc_ctype);
479530

531+
/* Here begins the critical section where we must not throw error */
532+
480533
/* use numeric to set the ctype */
481534
setlocale(LC_CTYPE, locale_numeric);
482535
#endif
483536

484537
/* Get formatting information for numeric */
485538
setlocale(LC_NUMERIC, locale_numeric);
486539
extlconv = localeconv();
487-
encoding = pg_get_encoding_from_locale(locale_numeric, true);
488540

489-
decimal_point = db_encoding_strdup(encoding, extlconv->decimal_point);
490-
thousands_sep = db_encoding_strdup(encoding, extlconv->thousands_sep);
491-
grouping = strdup(extlconv->grouping);
541+
/* Must copy data now in case setlocale() overwrites it */
542+
worklconv.decimal_point = strdup(extlconv->decimal_point);
543+
worklconv.thousands_sep = strdup(extlconv->thousands_sep);
544+
worklconv.grouping = strdup(extlconv->grouping);
492545

493546
#ifdef WIN32
494547
/* use monetary to set the ctype */
@@ -498,52 +551,111 @@ PGLC_localeconv(void)
498551
/* Get formatting information for monetary */
499552
setlocale(LC_MONETARY, locale_monetary);
500553
extlconv = localeconv();
501-
encoding = pg_get_encoding_from_locale(locale_monetary, true);
502554

503-
/*
504-
* Must copy all values since restoring internal settings may overwrite
505-
* localeconv()'s results. Note that if we were to fail within this
506-
* sequence before reaching "CurrentLocaleConvAllocated = true", we could
507-
* leak some memory --- but not much, so it's not worth agonizing over.
508-
*/
509-
CurrentLocaleConv = *extlconv;
510-
CurrentLocaleConv.decimal_point = decimal_point;
511-
CurrentLocaleConv.grouping = grouping;
512-
CurrentLocaleConv.thousands_sep = thousands_sep;
513-
CurrentLocaleConv.int_curr_symbol = db_encoding_strdup(encoding, extlconv->int_curr_symbol);
514-
CurrentLocaleConv.currency_symbol = db_encoding_strdup(encoding, extlconv->currency_symbol);
515-
CurrentLocaleConv.mon_decimal_point = db_encoding_strdup(encoding, extlconv->mon_decimal_point);
516-
CurrentLocaleConv.mon_grouping = strdup(extlconv->mon_grouping);
517-
CurrentLocaleConv.mon_thousands_sep = db_encoding_strdup(encoding, extlconv->mon_thousands_sep);
518-
CurrentLocaleConv.negative_sign = db_encoding_strdup(encoding, extlconv->negative_sign);
519-
CurrentLocaleConv.positive_sign = db_encoding_strdup(encoding, extlconv->positive_sign);
520-
CurrentLocaleConvAllocated = true;
555+
/* Must copy data now in case setlocale() overwrites it */
556+
worklconv.int_curr_symbol = strdup(extlconv->int_curr_symbol);
557+
worklconv.currency_symbol = strdup(extlconv->currency_symbol);
558+
worklconv.mon_decimal_point = strdup(extlconv->mon_decimal_point);
559+
worklconv.mon_thousands_sep = strdup(extlconv->mon_thousands_sep);
560+
worklconv.mon_grouping = strdup(extlconv->mon_grouping);
561+
worklconv.positive_sign = strdup(extlconv->positive_sign);
562+
worklconv.negative_sign = strdup(extlconv->negative_sign);
563+
/* Copy scalar fields as well */
564+
worklconv.int_frac_digits = extlconv->int_frac_digits;
565+
worklconv.frac_digits = extlconv->frac_digits;
566+
worklconv.p_cs_precedes = extlconv->p_cs_precedes;
567+
worklconv.p_sep_by_space = extlconv->p_sep_by_space;
568+
worklconv.n_cs_precedes = extlconv->n_cs_precedes;
569+
worklconv.n_sep_by_space = extlconv->n_sep_by_space;
570+
worklconv.p_sign_posn = extlconv->p_sign_posn;
571+
worklconv.n_sign_posn = extlconv->n_sign_posn;
521572

522573
/* Try to restore internal settings */
523574
if (save_lc_monetary)
524575
{
525576
if (!setlocale(LC_MONETARY, save_lc_monetary))
526-
elog(WARNING, "failed to restore old locale");
527-
pfree(save_lc_monetary);
577+
trouble = true;
528578
}
529579

530580
if (save_lc_numeric)
531581
{
532582
if (!setlocale(LC_NUMERIC, save_lc_numeric))
533-
elog(WARNING, "failed to restore old locale");
534-
pfree(save_lc_numeric);
583+
trouble = true;
535584
}
536585

537586
#ifdef WIN32
538587
/* Try to restore internal ctype settings */
539588
if (save_lc_ctype)
540589
{
541590
if (!setlocale(LC_CTYPE, save_lc_ctype))
542-
elog(WARNING, "failed to restore old locale");
543-
pfree(save_lc_ctype);
591+
trouble = true;
544592
}
545593
#endif
546594

595+
/*
596+
* At this point we've done our best to clean up, and can call functions
597+
* that might possibly throw errors with a clean conscience. But let's
598+
* make sure we don't leak any already-strdup'd fields in worklconv.
599+
*/
600+
PG_TRY();
601+
{
602+
int encoding;
603+
604+
/*
605+
* Report it if we failed to restore anything. Perhaps this should be
606+
* FATAL, rather than continuing with bad locale settings?
607+
*/
608+
if (trouble)
609+
elog(WARNING, "failed to restore old locale");
610+
611+
/* Release the pstrdup'd locale names */
612+
if (save_lc_monetary)
613+
pfree(save_lc_monetary);
614+
if (save_lc_numeric)
615+
pfree(save_lc_numeric);
616+
#ifdef WIN32
617+
if (save_lc_ctype)
618+
pfree(save_lc_ctype);
619+
#endif
620+
621+
/* If any of the preceding strdup calls failed, complain now. */
622+
if (!struct_lconv_is_valid(&worklconv))
623+
ereport(ERROR,
624+
(errcode(ERRCODE_OUT_OF_MEMORY),
625+
errmsg("out of memory")));
626+
627+
/*
628+
* Now we must perform encoding conversion from whatever's associated
629+
* with the locale into the database encoding.
630+
*/
631+
encoding = pg_get_encoding_from_locale(locale_numeric, true);
632+
633+
db_encoding_convert(encoding, &worklconv.decimal_point);
634+
db_encoding_convert(encoding, &worklconv.thousands_sep);
635+
/* grouping is not text and does not require conversion */
636+
637+
encoding = pg_get_encoding_from_locale(locale_monetary, true);
638+
639+
db_encoding_convert(encoding, &worklconv.int_curr_symbol);
640+
db_encoding_convert(encoding, &worklconv.currency_symbol);
641+
db_encoding_convert(encoding, &worklconv.mon_decimal_point);
642+
db_encoding_convert(encoding, &worklconv.mon_thousands_sep);
643+
/* mon_grouping is not text and does not require conversion */
644+
db_encoding_convert(encoding, &worklconv.positive_sign);
645+
db_encoding_convert(encoding, &worklconv.negative_sign);
646+
}
647+
PG_CATCH();
648+
{
649+
free_struct_lconv(&worklconv);
650+
PG_RE_THROW();
651+
}
652+
PG_END_TRY();
653+
654+
/*
655+
* Everything is good, so save the results.
656+
*/
657+
CurrentLocaleConv = worklconv;
658+
CurrentLocaleConvAllocated = true;
547659
CurrentLocaleConvValid = true;
548660
return &CurrentLocaleConv;
549661
}

0 commit comments

Comments
 (0)