php源码阅读----php5.3.27 mysqli扩展bug及修复

    xiaoxiao2021-03-25  145

    最近在做一个功能,需要设置mysql查询超时。于是就使用mysqli的options(MYSQL_OPT_READ_TIMEOUT,5)或者options(11,5),但结果却出人意料。

    首先,我写了如下的测试代码:

    <?php $mysqli = mysqli_init(); $r = $mysqli->options(11, 1); var_dump($r); $mysqli->real_connect('10.0.3.25', 'test', 'test123', 'test'); $res = $mysqli->query("select sleep(5)"); if (!$res) { echo "query error: ". $mysqli->error ."\n"; } else { echo "Query: query success\n"; }

    然后在两台机器上执行上面的代码,结果如下: 机器a,php版本5.3.27 机器b,php版本5.2.17

    在机器b上,是成功的执行了超时设置,但机器a上并没有成功。于是我便怀疑是php5.3.27的mysqli扩展源码有问题。

    于是怀着一颗打破砂锅问到底的心,我打开了php5.3.27的源码。并找到mysqli_options代码(在src/ext/mysqli/mysqli_api.c),代码如下:

    PHP_FUNCTION(mysqli_options) { MY_MYSQL *mysql; zval *mysql_link = NULL; zval **mysql_value; long mysql_option; unsigned int l_value; long ret; int expected_type; if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, getThis(), "OlZ", &mysql_link, mysqli_link_class_entry, &mysql_option, &mysql_value) == FAILURE) { return; } MYSQLI_FETCH_RESOURCE_CONN(mysql, &mysql_link, MYSQLI_STATUS_INITIALIZED); #if PHP_API_VERSION < 20100412 if ((PG(open_basedir) && PG(open_basedir)[0] != '\0') || PG(safe_mode)) { #else if (PG(open_basedir) && PG(open_basedir)[0] != '\0') { #endif if(mysql_option == MYSQL_OPT_LOCAL_INFILE) { RETURN_FALSE; } } expected_type = mysqli_options_get_option_zval_type(mysql_option); if (expected_type != Z_TYPE_PP(mysql_value)) { switch (expected_type) { case IS_STRING: convert_to_string_ex(mysql_value); break; case IS_LONG: convert_to_long_ex(mysql_value); break; default: break; } } switch (expected_type) { case IS_STRING: ret = mysql_options(mysql->mysql, mysql_option, Z_STRVAL_PP(mysql_value)); break; case IS_LONG: l_value = Z_LVAL_PP(mysql_value); ret = mysql_options(mysql->mysql, mysql_option, (char *)&l_value); break; default: ret = 1; break; } RETURN_BOOL(!ret); }

    问题就出在这块代码:

    expected_type = mysqli_options_get_option_zval_type(mysql_option);

    其中mysql_option就是我们php代码里面向mysqil_options传的第一个参数。这段代码的作用是通过mysqli_options的第一个参数,得到第二个参数的类型。我们在看一下这个函数mysqli_options_get_option_zval_type(在src/ext/mysqli/mysqli_api.c),代码如下:

    static int mysqli_options_get_option_zval_type(int option) { switch (option) { #ifdef MYSQLI_USE_MYSQLND #if PHP_MAJOR_VERSION >= 6 case MYSQLND_OPT_NUMERIC_AND_DATETIME_AS_UNICODE: #endif case MYSQLND_OPT_NET_CMD_BUFFER_SIZE: case MYSQLND_OPT_NET_READ_BUFFER_SIZE: #ifdef MYSQLND_STRING_TO_INT_CONVERSION case MYSQLND_OPT_INT_AND_FLOAT_NATIVE: #endif #endif /* MYSQLI_USE_MYSQLND */ case MYSQL_OPT_CONNECT_TIMEOUT: #ifdef MYSQL_REPORT_DATA_TRUNCATION case MYSQL_REPORT_DATA_TRUNCATION: #endif case MYSQL_OPT_LOCAL_INFILE: case MYSQL_OPT_NAMED_PIPE: #ifdef MYSQL_OPT_PROTOCOL case MYSQL_OPT_PROTOCOL: #endif /* MySQL 4.1.0 */ #ifdef MYSQL_OPT_READ_TIMEOUT case MYSQL_OPT_READ_TIMEOUT: case MYSQL_OPT_WRITE_TIMEOUT: case MYSQL_OPT_GUESS_CONNECTION: case MYSQL_OPT_USE_EMBEDDED_CONNECTION: case MYSQL_OPT_USE_REMOTE_CONNECTION: case MYSQL_SECURE_AUTH: #endif /* MySQL 4.1.1 */ #ifdef MYSQL_OPT_RECONNECT case MYSQL_OPT_RECONNECT: #endif /* MySQL 5.0.13 */ #ifdef MYSQL_OPT_SSL_VERIFY_SERVER_CERT case MYSQL_OPT_SSL_VERIFY_SERVER_CERT: #endif /* MySQL 5.0.23 */ #ifdef MYSQL_OPT_COMPRESS case MYSQL_OPT_COMPRESS: #endif /* mysqlnd @ PHP 5.3.2 */ #ifdef MYSQL_OPT_SSL_VERIFY_SERVER_CERT REGISTER_LONG_CONSTANT("MYSQLI_OPT_SSL_VERIFY_SERVER_CERT", MYSQL_OPT_SSL_VERIFY_SERVER_CERT, CONST_CS | CONST_PERSISTENT); #endif /* MySQL 5.1.1., mysqlnd @ PHP 5.3.3 */ return IS_LONG; #ifdef MYSQL_SHARED_MEMORY_BASE_NAME case MYSQL_SHARED_MEMORY_BASE_NAME: #endif /* MySQL 4.1.0 */ #ifdef MYSQL_SET_CLIENT_IP case MYSQL_SET_CLIENT_IP: #endif /* MySQL 4.1.1 */ case MYSQL_READ_DEFAULT_FILE: case MYSQL_READ_DEFAULT_GROUP: case MYSQL_INIT_COMMAND: case MYSQL_SET_CHARSET_NAME: case MYSQL_SET_CHARSET_DIR: return IS_STRING; default: return IS_NULL; } }

    可以看到,这个函数其实挺简单的,就是一个switch case判断传进来的参数,然后返回相应的值。但问题出现了,有这么一段代码:

    #ifdef MYSQL_OPT_READ_TIMEOUT case MYSQL_OPT_READ_TIMEOUT: case MYSQL_OPT_WRITE_TIMEOUT: case MYSQL_OPT_GUESS_CONNECTION: case MYSQL_OPT_USE_EMBEDDED_CONNECTION: case MYSQL_OPT_USE_REMOTE_CONNECTION: case MYSQL_SECURE_AUTH: #endif /* MySQL 4.1.1 */

    当我们传入的参数是MYSQL_OPT_READ_TIMEOUT (或11)时,需要定义MYSQL_OPT_READ_TIMEOUT这个宏,才会返回IS_LONG,不然只会执行default,返回IS_NULL。如果返回IS_NULL,mysqli_options函数之后的执行逻辑如下:

    expected_type = mysqli_options_get_option_zval_type(mysql_option); // expected_type的值是IS_NULL,所以下面的switch执行default。 if (expected_type != Z_TYPE_PP(mysql_value)) { switch (expected_type) { case IS_STRING: convert_to_string_ex(mysql_value); break; case IS_LONG: convert_to_long_ex(mysql_value); break; default: break; } } // 到这里expected_type的值还是IS_NULL,所以还是执行default,所以并没有执行mysql_options这个函数 switch (expected_type) { case IS_STRING: ret = mysql_options(mysql->mysql, mysql_option, Z_STRVAL_PP(mysql_value)); break; case IS_LONG: l_value = Z_LVAL_PP(mysql_value); ret = mysql_options(mysql->mysql, mysql_option, (char *)&l_value); break; default: ret = 1; break; } RETURN_BOOL(!ret);

    解决方案,定义MYSQL_OPT_READ_TIMEOUT这个宏。另一个方案:使用php5.2.17的做法,不对这个参数已经判断。这个做法就会导致使用mysqli_options时,不能乱传参数,因为没有做过滤。 我这边使用方案二,修改后的代码如下:

    PHP_FUNCTION(mysqli_options) { MY_MYSQL *mysql; zval *mysql_link = NULL; zval **mysql_value; long mysql_option; unsigned int l_value; long ret; if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, getThis(), "OlZ", &mysql_link, mysqli_link_class_entry, &mysql_option, &mysql_value) == FAILURE) { return; } MYSQLI_FETCH_RESOURCE(mysql, MY_MYSQL *, &mysql_link, "mysqli_link", MYSQLI_STATUS_INITIALIZED); if ((PG(open_basedir) && PG(open_basedir)[0] != '\0') || PG(safe_mode)) { if(mysql_option == MYSQL_OPT_LOCAL_INFILE) { RETURN_FALSE; } } switch (Z_TYPE_PP(mysql_value)) { case IS_STRING: ret = mysql_options(mysql->mysql, mysql_option, Z_STRVAL_PP(mysql_value)); break; default: convert_to_long_ex(mysql_value); l_value = Z_LVAL_PP(mysql_value); ret = mysql_options(mysql->mysql, mysql_option, (char *)&l_value); break; } RETURN_BOOL(!ret); }

    修改完,如果mysqli在编译php的时候,是编译进php的,那需要重新编译一下php。如果时候通过so这种外部链接的,那就只需要重新编译一下mysqli扩展就可以了。

    转载请注明原文地址: https://ju.6miu.com/read-5848.html

    最新回复(0)