PHP内核之zend_try_ct_eval_const

我们来看看这么一段代码:

1
2
3
4
<?php // other.php
namespace foo {
var_dump(true);
}

执行结果如下:

1
2
php other.php
bool(true)

还算是符合我们的预期对吧。

我们再来看看这段代码:

1
2
3
<?php // test.php
define('foo\true', 'test');
require_once __DIR__ . '/other.php';

执行结果如下:

1
2
php test.php
string(4) "test"

因为在其他文件里面定义了foo\true常量,导致引入other.php文件的时候,true的值就被无情的修改了。

问题是出在了zend_try_ct_eval_const这个函数里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static bool zend_try_ct_eval_const(zval *zv, zend_string *name, bool is_fully_qualified) /* {{{ */
{
zend_constant *c = zend_hash_find_ptr(EG(zend_constants), name);
if (c && can_ct_eval_const(c)) {
ZVAL_COPY_OR_DUP(zv, &c->value);
return 1;
}

{
/* Substitute true, false and null (including unqualified usage in namespaces) */
const char *lookup_name = ZSTR_VAL(name);
size_t lookup_len = ZSTR_LEN(name);

if (!is_fully_qualified) {
zend_get_unqualified_name(name, &lookup_name, &lookup_len);
}

if ((c = zend_get_special_const(lookup_name, lookup_len))) {
ZVAL_COPY_VALUE(zv, &c->value);
return 1;
}

return 0;
}
}

可以看到,前面先调用can_ct_eval_const来判断常量是否能够被替换。因为我们这里定义了foo\true常量,所以这里就判断能够被替换。所以这里拿到的值就是test了。

PHP8中,这被当作了BUG来处理,解决方法也很简单,把zend_get_special_const放到can_ct_eval_const前面即可。special_const的值有三个,truefalsenull

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
s`tatic bool zend_try_ct_eval_const(zval *zv, zend_string *name, bool is_fully_qualified) /* {{{ */
{
/* Substitute true, false and null (including unqualified usage in namespaces)
* before looking up the possibly namespaced name. */
const char *lookup_name = ZSTR_VAL(name);
size_t lookup_len = ZSTR_LEN(name);

if (!is_fully_qualified) {
zend_get_unqualified_name(name, &lookup_name, &lookup_len);
}

zend_constant *c;
if ((c = zend_get_special_const(lookup_name, lookup_len))) {
ZVAL_COPY_VALUE(zv, &c->value);
return 1;
}
c = zend_hash_find_ptr(EG(zend_constants), name);
if (c && can_ct_eval_const(c)) {
ZVAL_COPY_OR_DUP(zv, &c->value);
return 1;
}
return 0;
}`