Skip to content

Commit 20ab60d

Browse files
orlitzkycharmitro
authored andcommitted
ext/gettext/gettext.c: handle NULLs from bindtextdomain()
According to POSIX, bindtextdomain() returns "the implementation- defined default directory pathname used by the gettext family of functions" when its second parameter is NULL (i.e. when you are querying the directory corresponding to some text domain and that directory has not yet been set). Its PHP counterpart is feeding that result direclty to RETURN_STRING, but this can go wrong in two ways: 1. If an error occurs, even POSIX-compliant implementations may return NULL. 2. At least one non-compliant implementation (musl) lacks a default directory and returns NULL whenever the domain has not yet been bound. In either of those cases, PHP segfaults on the NULL string. In this commit we check for the NULL, and RETURN_FALSE when it happens rather than crashing. This partially addresses GH php#13696
1 parent 3c356c9 commit 20ab60d

File tree

1 file changed

+80
-88
lines changed

1 file changed

+80
-88
lines changed

ext/gettext/gettext.c

Lines changed: 80 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
#ifdef HAVE_CONFIG_H
18-
#include <config.h>
18+
#include "config.h"
1919
#endif
2020

2121
#include "php.h"
@@ -54,9 +54,6 @@ ZEND_GET_MODULE(php_gettext)
5454
if (UNEXPECTED(domain_len > PHP_GETTEXT_MAX_DOMAIN_LENGTH)) { \
5555
zend_argument_value_error(_arg_num, "is too long"); \
5656
RETURN_THROWS(); \
57-
} else if (domain_len == 0) { \
58-
zend_argument_must_not_be_empty_error(_arg_num); \
59-
RETURN_THROWS(); \
6057
}
6158

6259
#define PHP_GETTEXT_LENGTH_CHECK(_arg_num, check_len) \
@@ -65,12 +62,6 @@ ZEND_GET_MODULE(php_gettext)
6562
RETURN_THROWS(); \
6663
}
6764

68-
#define PHP_DCGETTEXT_CATEGORY_CHECK(_arg_num, category) \
69-
if (category == LC_ALL) { \
70-
zend_argument_value_error(_arg_num, "cannot be LC_ALL"); \
71-
RETURN_THROWS(); \
72-
}
73-
7465
PHP_MINFO_FUNCTION(php_gettext)
7566
{
7667
php_info_print_table_start();
@@ -81,20 +72,15 @@ PHP_MINFO_FUNCTION(php_gettext)
8172
/* {{{ Set the textdomain to "domain". Returns the current domain */
8273
PHP_FUNCTION(textdomain)
8374
{
84-
char *domain_name = NULL, *retval = NULL;
75+
char *domain_name = NULL, *retval;
8576
zend_string *domain = NULL;
8677

87-
ZEND_PARSE_PARAMETERS_START(0, 1)
88-
Z_PARAM_OPTIONAL
89-
Z_PARAM_STR_OR_NULL(domain)
90-
ZEND_PARSE_PARAMETERS_END();
78+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "S!", &domain) == FAILURE) {
79+
RETURN_THROWS();
80+
}
9181

