Fix memory leak in json_tokener_parse_ex due to duplocale

json-c uses duplocale+newlocale+uselocale() to temporarily switch LC_NUMERIC
to "C" during JSON number parsing.

On AIX, every call to json_tokener_parse_ex permanently leaks memory.
This was confirmed with dbx malloc allocation.

0x20008af8           24    0     YORKTOWN
0x20008e18         5128    0     YORKTOWN
0x2000a458          776    0     YORKTOWN
0x2000a768          776    0     YORKTOWN
0x2000aa78          776    0     YORKTOWN
0x2000ad88          776    0     YORKTOWN
0x2000b098          776    0     YORKTOWN
0x2000b6b8          776    0     YORKTOWN

dbx malloc allocation showing new allocations surviving after each parse call with ~776
bytes left after invocations.

The root cause is that on AIX, locale category objects allocated by duplocale() are not
released back to the process heap, even when freelocale() is called correctly.
This causes unbounded heap growth in any long-running process that parses JSON repeatedly.

The fix it to replace the per-call duplocale+newlocale+reelocale() with a single
file-scope static locale_t created once on first call using
newlocale(LC_NUMERIC_MASK, "C", (locale_t)0) and reused for the lifetime of the library.
This reduces newlocale calls on all platforms.

__attribute__((destructor)) function json_tokener_locale_fini() frees the static locale
when the library is unloaded via dlclose() or process exit.
This commit is contained in:
Sangamesh Mallayya committed 2026-10-08 01:37:47 -05:00
1 parent 9e99c1cfe7
commit fd92d58f2d
1 file changed
+34 -27
+34 -27
View File
@@ -314,6 +314,15 @@ struct json_object *json_tokener_parse_verbose(const char *str, enum json_tokene
/* End optimization macro defs */ /* End optimization macro defs */
#ifdef HAVE_USELOCALE
/* One-time C-numeric locale, created on first call and reused forever.
* Avoids per-call duplocale() overhead and the memory leak seen on AIX
* where freelocale() does not release all locale category memory.
* Freed on library unload by json_tokener_locale_fini().
*/
static locale_t c_numeric_locale = (locale_t)0;
#endif /* HAVE_USELOCALE */
struct json_object *json_tokener_parse_ex(struct json_tokener *tok, const char *str, int len) struct json_object *json_tokener_parse_ex(struct json_tokener *tok, const char *str, int len)
{ {
struct json_object *obj = NULL; struct json_object *obj = NULL;
@@ -323,7 +332,6 @@ struct json_object *json_tokener_parse_ex(struct json_tokener *tok, const char *
#ifdef HAVE_USELOCALE #ifdef HAVE_USELOCALE
locale_t oldlocale = uselocale(NULL); locale_t oldlocale = uselocale(NULL);
locale_t newloc;
#elif defined(HAVE_SETLOCALE) #elif defined(HAVE_SETLOCALE)
char *oldlocale = NULL; char *oldlocale = NULL;
#endif #endif
@@ -345,33 +353,16 @@ struct json_object *json_tokener_parse_ex(struct json_tokener *tok, const char *
#ifdef HAVE_USELOCALE #ifdef HAVE_USELOCALE
{ {
#ifdef HAVE_DUPLOCALE if (c_numeric_locale == (locale_t)0)
locale_t duploc = duplocale(oldlocale);
if (duploc == NULL && errno == ENOMEM)
{ {
tok->err = json_tokener_error_memory; c_numeric_locale = newlocale(LC_NUMERIC_MASK, "C", (locale_t)0);
return NULL; if (c_numeric_locale == (locale_t)0)
{
tok->err = json_tokener_error_memory;
return NULL;
}
} }
newloc = newlocale(LC_NUMERIC_MASK, "C", duploc); uselocale(c_numeric_locale);
#else
newloc = newlocale(LC_NUMERIC_MASK, "C", oldlocale);
#endif
if (newloc == NULL)
{
tok->err = json_tokener_error_memory;
#ifdef HAVE_DUPLOCALE
freelocale(duploc);
#endif
return NULL;
}
#ifdef NEWLOCALE_NEEDS_FREELOCALE
#ifdef HAVE_DUPLOCALE
// Older versions of FreeBSD (<12.4) don't free the locale
// passed to newlocale(), so do it here
freelocale(duploc);
#endif
#endif
uselocale(newloc);
} }
#elif defined(HAVE_SETLOCALE) #elif defined(HAVE_SETLOCALE)
{ {
@@ -1409,7 +1400,6 @@ out:
#ifdef HAVE_USELOCALE #ifdef HAVE_USELOCALE
uselocale(oldlocale); uselocale(oldlocale);
freelocale(newloc);
#elif defined(HAVE_SETLOCALE) #elif defined(HAVE_SETLOCALE)
setlocale(LC_NUMERIC, oldlocale); setlocale(LC_NUMERIC, oldlocale);
free(oldlocale); free(oldlocale);
@@ -1476,3 +1466,20 @@ static int json_tokener_parse_double(const char *buf, int len, double *retval)
return 0; // It worked return 0; // It worked
return 1; return 1;
} }
#ifdef HAVE_USELOCALE
/* Release the static C-numeric locale when the library is unloaded.
* This keeps Valgrind clean.
*/
#if defined(__GNUC__) || defined(__clang__)
__attribute__((destructor))
static void json_tokener_locale_fini(void)
{
if (c_numeric_locale != (locale_t)0)
{
freelocale(c_numeric_locale);
c_numeric_locale = (locale_t)0;
}
}
#endif /* __GNUC__ || __clang__ */
#endif /* HAVE_USELOCALE */