[glibc] intl: Import plural expression hardening from GNU gettext
Adhemerval Zanella via Glibc-cvs <[email protected]> Mon, 11 May 2026 18:52:06 +0000 (GMT)
| Newsgroups | gmane.comp.lib.glibc.cvs |
|---|---|
| Message-ID | <[email protected]> |
https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=e7f5359db75ac4713b8c45bd4213264c0a26bc06 commit e7f5359db75ac4713b8c45bd4213264c0a26bc06 Author: Avinal Kumar <[email protected]> Date: Mon May 4 22:52:05 2026 +0530 intl: Import plural expression hardening from GNU gettext The plural expression evaluator plural_eval() in eval-plural.h uses unbounded recursion, which can cause a stack overflow crash with deeply nested expressions in malicious .mo files. This is particularly dangerous on threads with small stacks (musl libc default: 128 KB, AIX 7 default: 96 KB, glibc after ulimit -s 260: ~3919 recursions max). Additionally, division by zero in plural expressions triggers raise(SIGFPE), which is not multithread-safe, catching SIGFPE requires per-process signal handlers that race with other threads. Fix both by importing the hardening from GNU gettext: - Replace unbounded plural_eval() with depth-limited plural_eval_recurse() (EVAL_MAXDEPTH=100), returning a struct eval_result with status instead of a bare unsigned long. - Return PE_INTDIV status on division by zero instead of raising SIGFPE. Remove the architecture-specific INTDIV0_RAISES_SIGFPE macro and the conditional #include <signal.h>. - Update plural_lookup() in dcigettext.c to handle the new return type, falling back to index 0 on any evaluation failure. Based on GNU gettext commits ef37a1540 and 726bfb1d1. Discussed on: https://sourceware.org/pipermail/libc-alpha/2023-October/152010.html Original author: Bruno Haible <[email protected]> Signed-off-by: Avinal Kumar <[email protected]> Reviewed-by: Adhemerval Zanella <[email protected]> Diff: --- intl/dcigettext.c | 27 ++++------- intl/eval-plural.h | 138 ++++++++++++++++++++++++++++++++++++++++------------- intl/plural-exp.h | 21 +++++++- 3 files changed, 133 insertions(+), 53 deletions(-) diff --git a/intl/dcigettext.c b/intl/dcigettext.c index 9ebc54eeec..fb948c1092 100644 --- a/intl/dcigettext.c +++ b/intl/dcigettext.c @@ -68,20 +68,6 @@ extern int errno; #include <locale.h> -#ifdef _LIBC - /* Guess whether integer division by zero raises signal SIGFPE. - Set to 1 only if you know for sure. In case of doubt, set to 0. */ -# if defined __alpha__ || defined __arm__ || defined __i386__ \ - || defined __m68k__ || defined __s390x__ -# define INTDIV0_RAISES_SIGFPE 1 -# else -# define INTDIV0_RAISES_SIGFPE 0 -# endif -#endif -#if !INTDIV0_RAISES_SIGFPE -# include <signal.h> -#endif - #if defined HAVE_SYS_PARAM_H || defined _LIBC # include <sys/param.h> #endif @@ -1381,14 +1367,19 @@ plural_lookup (struct loaded_l10nfile *domain, unsigned long int n, const char *translation, size_t translation_len) { struct loaded_domain *domaindata = (struct loaded_domain *) domain->data; + struct eval_result result; unsigned long int index; const char *p; - index = plural_eval (domaindata->plural, n); - if (index >= domaindata->nplurals) - /* This should never happen. It means the plural expression and the - given maximum value do not match. */ + result = plural_eval (domaindata->plural, n); + if (result.status != PE_OK) + /* The plural expression evaluation failed. */ index = 0; + else if (result.value >= domaindata->nplurals) + /* The plural expression and the given maximum value do not match. */ + index = 0; + else + index = result.value; /* Skip INDEX strings at TRANSLATION. */ p = translation; diff --git a/intl/eval-plural.h b/intl/eval-plural.h index 243f6027a1..677b13d0f1 100644 --- a/intl/eval-plural.h +++ b/intl/eval-plural.h @@ -14,24 +14,59 @@ You should have received a copy of the GNU Lesser General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. */ +/* Written by Ulrich Drepper and Bruno Haible. */ + #ifndef STATIC #define STATIC static #endif -/* Evaluate the plural expression and return an index value. */ -STATIC -unsigned long int -plural_eval (const struct expression *pexp, unsigned long int n) +/* While the bison parser is able to support expressions of a maximum depth + of YYMAXDEPTH = 10000, the runtime evaluation of a parsed plural expression + has a smaller maximum recursion depth. + If we did not limit the recursion depth, a program that just invokes + ngettext() on a thread other than the main thread could get a crash by + stack overflow in the following circumstances: + - On systems with glibc, after the stack size has been reduced, + e.g. on x86_64 systems after "ulimit -s 260". + This stack size is only sufficient for ca. 3919 recursions. + Cf. <https://unix.stackexchange.com/questions/620720/> + - On systems with musl libc, because there the thread stack size (for a + thread other than the main thread) by default is only 128 KB, see + <https://wiki.musl-libc.org/functional-differences-from-glibc.html#Thread-stack-size>. + - On AIX 7 systems, because there the thread stack size (for a thread + other than the main thread) by default is only 96 KB, see + <https://www.ibm.com/docs/en/aix/7.1?topic=programming-threads-library-options>. + This stack size is only sufficient for between 887 and 1363 recursions, + depending on the compiler and compiler optimization options. + A maximum depth of 100 is a large enough for all practical needs + and also small enough to avoid stack overflow even with small thread stack + sizes. */ +#ifndef EVAL_MAXDEPTH +# define EVAL_MAXDEPTH 100 +#endif + +/* A shorthand that denotes a successful evaluation result with a value V. */ +#define OK(v) (struct eval_result) { .status = PE_OK, .value = (v) } + +/* Evaluates a plural expression PEXP for n=N, up to ALLOWED_DEPTH. */ +static struct eval_result +plural_eval_recurse (const struct expression *pexp, unsigned long int n, + unsigned int allowed_depth) { + if (allowed_depth == 0) + /* The allowed recursion depth is exhausted. */ + return (struct eval_result) { .status = PE_STACKOVF }; + allowed_depth--; + switch (pexp->nargs) { case 0: switch (pexp->operation) { case var: - return n; + return OK (n); case num: - return pexp->val.num; + return OK (pexp->val.num); default: break; } @@ -40,52 +75,73 @@ plural_eval (const struct expression *pexp, unsigned long int n) case 1: { /* pexp->operation must be lnot. */ - unsigned long int arg = plural_eval (pexp->val.args[0], n); - return ! arg; + struct eval_result arg = + plural_eval_recurse (pexp->val.args[0], n, allowed_depth); + if (arg.status != PE_OK) + return arg; + return OK (! arg.value); } case 2: { - unsigned long int leftarg = plural_eval (pexp->val.args[0], n); + struct eval_result leftarg = + plural_eval_recurse (pexp->val.args[0], n, allowed_depth); + if (leftarg.status != PE_OK) + return leftarg; if (pexp->operation == lor) - return leftarg || plural_eval (pexp->val.args[1], n); + { + if (leftarg.value) + return OK (1); + struct eval_result rightarg = + plural_eval_recurse (pexp->val.args[1], n, allowed_depth); + if (rightarg.status != PE_OK) + return rightarg; + return OK (rightarg.value ? 1 : 0); + } else if (pexp->operation == land) - return leftarg && plural_eval (pexp->val.args[1], n); + { + if (!leftarg.value) + return OK (0); + struct eval_result rightarg = + plural_eval_recurse (pexp->val.args[1], n, allowed_depth); + if (rightarg.status != PE_OK) + return rightarg; + return OK (rightarg.value ? 1 : 0); + } else { - unsigned long int rightarg = plural_eval (pexp->val.args[1], n); + struct eval_result rightarg = + plural_eval_recurse (pexp->val.args[1], n, allowed_depth); + if (rightarg.status != PE_OK) + return rightarg; switch (pexp->operation) { case mult: - return leftarg * rightarg; + return OK (leftarg.value * rightarg.value); case divide: -#if !INTDIV0_RAISES_SIGFPE - if (rightarg == 0) - raise (SIGFPE); -#endif - return leftarg / rightarg; + if (rightarg.value == 0) + return (struct eval_result) { .status = PE_INTDIV }; + return OK (leftarg.value / rightarg.value); case module: -#if !INTDIV0_RAISES_SIGFPE - if (rightarg == 0) - raise (SIGFPE); -#endif - return leftarg % rightarg; + if (rightarg.value == 0) + return (struct eval_result) { .status = PE_INTDIV }; + return OK (leftarg.value % rightarg.value); case plus: - return leftarg + rightarg; + return OK (leftarg.value + rightarg.value); case minus: - return leftarg - rightarg; + return OK (leftarg.value - rightarg.value); case less_than: - return leftarg < rightarg; + return OK (leftarg.value < rightarg.value); case greater_than: - return leftarg > rightarg; + return OK (leftarg.value > rightarg.value); case less_or_equal: - return leftarg <= rightarg; + return OK (leftarg.value <= rightarg.value); case greater_or_equal: - return leftarg >= rightarg; + return OK (leftarg.value >= rightarg.value); case equal: - return leftarg == rightarg; + return OK (leftarg.value == rightarg.value); case not_equal: - return leftarg != rightarg; + return OK (leftarg.value != rightarg.value); default: break; } @@ -96,10 +152,24 @@ plural_eval (const struct expression *pexp, unsigned long int n) case 3: { /* pexp->operation must be qmop. */ - unsigned long int boolarg = plural_eval (pexp->val.args[0], n); - return plural_eval (pexp->val.args[boolarg ? 1 : 2], n); + struct eval_result boolarg = + plural_eval_recurse (pexp->val.args[0], n, allowed_depth); + if (boolarg.status != PE_OK) + return boolarg; + return plural_eval_recurse (pexp->val.args[boolarg.value ? 1 : 2], n, + allowed_depth); } } /* NOTREACHED */ - return 0; + return (struct eval_result) { .status = PE_ASSERT }; } + +/* Evaluates a plural expression PEXP for n=N. */ +STATIC +struct eval_result +plural_eval (const struct expression *pexp, unsigned long int n) +{ + return plural_eval_recurse (pexp, n, EVAL_MAXDEPTH); +} + +#undef OK diff --git a/intl/plural-exp.h b/intl/plural-exp.h index f0d3a611e1..e2e0c30563 100644 --- a/intl/plural-exp.h +++ b/intl/plural-exp.h @@ -26,6 +26,7 @@ extern "C" { #endif +/* Parsing a plural expression. */ enum expression_operator { @@ -109,8 +110,26 @@ extern void EXTRACT_PLURAL_EXPRESSION (const char *nullentry, unsigned long int *npluralsp) attribute_hidden; +/* Evaluating a parsed plural expression. */ + +enum eval_status +{ + PE_OK, /* Evaluation succeeded, produced a value */ + PE_INTDIV, /* Integer division by zero */ + PE_INTOVF, /* Integer overflow */ + PE_STACKOVF, /* Stack overflow */ + PE_ASSERT /* Assertion failure */ +}; + +struct eval_result +{ + enum eval_status status; + unsigned long int value; /* Only relevant for status == PE_OK */ +}; + + #if !defined (_LIBC) && !defined (IN_LIBINTL) && !defined (IN_LIBGLOCALE) -extern unsigned long int plural_eval (const struct expression *pexp, +extern struct eval_result plural_eval (const struct expression *pexp, unsigned long int n); #endif