92-
if (domain != NULL) {
82+
if (domain != NULL && ZSTR_LEN(domain) != 0 && !zend_string_equals_literal(domain, "0")) {
9383
PHP_GETTEXT_DOMAIN_LENGTH_CHECK(1, ZSTR_LEN(domain))
94-
if (zend_string_equals_literal(domain, "0")) {
95-
zend_argument_value_error(1, "cannot be zero");
96-
RETURN_THROWS();
97-
}
9884
domain_name = ZSTR_VAL(domain);
9985
}
10086

@@ -107,7 +93,7 @@ PHP_FUNCTION(textdomain)
10793
/* {{{ Return the translation of msgid for the current domain, or msgid unaltered if a translation does not exist */
10894
PHP_FUNCTION(gettext)
10995
{
110-
char *msgstr = NULL;
96+
char *msgstr;
11197
zend_string *msgid;
11298

11399
ZEND_PARSE_PARAMETERS_START(1, 1)
@@ -128,13 +114,12 @@ PHP_FUNCTION(gettext)
128114
/* {{{ Return the translation of msgid for domain_name, or msgid unaltered if a translation does not exist */
129115
PHP_FUNCTION(dgettext)
130116
{
131-
char *msgstr = NULL;
117+
char *msgstr;
132118
zend_string *domain, *msgid;
133119

134-
ZEND_PARSE_PARAMETERS_START(2, 2)
135-
Z_PARAM_STR(domain)
136-
Z_PARAM_STR(msgid)
137-
ZEND_PARSE_PARAMETERS_END();
120+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "SS", &domain, &msgid) == FAILURE) {
121+
RETURN_THROWS();
122+
}
138123

139124
PHP_GETTEXT_DOMAIN_LENGTH_CHECK(1, ZSTR_LEN(domain))
140125
PHP_GETTEXT_LENGTH_CHECK(2, ZSTR_LEN(msgid))
@@ -152,19 +137,19 @@ PHP_FUNCTION(dgettext)
152137
/* {{{ Return the translation of msgid for domain_name and category, or msgid unaltered if a translation does not exist */
153138
PHP_FUNCTION(dcgettext)
154139
{
155-
char *msgstr = NULL;
140+
char *msgstr;
156141
zend_string *domain, *msgid;
157142
zend_long category;
158143

159-
ZEND_PARSE_PARAMETERS_START(3, 3)
160-
Z_PARAM_STR(domain)
161-
Z_PARAM_STR(msgid)
162-
Z_PARAM_LONG(category)
163-
ZEND_PARSE_PARAMETERS_END();
144+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "SSl", &domain, &msgid, &category) == FAILURE) {
145+
RETURN_THROWS();
146+
}
164147

165148
PHP_GETTEXT_DOMAIN_LENGTH_CHECK(1, ZSTR_LEN(domain))
166149
PHP_GETTEXT_LENGTH_CHECK(2, ZSTR_LEN(msgid))
167-
PHP_DCGETTEXT_CATEGORY_CHECK(3, category)
150+
if (category == LC_ALL) {
151+
RETURN_STR_COPY(msgid);
152+
}
168153

169154
msgstr = dcgettext(ZSTR_VAL(domain), ZSTR_VAL(msgid), category);
170155

@@ -179,19 +164,33 @@ PHP_FUNCTION(dcgettext)
179164
/* {{{ Bind to the text domain domain_name, looking for translations in dir. Returns the current domain */
180165
PHP_FUNCTION(bindtextdomain)
181166
{
182-
zend_string *domain, *dir = NULL;
183-
char *retval, dir_name[MAXPATHLEN];
167+
char *domain;
168+
size_t domain_len;
169+
zend_string *dir = NULL;
170+
char *retval, dir_name[MAXPATHLEN], *btd_result;
184171

185-
ZEND_PARSE_PARAMETERS_START(1, 2)
186-
Z_PARAM_STR(domain)
187-
Z_PARAM_OPTIONAL
188-
Z_PARAM_STR_OR_NULL(dir)
189-
ZEND_PARSE_PARAMETERS_END();
172+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "sS!", &domain, &domain_len, &dir) == FAILURE) {
173+
RETURN_THROWS();
174+
}
190175

191-
PHP_GETTEXT_DOMAIN_LENGTH_CHECK(1, ZSTR_LEN(domain))
176+
PHP_GETTEXT_DOMAIN_LENGTH_CHECK(1, domain_len)
177+
178+
if (domain[0] == '\0') {
179+
zend_argument_value_error(1, "cannot be empty");
180+
RETURN_THROWS();
181+
}
192182

193183
if (dir == NULL) {
194-
RETURN_STRING(bindtextdomain(ZSTR_VAL(domain), NULL));
184+
btd_result = bindtextdomain(domain, NULL);
185+
if (btd_result == NULL) {
186+
/* POSIX-compliant implementations can return
187+
* NULL if an error occured. On musl you will
188+
* also get NULL if the domain is not yet
189+
* bound, because musl has no default directory
190+
* to return in that case. */
191+
RETURN_FALSE;
192+
}
193+
RETURN_STRING(btd_result);
195194
}
196195

