[v2,2/2] intl: Add tests for plural expression hardening
Checks
| Context |
Check |
Description |
| redhat-pt-bot/TryBot-apply_patch |
success
|
Patch applied to master at the time it was sent
|
| redhat-pt-bot/TryBot-32bit |
success
|
Build for i686
|
Commit Message
The first test checks for stack overflow. It uses a plural expression
nested 5000 levels deep using the !(1-(...)) pattern. The parser
accepts it (below YYMAXDEPTH=10000), but evaluation exeeds
EVAL_MAXDEPTH=100 and falls back to index 0 instead of crashing with
SIGSEGV.
The second test checks for division by zero in plural expression. The
expression (n!=1)+1/(n!=1729) triggers 1/0 for n=1729. msgfmt only
validates 0<= n <= 1000, so the .mo file is accepted. Evaluation
returns PE_INTDIV and falls back instead of raining SIGFPE.
Adaptations from gettext to glibc:
- gettext's plural-3 embeds the nested expresion as a literal string.
This test uses an AWK script (plural-depth.awk) to generate the same
expression.
- gettext uses LANGUAGE= (empty) with LC_ALL=ll and its own locale
setup. glibc requires a real locale for setlocale() or else the "C"
locale override in dcigettext.c ignores LANGUAGE entriely).
The tests are derived from GNU gettext's plural-3 (commit 021348871a22)
and plural-4 (commit 429ba6c6b835), adapted to glibc's test framework.
Original author: Bruno Haible <bruno@clisp.org>
Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
---
intl/Makefile | 11 ++++--
intl/plural-depth.awk | 54 +++++++++++++++++++++++++++++
intl/tst-plural-eval.c | 75 +++++++++++++++++++++++++++++++++++++++++
intl/tst-plural-eval.sh | 67 ++++++++++++++++++++++++++++++++++++
4 files changed, 205 insertions(+), 2 deletions(-)
create mode 100644 intl/plural-depth.awk
create mode 100644 intl/tst-plural-eval.c
create mode 100644 intl/tst-plural-eval.sh
Comments
On 01/05/26 08:24, Avinal Kumar wrote:
> The first test checks for stack overflow. It uses a plural expression
> nested 5000 levels deep using the !(1-(...)) pattern. The parser
> accepts it (below YYMAXDEPTH=10000), but evaluation exeeds
> EVAL_MAXDEPTH=100 and falls back to index 0 instead of crashing with
> SIGSEGV.
>
> The second test checks for division by zero in plural expression. The
> expression (n!=1)+1/(n!=1729) triggers 1/0 for n=1729. msgfmt only
> validates 0<= n <= 1000, so the .mo file is accepted. Evaluation
> returns PE_INTDIV and falls back instead of raining SIGFPE.
>
> Adaptations from gettext to glibc:
>
> - gettext's plural-3 embeds the nested expresion as a literal string.
> This test uses an AWK script (plural-depth.awk) to generate the same
> expression.
>
> - gettext uses LANGUAGE= (empty) with LC_ALL=ll and its own locale
> setup. glibc requires a real locale for setlocale() or else the "C"
> locale override in dcigettext.c ignores LANGUAGE entriely).
>
> The tests are derived from GNU gettext's plural-3 (commit 021348871a22)
> and plural-4 (commit 429ba6c6b835), adapted to glibc's test framework.
> Original author: Bruno Haible <bruno@clisp.org>
>
> Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
The patch looks ok, some style and tests changes.
> ---
> intl/Makefile | 11 ++++--
> intl/plural-depth.awk | 54 +++++++++++++++++++++++++++++
> intl/tst-plural-eval.c | 75 +++++++++++++++++++++++++++++++++++++++++
> intl/tst-plural-eval.sh | 67 ++++++++++++++++++++++++++++++++++++
> 4 files changed, 205 insertions(+), 2 deletions(-)
> create mode 100644 intl/plural-depth.awk
> create mode 100644 intl/tst-plural-eval.c
> create mode 100644 intl/tst-plural-eval.sh
>
> diff --git a/intl/Makefile b/intl/Makefile
> index 42875eb1a9..abfe3ac28c 100644
> --- a/intl/Makefile
> +++ b/intl/Makefile
> @@ -28,7 +28,8 @@ routines = bindtextdom dcgettext dgettext gettext \
> aux = l10nflist explodename plural plural-exp hash-string
>
> multithread-test-srcs := tst-gettext4 tst-gettext5 tst-gettext6
> -test-srcs := tst-gettext tst-translit tst-gettext2 tst-codeset tst-gettext3
> +test-srcs := tst-gettext tst-translit tst-gettext2 tst-codeset tst-gettext3 \
> + tst-plural-eval
Change to:
test-srcs := \
tst-gettext \
... \
# test-srcs
> ifeq ($(have-thread-library),yes)
> test-srcs += $(multithread-test-srcs)
> endif
> @@ -55,7 +56,7 @@ ifeq (yes,$(build-shared))
> ifneq ($(strip $(MSGFMT)),:)
> tests-special += $(objpfx)tst-translit.out $(objpfx)tst-gettext.out \
> $(objpfx)tst-gettext2.out $(objpfx)tst-codeset.out \
> - $(objpfx)tst-gettext3.out
> + $(objpfx)tst-gettext3.out $(objpfx)tst-plural-eval.out
Same as before.
> ifeq ($(have-thread-library),yes)
> tests-special += $(objpfx)tst-gettext4.out $(objpfx)tst-gettext5.out \
> $(objpfx)tst-gettext6.out
> @@ -103,6 +104,10 @@ $(objpfx)tst-gettext4.out: tst-gettext4.sh $(objpfx)tst-gettext4
> $(objpfx)tst-gettext6.out: tst-gettext6.sh $(objpfx)tst-gettext6
> $(SHELL) $< $(common-objpfx) '$(test-program-prefix)' $(common-objpfx)intl/; \
> $(evaluate-test)
> +$(objpfx)tst-plural-eval.out: tst-plural-eval.sh $(objpfx)tst-plural-eval
> + $(SHELL) $< $(common-objpfx) '$(test-program-prefix)' \
> + $(common-objpfx)intl/; \
> + $(evaluate-test)
>
> $(objpfx)tst-codeset.out: $(codeset_mo)
> $(objpfx)tst-gettext3.out: $(codeset_mo)
> @@ -140,6 +145,7 @@ CFLAGS-tst-gettext3.c += -DOBJPFX=\"$(objpfx)\"
> CFLAGS-tst-gettext4.c += -DOBJPFX=\"$(objpfx)\"
> CFLAGS-tst-gettext5.c += -DOBJPFX=\"$(objpfx)\"
> CFLAGS-tst-gettext6.c += -DOBJPFX=\"$(objpfx)\"
> +CFLAGS-tst-plural-eval.c += -DOBJPFX=\"$(objpfx)\"
>
> ifeq ($(have-thread-library),yes)
> ifeq (yes,$(build-shared))
> @@ -156,6 +162,7 @@ $(objpfx)tst-gettext3.out: $(objpfx)tst-gettext.out
> $(objpfx)tst-gettext4.out: $(objpfx)tst-gettext.out
> $(objpfx)tst-gettext5.out: $(objpfx)tst-gettext.out
> $(objpfx)tst-gettext6.out: $(objpfx)tst-gettext.out
> +$(objpfx)tst-plural-eval.out: $(objpfx)tst-gettext.out
>
> CPPFLAGS += -D'LOCALEDIR="$(localedir)"' \
> -D'LOCALE_ALIAS_PATH="$(localedir)"' \
> diff --git a/intl/plural-depth.awk b/intl/plural-depth.awk
> new file mode 100644
> index 0000000000..7b200c00e9
> --- /dev/null
> +++ b/intl/plural-depth.awk
> @@ -0,0 +1,54 @@
> +# plural-depth.awk - Generate .po file with deeply nested plural expression.
> +# Copyright (C) 2026 Free Software Foundation, Inc.
> +#
> +# This file is part of the GNU C Library.
> +#
> +# The GNU C Library is free software; you can redistribute it and/or
> +# modify it under the terms of the GNU Lesser General Public
> +# License as published by the Free Software Foundation; either
> +# version 2.1 of the License, or (at your option) any later version.
> +#
> +# The GNU C Library is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> +# Lesser General Public License for more details.
> +#
> +# You should have received a copy of the GNU Lesser General Public
> +# License along with the GNU C Library; if not, see
> +# <https://www.gnu.org/licenses/>.
> +
> +# Generate a .po file whose Plural-Forms header contains a plural
> +# expression nested DEPTH levels deep. Each level wraps as !(1-(...)),
> +# producing an expression that is accepted by the parser (YYMAXDEPTH=10000)
> +# but exceeds EVAL_MAXDEPTH=100 at runtime.
> +#
> +# Usage: awk -v DEPTH=5000 -f plural-depth.awk > plural-depth.po
> +
> +BEGIN {
> + if (DEPTH == 0)
> + DEPTH = 5000
> +
> + expr = ""
> + for (i = 0; i < DEPTH; i++)
> + expr = expr "!(1-"
> + expr = expr "(n!=1)"
> + for (i = 0; i < DEPTH; i++)
> + expr = expr ")"
> +
> + print "msgid \"\""
> + print "msgstr \"\""
> + print "\"Project-Id-Version: test\\n\""
> + print "\"PO-Revision-Date: 2026-01-01 00:00+0000\\n\""
> + print "\"Last-Translator: \\n\""
> + print "\"Language-Team: \\n\""
> + print "\"Language: ll\\n\""
> + print "\"MIME-Version: 1.0\\n\""
> + print "\"Content-Type: text/plain; charset=ASCII\\n\""
> + print "\"Content-Transfer-Encoding: 8bit\\n\""
> + print "\"Plural-Forms: nplurals=2; plural=" expr ";\\n\""
> + print ""
> + print "msgid \"X\""
> + print "msgid_plural \"Y\""
> + print "msgstr[0] \"x\""
> + print "msgstr[1] \"y\""
> +}
> diff --git a/intl/tst-plural-eval.c b/intl/tst-plural-eval.c
> new file mode 100644
> index 0000000000..cdbc84c6b6
> --- /dev/null
> +++ b/intl/tst-plural-eval.c
> @@ -0,0 +1,75 @@
> +/* Test plural expression evaluation hardening.
> + Copyright (C) 2026 Free Software Foundation, Inc.
> + This file is part of the GNU C Library.
> +
> + The GNU C Library is free software; you can redistribute it and/or
> + modify it under the terms of the GNU Lesser General Public
> + License as published by the Free Software Foundation; either
> + version 2.1 of the License, or (at your option) any later version.
> +
> + The GNU C Library is distributed in the hope that it will be useful,
> + but WITHOUT ANY WARRANTY; without even the implied warranty of
> + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> + Lesser General Public License for more details.
> +
> + You should have received a copy of the GNU Lesser General Public
> + License along with the GNU C Library; if not, see
> + <https://www.gnu.org/licenses/>. */
> +
> +#include <libintl.h>
> +#include <locale.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <support/check.h>
> +
> +static int
> +do_test (void)
> +{
> + unsetenv ("OUTPUT_CHARSET");
> +
> + /* Use a real locale so that the active locale is not "C". */
> + setenv ("LC_ALL", "de_DE.UTF-8", 1);
Use TEST_VERFIY (setenv (...) === 0);
> + setlocale (LC_ALL, "");
Use xsetlocale.
> + /* Set a dummy langauge to override lookup. */
> + setenv ("LANGUAGE", "ll", 1);
> +
> + /* Test 1: Stack overflow in plural evaluation.
> + The .mo file has a plural expression with 5000 levels of nesting
> + like !(1-(!(1-(...(n!=1)...)))). Before the fix, plural_eval()
> + used unbounded recursion and would crash with SIGSEGV on threads
> + with small stacks. After the fix, EVAL_MAXDEPTH=100 causes
> + plural_eval_recurse() to return PE_STACKOVF, and plural_lookup()
> + falls back to index 0 (the singular form). */
> +
> + bindtextdomain ("plural-depth", OBJPFX "domaindir");
Use TEST_VERFIY.
> + textdomain ("plural-depth");
Ditto.
> +
> + /* ngettext must not crash. The return value depends on whether
> + the depth limit is hit (falls back to index 0) or the expression
> + evaluates successfully. Either result is acceptable. */
> +
> + const char *tr = ngettext ("X", "Y", 42);
> + TEST_VERIFY (tr != NULL);
> + TEST_VERIFY (strcmp (tr, "x") == 0 || strcmp (tr, "y") == 0);
> +
> + /* Test 2: Division by zero in plural evaluation.
> + The .mo file has plural expression (n!=1)+1/(n!=1729).
> + For n=1729, (n!=1729) is 0, so 1/0 triggers division by zero.
> + Before the fix, this raised SIGFPE. After the fix,
> + plural_eval_recurse() returns PE_INTDIV, and plural_lookup()
> + falls back to index 0. */
> +
> + bindtextdomain ("plural-divzero", OBJPFX "domaindir");
> + textdomain ("plural-divzero");
> +
> + /* ngettext with n=1729 must not crash with SIGFPE. */
> + tr = ngettext ("X", "Y", 1729);
> + TEST_VERIFY (tr != NULL);
> + TEST_VERIFY (strcmp (tr, "x") == 0 || strcmp (tr, "y") == 0
> + || strcmp (tr, "z") == 0);
> +
> + return 0;
> +}
> +
> +#include <support/test-driver.c>
> diff --git a/intl/tst-plural-eval.sh b/intl/tst-plural-eval.sh
> new file mode 100644
> index 0000000000..f020fa9925
> --- /dev/null
> +++ b/intl/tst-plural-eval.sh
> @@ -0,0 +1,67 @@
> +#!/bin/sh
> +# Test plural expression evaluation hardening.
> +# Copyright (C) 2026 Free Software Foundation, Inc.
> +# This file is part of the GNU C Library.
> +
> +# The GNU C Library is free software; you can redistribute it and/or
> +# modify it under the terms of the GNU Lesser General Public
> +# License as published by the Free Software Foundation; either
> +# version 2.1 of the License, or (at your option) any later version.
> +
> +# The GNU C Library is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> +# Lesser General Public License for more details.
> +
> +# You should have received a copy of the GNU Lesser General Public
> +# License along with the GNU C Library; if not, see
> +# <https://www.gnu.org/licenses/>.
> +
> +set -e
> +
> +common_objpfx=$1
> +test_program_prefix=$2
> +objpfx=$3
> +
> +# Create domain directories.
> +mkdir -p ${objpfx}domaindir/ll/LC_MESSAGES
> +
> +# Test 1: Deeply nested plural expression (stack overflow test).
> +# This expression has 5000 levels of nesting, well above EVAL_MAXDEPTH=100
> +# but below YYMAXDEPTH=10000 so the parser accepts it.
> +LC_ALL=C awk -v DEPTH=5000 -f plural-depth.awk > ${objpfx}plural-depth.po
> +
> +msgfmt -o ${objpfx}domaindir/ll/LC_MESSAGES/plural-depth.mo \
> + ${objpfx}plural-depth.po || exit 1
> +
> +# Test 2: Division by zero in plural expression (SIGFPE test).
> +# The expression 1/(n!=1729) triggers division by zero for n=1729.
> +# msgfmt -c only checks 0 <= n <= 1000, so this passes validation.
> +cat > ${objpfx}plural-divzero.po <<EOF
> +msgid ""
> +msgstr ""
> +"Project-Id-Version: test\n"
> +"PO-Revision-Date: 2026-01-01 00:00+0000\n"
> +"Last-Translator: \n"
> +"Language-Team: \n"
> +"Language: ll\n"
> +"MIME-Version: 1.0\n"
> +"Content-Type: text/plain; charset=ASCII\n"
> +"Content-Transfer-Encoding: 8bit\n"
> +"Plural-Forms: nplurals=3; plural=(n!=1)+1/(n!=1729);\n"
> +
> +msgid "X"
> +msgid_plural "Y"
> +msgstr[0] "x"
> +msgstr[1] "y"
> +msgstr[2] "z"
> +EOF
> +
> +msgfmt -o ${objpfx}domaindir/ll/LC_MESSAGES/plural-divzero.mo \
> + ${objpfx}plural-divzero.po || exit 1
> +
> +# Run the test.
> +${test_program_prefix} \
> +${objpfx}tst-plural-eval ${objpfx}domaindir
> +
> +exit $?
@@ -28,7 +28,8 @@ routines = bindtextdom dcgettext dgettext gettext \
aux = l10nflist explodename plural plural-exp hash-string
multithread-test-srcs := tst-gettext4 tst-gettext5 tst-gettext6
-test-srcs := tst-gettext tst-translit tst-gettext2 tst-codeset tst-gettext3
+test-srcs := tst-gettext tst-translit tst-gettext2 tst-codeset tst-gettext3 \
+ tst-plural-eval
ifeq ($(have-thread-library),yes)
test-srcs += $(multithread-test-srcs)
endif
@@ -55,7 +56,7 @@ ifeq (yes,$(build-shared))
ifneq ($(strip $(MSGFMT)),:)
tests-special += $(objpfx)tst-translit.out $(objpfx)tst-gettext.out \
$(objpfx)tst-gettext2.out $(objpfx)tst-codeset.out \
- $(objpfx)tst-gettext3.out
+ $(objpfx)tst-gettext3.out $(objpfx)tst-plural-eval.out
ifeq ($(have-thread-library),yes)
tests-special += $(objpfx)tst-gettext4.out $(objpfx)tst-gettext5.out \
$(objpfx)tst-gettext6.out
@@ -103,6 +104,10 @@ $(objpfx)tst-gettext4.out: tst-gettext4.sh $(objpfx)tst-gettext4
$(objpfx)tst-gettext6.out: tst-gettext6.sh $(objpfx)tst-gettext6
$(SHELL) $< $(common-objpfx) '$(test-program-prefix)' $(common-objpfx)intl/; \
$(evaluate-test)
+$(objpfx)tst-plural-eval.out: tst-plural-eval.sh $(objpfx)tst-plural-eval
+ $(SHELL) $< $(common-objpfx) '$(test-program-prefix)' \
+ $(common-objpfx)intl/; \
+ $(evaluate-test)
$(objpfx)tst-codeset.out: $(codeset_mo)
$(objpfx)tst-gettext3.out: $(codeset_mo)
@@ -140,6 +145,7 @@ CFLAGS-tst-gettext3.c += -DOBJPFX=\"$(objpfx)\"
CFLAGS-tst-gettext4.c += -DOBJPFX=\"$(objpfx)\"
CFLAGS-tst-gettext5.c += -DOBJPFX=\"$(objpfx)\"
CFLAGS-tst-gettext6.c += -DOBJPFX=\"$(objpfx)\"
+CFLAGS-tst-plural-eval.c += -DOBJPFX=\"$(objpfx)\"
ifeq ($(have-thread-library),yes)
ifeq (yes,$(build-shared))
@@ -156,6 +162,7 @@ $(objpfx)tst-gettext3.out: $(objpfx)tst-gettext.out
$(objpfx)tst-gettext4.out: $(objpfx)tst-gettext.out
$(objpfx)tst-gettext5.out: $(objpfx)tst-gettext.out
$(objpfx)tst-gettext6.out: $(objpfx)tst-gettext.out
+$(objpfx)tst-plural-eval.out: $(objpfx)tst-gettext.out
CPPFLAGS += -D'LOCALEDIR="$(localedir)"' \
-D'LOCALE_ALIAS_PATH="$(localedir)"' \
new file mode 100644
@@ -0,0 +1,54 @@
+# plural-depth.awk - Generate .po file with deeply nested plural expression.
+# Copyright (C) 2026 Free Software Foundation, Inc.
+#
+# This file is part of the GNU C Library.
+#
+# The GNU C Library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# The GNU C Library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with the GNU C Library; if not, see
+# <https://www.gnu.org/licenses/>.
+
+# Generate a .po file whose Plural-Forms header contains a plural
+# expression nested DEPTH levels deep. Each level wraps as !(1-(...)),
+# producing an expression that is accepted by the parser (YYMAXDEPTH=10000)
+# but exceeds EVAL_MAXDEPTH=100 at runtime.
+#
+# Usage: awk -v DEPTH=5000 -f plural-depth.awk > plural-depth.po
+
+BEGIN {
+ if (DEPTH == 0)
+ DEPTH = 5000
+
+ expr = ""
+ for (i = 0; i < DEPTH; i++)
+ expr = expr "!(1-"
+ expr = expr "(n!=1)"
+ for (i = 0; i < DEPTH; i++)
+ expr = expr ")"
+
+ print "msgid \"\""
+ print "msgstr \"\""
+ print "\"Project-Id-Version: test\\n\""
+ print "\"PO-Revision-Date: 2026-01-01 00:00+0000\\n\""
+ print "\"Last-Translator: \\n\""
+ print "\"Language-Team: \\n\""
+ print "\"Language: ll\\n\""
+ print "\"MIME-Version: 1.0\\n\""
+ print "\"Content-Type: text/plain; charset=ASCII\\n\""
+ print "\"Content-Transfer-Encoding: 8bit\\n\""
+ print "\"Plural-Forms: nplurals=2; plural=" expr ";\\n\""
+ print ""
+ print "msgid \"X\""
+ print "msgid_plural \"Y\""
+ print "msgstr[0] \"x\""
+ print "msgstr[1] \"y\""
+}
new file mode 100644
@@ -0,0 +1,75 @@
+/* Test plural expression evaluation hardening.
+ Copyright (C) 2026 Free Software Foundation, Inc.
+ This file is part of the GNU C Library.
+
+ The GNU C Library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ The GNU C Library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with the GNU C Library; if not, see
+ <https://www.gnu.org/licenses/>. */
+
+#include <libintl.h>
+#include <locale.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <support/check.h>
+
+static int
+do_test (void)
+{
+ unsetenv ("OUTPUT_CHARSET");
+
+ /* Use a real locale so that the active locale is not "C". */
+ setenv ("LC_ALL", "de_DE.UTF-8", 1);
+ setlocale (LC_ALL, "");
+ /* Set a dummy langauge to override lookup. */
+ setenv ("LANGUAGE", "ll", 1);
+
+ /* Test 1: Stack overflow in plural evaluation.
+ The .mo file has a plural expression with 5000 levels of nesting
+ like !(1-(!(1-(...(n!=1)...)))). Before the fix, plural_eval()
+ used unbounded recursion and would crash with SIGSEGV on threads
+ with small stacks. After the fix, EVAL_MAXDEPTH=100 causes
+ plural_eval_recurse() to return PE_STACKOVF, and plural_lookup()
+ falls back to index 0 (the singular form). */
+
+ bindtextdomain ("plural-depth", OBJPFX "domaindir");
+ textdomain ("plural-depth");
+
+ /* ngettext must not crash. The return value depends on whether
+ the depth limit is hit (falls back to index 0) or the expression
+ evaluates successfully. Either result is acceptable. */
+
+ const char *tr = ngettext ("X", "Y", 42);
+ TEST_VERIFY (tr != NULL);
+ TEST_VERIFY (strcmp (tr, "x") == 0 || strcmp (tr, "y") == 0);
+
+ /* Test 2: Division by zero in plural evaluation.
+ The .mo file has plural expression (n!=1)+1/(n!=1729).
+ For n=1729, (n!=1729) is 0, so 1/0 triggers division by zero.
+ Before the fix, this raised SIGFPE. After the fix,
+ plural_eval_recurse() returns PE_INTDIV, and plural_lookup()
+ falls back to index 0. */
+
+ bindtextdomain ("plural-divzero", OBJPFX "domaindir");
+ textdomain ("plural-divzero");
+
+ /* ngettext with n=1729 must not crash with SIGFPE. */
+ tr = ngettext ("X", "Y", 1729);
+ TEST_VERIFY (tr != NULL);
+ TEST_VERIFY (strcmp (tr, "x") == 0 || strcmp (tr, "y") == 0
+ || strcmp (tr, "z") == 0);
+
+ return 0;
+}
+
+#include <support/test-driver.c>
new file mode 100644
@@ -0,0 +1,67 @@
+#!/bin/sh
+# Test plural expression evaluation hardening.
+# Copyright (C) 2026 Free Software Foundation, Inc.
+# This file is part of the GNU C Library.
+
+# The GNU C Library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+
+# The GNU C Library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+
+# You should have received a copy of the GNU Lesser General Public
+# License along with the GNU C Library; if not, see
+# <https://www.gnu.org/licenses/>.
+
+set -e
+
+common_objpfx=$1
+test_program_prefix=$2
+objpfx=$3
+
+# Create domain directories.
+mkdir -p ${objpfx}domaindir/ll/LC_MESSAGES
+
+# Test 1: Deeply nested plural expression (stack overflow test).
+# This expression has 5000 levels of nesting, well above EVAL_MAXDEPTH=100
+# but below YYMAXDEPTH=10000 so the parser accepts it.
+LC_ALL=C awk -v DEPTH=5000 -f plural-depth.awk > ${objpfx}plural-depth.po
+
+msgfmt -o ${objpfx}domaindir/ll/LC_MESSAGES/plural-depth.mo \
+ ${objpfx}plural-depth.po || exit 1
+
+# Test 2: Division by zero in plural expression (SIGFPE test).
+# The expression 1/(n!=1729) triggers division by zero for n=1729.
+# msgfmt -c only checks 0 <= n <= 1000, so this passes validation.
+cat > ${objpfx}plural-divzero.po <<EOF
+msgid ""
+msgstr ""
+"Project-Id-Version: test\n"
+"PO-Revision-Date: 2026-01-01 00:00+0000\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"Language: ll\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=ASCII\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n!=1)+1/(n!=1729);\n"
+
+msgid "X"
+msgid_plural "Y"
+msgstr[0] "x"
+msgstr[1] "y"
+msgstr[2] "z"
+EOF
+
+msgfmt -o ${objpfx}domaindir/ll/LC_MESSAGES/plural-divzero.mo \
+ ${objpfx}plural-divzero.po || exit 1
+
+# Run the test.
+${test_program_prefix} \
+${objpfx}tst-plural-eval ${objpfx}domaindir
+
+exit $?