[glibc] intl: Add tests for plural expression hardening
Adhemerval Zanella via Glibc-cvs <[email protected]> Mon, 11 May 2026 18:52:11 +0000 (GMT)
| Newsgroups | gmane.comp.lib.glibc.cvs |
|---|---|
| Message-ID | <[email protected]> |
https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=ba305d8268530a20eb537b9880e744c6eed233f9 commit ba305d8268530a20eb537b9880e744c6eed233f9 Author: Avinal Kumar <[email protected]> Date: Mon May 4 22:52:06 2026 +0530 intl: Add tests for plural expression hardening 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 raising 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 entirely. 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 <[email protected]> Signed-off-by: Avinal Kumar <[email protected]> Reviewed-by: Adhemerval Zanella <[email protected]> Diff: --- intl/Makefile | 32 +++++++++++++++++---- intl/plural-depth.awk | 54 +++++++++++++++++++++++++++++++++++ intl/tst-plural-eval.c | 76 +++++++++++++++++++++++++++++++++++++++++++++++++ intl/tst-plural-eval.sh | 67 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 223 insertions(+), 6 deletions(-) diff --git a/intl/Makefile b/intl/Makefile index 42875eb1a9..a8b41a1993 100644 --- a/intl/Makefile +++ b/intl/Makefile @@ -22,13 +22,21 @@ subdir = intl include ../Makeconfig headers = libintl.h -routines = bindtextdom dcgettext dgettext gettext \ +routines = bindtextdom dcgettext dgettext gettext \ dcigettext dcngettext dngettext ngettext \ finddomain loadmsgcat localealias textdomain -aux = l10nflist explodename plural plural-exp hash-string +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 + # test-srcs + ifeq ($(have-thread-library),yes) test-srcs += $(multithread-test-srcs) endif @@ -53,9 +61,15 @@ $(objpfx)plural.o: $(objpfx)plural.c ifeq ($(run-built-tests),yes) 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 +tests-special += \ + $(objpfx)tst-translit.out \ + $(objpfx)tst-gettext.out \ + $(objpfx)tst-gettext2.out \ + $(objpfx)tst-codeset.out \ + $(objpfx)tst-gettext3.out \ + $(objpfx)tst-plural-eval.out + # tests-special + ifeq ($(have-thread-library),yes) tests-special += $(objpfx)tst-gettext4.out $(objpfx)tst-gettext5.out \ $(objpfx)tst-gettext6.out @@ -103,6 +117,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 +158,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 +175,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..8658598330 --- /dev/null +++ b/intl/tst-plural-eval.c @@ -0,0 +1,76 @@ +/* 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> +#include <support/support.h> + +static int +do_test (void) +{ + unsetenv ("OUTPUT_CHARSET"); + + /* Use a real locale so that the active locale is not "C". */ + TEST_VERIFY (setenv ("LC_ALL", "de_DE.UTF-8", 1) == 0); + xsetlocale (LC_ALL, ""); + /* Set a dummy langauge to override lookup. */ + TEST_VERIFY (setenv ("LANGUAGE", "ll", 1) == 0); + + /* 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). */ + + TEST_VERIFY (bindtextdomain ("plural-depth", OBJPFX "domaindir") != NULL); + TEST_VERIFY (textdomain ("plural-depth") != NULL); + + /* 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. */ + + TEST_VERIFY (bindtextdomain ("plural-divzero", OBJPFX "domaindir") != NULL); + TEST_VERIFY (textdomain ("plural-divzero") != NULL); + + /* 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 $?