197196
if (ZSTR_LEN(dir) != 0 && !zend_string_equals_literal(dir, "0")) {
@@ -202,7 +201,7 @@ PHP_FUNCTION(bindtextdomain)
202201
RETURN_FALSE;
203202
}
204203

205-
retval = bindtextdomain(ZSTR_VAL(domain), dir_name);
204+
retval = bindtextdomain(domain, dir_name);
206205

207206
RETURN_STRING(retval);
208207
}
@@ -212,20 +211,18 @@ PHP_FUNCTION(bindtextdomain)
212211
/* {{{ Plural version of gettext() */
213212
PHP_FUNCTION(ngettext)
214213
{
215-
char *msgstr = NULL;
216-
zend_string *msgid1, *msgid2;
214+
char *msgid1, *msgid2, *msgstr;
215+
size_t msgid1_len, msgid2_len;
217216
zend_long count;
218217

219-
ZEND_PARSE_PARAMETERS_START(3, 3)
220-
Z_PARAM_STR(msgid1)
221-
Z_PARAM_STR(msgid2)
222-
Z_PARAM_LONG(count)
223-
ZEND_PARSE_PARAMETERS_END();
218+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "ssl", &msgid1, &msgid1_len, &msgid2, &msgid2_len, &count) == FAILURE) {
219+
RETURN_THROWS();
220+
}
224221

225-
PHP_GETTEXT_LENGTH_CHECK(1, ZSTR_LEN(msgid1))
226-
PHP_GETTEXT_LENGTH_CHECK(2, ZSTR_LEN(msgid2))
222+
PHP_GETTEXT_LENGTH_CHECK(1, msgid1_len)
223+
PHP_GETTEXT_LENGTH_CHECK(2, msgid2_len)
227224

228-
msgstr = ngettext(ZSTR_VAL(msgid1), ZSTR_VAL(msgid2), count);
225+
msgstr = ngettext(msgid1, msgid2, count);
229226

230227
ZEND_ASSERT(msgstr);
231228
RETURN_STRING(msgstr);
@@ -237,22 +234,20 @@ PHP_FUNCTION(ngettext)
237234
/* {{{ Plural version of dgettext() */
238235
PHP_FUNCTION(dngettext)
239236
{
240-
char *msgstr = NULL;
241-
zend_string *domain, *msgid1, *msgid2;
237+
char *domain, *msgid1, *msgid2, *msgstr = NULL;
238+
size_t domain_len, msgid1_len, msgid2_len;
242239
zend_long count;
243240

244-
ZEND_PARSE_PARAMETERS_START(4, 4)
245-
Z_PARAM_STR(domain)
246-
Z_PARAM_STR(msgid1)
247-
Z_PARAM_STR(msgid2)
248-
Z_PARAM_LONG(count)
249-
ZEND_PARSE_PARAMETERS_END();
241+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "sssl", &domain, &domain_len,
242+
&msgid1, &msgid1_len, &msgid2, &msgid2_len, &count) == FAILURE) {
243+
RETURN_THROWS();
244+
}
250245

251-
PHP_GETTEXT_DOMAIN_LENGTH_CHECK(1, ZSTR_LEN(domain))
252-
PHP_GETTEXT_LENGTH_CHECK(2, ZSTR_LEN(msgid1))
253-
PHP_GETTEXT_LENGTH_CHECK(3, ZSTR_LEN(msgid2))
246+
PHP_GETTEXT_DOMAIN_LENGTH_CHECK(1, domain_len)
247+
PHP_GETTEXT_LENGTH_CHECK(2, msgid1_len)
248+
PHP_GETTEXT_LENGTH_CHECK(3, msgid2_len)
254249

255-
msgstr = dngettext(ZSTR_VAL(domain), ZSTR_VAL(msgid1), ZSTR_VAL(msgid2), count);
250+
msgstr = dngettext(domain, msgid1, msgid2, count);
256251

257252
ZEND_ASSERT(msgstr);
258253
RETURN_STRING(msgstr);
@@ -264,26 +259,25 @@ PHP_FUNCTION(dngettext)
264259
/* {{{ Plural version of dcgettext() */
265260
PHP_FUNCTION(dcngettext)
266261
{
267-
char *msgstr = NULL;
268-
zend_string *domain, *msgid1, *msgid2;
262+
char *domain, *msgid1, *msgid2, *msgstr = NULL;
263+
size_t domain_len, msgid1_len, msgid2_len;
269264
zend_long count, category;
270265

271266
RETVAL_FALSE;
272267

273-
ZEND_PARSE_PARAMETERS_START(5, 5)
274-
Z_PARAM_STR(domain)
275-
Z_PARAM_STR(msgid1)
276-
Z_PARAM_STR(msgid2)
277-
Z_PARAM_LONG(count)
278-
Z_PARAM_LONG(category)
279-
ZEND_PARSE_PARAMETERS_END();
268+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "sssll", &domain, &domain_len,
269+
&msgid1, &msgid1_len, &msgid2, &msgid2_len, &count, &category) == FAILURE) {
270+
RETURN_THROWS();
271+
}
280272

281-
PHP_GETTEXT_DOMAIN_LENGTH_CHECK(1, ZSTR_LEN(domain))
282-
PHP_GETTEXT_LENGTH_CHECK(2, ZSTR_LEN(msgid1))
283-
PHP_GETTEXT_LENGTH_CHECK(3, ZSTR_LEN(msgid2))
284-
PHP_DCGETTEXT_CATEGORY_CHECK(5, category)
273+
PHP_GETTEXT_DOMAIN_LENGTH_CHECK(1, domain_len)
274+
PHP_GETTEXT_LENGTH_CHECK(2, msgid1_len)
275+
PHP_GETTEXT_LENGTH_CHECK(3, msgid2_len)
276+
if (category == LC_ALL) {
277+
RETURN_STRING(msgid1);
278+
}
285279

286-
msgstr = dcngettext(ZSTR_VAL(domain), ZSTR_VAL(msgid1), ZSTR_VAL(msgid2), count, category);
280+
msgstr = dcngettext(domain, msgid1, msgid2, count, category);
287281

288282
ZEND_ASSERT(msgstr);
289283
RETURN_STRING(msgstr);
@@ -296,18 +290,16 @@ PHP_FUNCTION(dcngettext)
296290
/* {{{ Specify the character encoding in which the messages from the DOMAIN message catalog will be returned. */
297291
PHP_FUNCTION(bind_textdomain_codeset)
298292
{
299-
char *retval = NULL;
300-
zend_string *domain, *codeset = NULL;
293+
char *domain, *codeset = NULL, *retval = NULL;
294+
size_t domain_len, codeset_len;
301295

302-
ZEND_PARSE_PARAMETERS_START(1, 2)
303-
Z_PARAM_STR(domain)
304-
Z_PARAM_OPTIONAL
305-
Z_PARAM_STR_OR_NULL(codeset)
306-
ZEND_PARSE_PARAMETERS_END();
296+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss!", &domain, &domain_len, &codeset, &codeset_len) == FAILURE) {
297+
RETURN_THROWS();
298+
}
307299

308-
PHP_GETTEXT_DOMAIN_LENGTH_CHECK(1, ZSTR_LEN(domain))
300+
PHP_GETTEXT_DOMAIN_LENGTH_CHECK(1, domain_len)
309301

310-
retval = bind_textdomain_codeset(ZSTR_VAL(domain), codeset ? ZSTR_VAL(codeset) : NULL);
302+
retval = bind_textdomain_codeset(domain, codeset);
311303

312304
if (!retval) {
313305
RETURN_FALSE;

0 commit comments

Comments
 (0)