Segmentation Fault en PHP con phpAds 0
Recientemente configuramos un servidor de desarrollo local para que nuestros programadores tuvieran acceso a los ficheros de forma más cómoda y transparente y, al mismo tiempo, minimizar la utilización del ancho de banda internacional.
Al principio todo andaba bien pero, de un momento al otro, se comenzaron a producir algunos problemas cuando se intentaba acceder a los scripts PHP de un determinado proyecto. PHP producia una violación de segmento – o acceso – y finalizaba la aplicación.
El volcado con gdb era el siguiente:
# gdb /usr/local/apache2/bin/httpd core.25582 [...] Reading symbols from /usr/lib/php/modules/mysql.so...done. Loaded symbols for /usr/lib/php/modules/mysql.so Reading symbols from /usr/lib/php/modules/mysqli.so...done. Loaded symbols for /usr/lib/php/modules/mysqli.so Reading symbols from /usr/lib/php/modules/pdo.so...done. Loaded symbols for /usr/lib/php/modules/pdo.so Reading symbols from /usr/lib/php/modules/pdo_mysql.so...done. Loaded symbols for /usr/lib/php/modules/pdo_mysql.so Reading symbols from /usr/lib/php/modules/pdo_sqlite.so...done. Loaded symbols for /usr/lib/php/modules/pdo_sqlite.so Reading symbols from /lib/librt.so.1...done. Loaded symbols for /lib/librt.so.1 Core was generated by `/usr/sbin/httpd'. Program terminated with signal 11, Segmentation fault. [New process 9713] #0 0x001a2c39 in vfprintf () from /lib/libc.so.6
Ahora vemos el backtrace:
(gdb) bt full
#0 0x00466bf3 in vfprintf () from /lib/libc.so.6
No symbol table info available.
#1 0x0048589c in vsprintf () from /lib/libc.so.6
No symbol table info available.
#2 0x00470efe in sprintf () from /lib/libc.so.6
No symbol table info available.
#3 0x01027e18 in vio_new (sd=-1087675456, type=VIO_TYPE_SOCKET, flags=3) at vio.c:146
vio = (Vio *) 0x9d6254c
#4 0x01024dc9 in mysql_real_connect (mysql=0xa313ec8, host=0x9e20578 "localhost", user=0x9e45e24 "webuser", passwd=0x9e45e50 "xxxxxxxx", db=0x0, port=3306, unix_socket=0x102a422 "/var/lib/mysql/mysql.sock", client_flag=0) at client.c:1963
buff = '\0' , "QKI\000\000\000\000\000\000\000\000\000\002\b6\001", '\0' , "do+¿pÑV\000\003", '\0' , "s", '\0'
end =
host_info = 0x102a552 "Localhost via UNIX socket"
sock = 48
ip_addr =
sock_addr = {sin_family = 16072, sin_port = 2609, sin_addr = {s_addr = 3207294824}, sin_zero = "±!\002\001\035A1\n"}
pkt_length =
UNIXaddr = {sun_family = 0,
sun_path = '\0' , "Á\204E\000\000\000\000\000\000\000\000\000\002", '\0' , "äo+¿^Î3\001tèð¶\000\000\000\000\231\231\231\031\005", '\0' , "pÍÿ\000À>1\nÄ\003\000\000ho+¿\000\000\000\000pF\020\001"}
old_signal_handler = (sig_return) 0
#5 0x00a31ebb in php_mysql_do_connect (ht=, return_value=0xa313eb0, return_value_ptr=, this_ptr=0x0, return_value_used=1, persistent=0) at /home/brewbuilder/rpms/BUILD/php-5.2.11/ext/mysql/php_mysql.c:754
index_ptr = (zend_rsrc_list_entry *) 0xa313ea8
new_index_ptr = {ptr = 0x18, type = 20, refcount = 12}
user = 0x9e45e24 "webuser"
passwd = 0x9e45e50 "xxxxxxxxxx"
host_and_port = 0xa313e98 "localhost:3306"
socket = 0x0
tmp =
host = 0x9e20578 "localhost"
user_len = 11
passwd_len = 12
host_len = 14
hashed_details = 0x9e1da44 "mysql_localhost:3306_webuser_xxxxxxxx"
hashed_details_length = 47
port = 3306
client_flags = 0
mysql =
free_host = 1 '\001'
new_link = 0 '\0'
connect_timeout = 60
#6 0x013866a0 in zend_do_fcall_common_helper_SPEC (execute_data=0xbf2b742c) at /home/brewbuilder/rpms/BUILD/php-5.2.11/Zend/zend_vm_execute.h:200
return_reference = 0 '\0'
opline = (zend_op *) 0x9e5ce58
original_return_value =
current_scope = (zend_class_entry *) 0x0
current_this = (zval *) 0x0
should_change_scope = 0 '\0'
#7 0x01379d5d in execute (op_array=0x9d60048) at /home/brewbuilder/rpms/BUILD/php-5.2.11/Zend/zend_vm_execute.h:92
execute_data = {opline = 0x9e5ce58, function_state = {function_symbol_table = 0x4, function = 0x9bc1638, reserved = {0xa313db0, 0x133d6b2, 0xa313db0, 0x0}}, fbc = 0x0, op_array = 0x9d60048, object = 0x0, Ts = 0xbf2b7110, CVs = 0xbf2b70f0,
original_in_execution = 1 '\001', symbol_table = 0xa313d80, prev_execute_data = 0xbf2b764c, old_error_reporting = 0xbf2b7318}
#8 0x0138606e in zend_do_fcall_common_helper_SPEC (execute_data=0xbf2b764c) at /home/brewbuilder/rpms/BUILD/php-5.2.11/Zend/zend_vm_execute.h:234
opline = (zend_op *) 0x9e5d880
original_return_value = (zval **) 0xbf2b7a38
current_scope = (zend_class_entry *) 0x0
current_this = (zval *) 0x0
should_change_scope = 1 '\001'
#9 0x01379d5d in execute (op_array=0x9d601a0) at /home/brewbuilder/rpms/BUILD/php-5.2.11/Zend/zend_vm_execute.h:92
execute_data = {opline = 0x9e5d880, function_state = {function_symbol_table = 0xa313d80, function = 0x9d60048, reserved = {0xa313ca8, 0x133d6b2, 0xa313ca8, 0x0}}, fbc = 0x0, op_array = 0x9d601a0, object = 0x0, Ts = 0xbf2b7580,
CVs = 0xbf2b7560, original_in_execution = 1 '\001', symbol_table = 0xa313c78, prev_execute_data = 0xbf2b7abc, old_error_reporting = 0x0}
#10 0x0138606e in zend_do_fcall_common_helper_SPEC (execute_data=0xbf2b7abc) at /home/brewbuilder/rpms/BUILD/php-5.2.11/Zend/zend_vm_execute.h:234
opline = (zend_op *) 0x9e5d06c
original_return_value = (zval **) 0xbf2b7c50
current_scope = (zend_class_entry *) 0x0
current_this = (zval *) 0x0
should_change_scope = 1 '\001'
Bueno, ya tenemos un indicio del problema. La función que nos devulve la traza es mysql_real_connect(), pero… ¿En qué parte de nuestro código se produce esta llamada y por qué provoca segmentation fault?
Dentro del proyecto en concreto, estamos utilizando phpAds para servir los banners.
Si la violación se produce justo en la llamada a mysql_real_connect(), entonces se debe estár originando en el script que realiza la conexión a la base de datos. Aunque esto parezca obvio, no lo es tanto cuando se tienen muchisimas de líneas de código.
Verificando el script me doy cuenta de que el nombre de usuario de conexión a la DB es incorrecto: algún desarrollador lo ha cambiado sin malas intenciones.
¿Pero esto no debería generar un error MYSQL 1044 (ER_DBACCESS_DENIED_ERROR) en lugar de segmentation fault?
Veamos el código de conexión:
function phpAds_dbConnect()
{
global $phpAds_config;
global $phpAds_db_link;
// Add port to connect, if needed
if (!isset($phpAds_config['dbport']) || !$phpAds_config['dbport'])
$phpAds_config['dbport'] = 3306;
$host = $phpAds_config['dbhost'];
if ((!isset($phpAds_config['dblocal']) || !$phpAds_config['dblocal']) && $host{0} != ':')
$host .= ':'.$phpAds_config['dbport'];
if ($phpAds_config['persistent_connections'])
$phpAds_db_link = @mysql_pconnect ($host, $phpAds_config['dbuser'], $phpAds_config['dbpassword']);
else
$phpAds_db_link = @mysql_connect ($host, $phpAds_config['dbuser'], $phpAds_config['dbpassword']);
if ($phpAds_config['mysql4_compatibility'])
phpAds_dbQuery("SET SESSION sql_mode='MYSQL40'");
if ($phpAds_config['compatibility_mode'])
return $phpAds_db_link;
if (@mysql_select_db ($phpAds_config['dbname'], $phpAds_db_link))
return $phpAds_db_link;
}
[...]
function phpAds_dbQuery($query)
{
global $phpAds_last_query;
global $phpAds_db_link;
// Connect to the database, if needed
if (!$phpAds_db_link && !phpAds_dbConnect())
return false;
$phpAds_last_query = $query;
return @mysql_query ($query, $phpAds_db_link);
}
Como podemos ver, si $phpAds_config['mysql4_compatibility'] es TRUE, entonces se realiza a una llamada a la función $phpAds_dbQuery(). El problema surge en cuanto la conexión no ha sido establecida, ya que se volverá a llamar a la función $phpAds_dbConnect() desde $phpAds_dbQuery() entrando en un loop infinito hasta producir una violación de acceso.
La solución
Eliminar el bucle infinito mediante una comprobación antes de realizar la llamada a la función que realiza el query.
if ($phpAds_config['mysql4_compatibility'] && $phpAds_db_link)
phpAds_dbQuery("SET SESSION sql_mode='MYSQL40'");
Conclusión
Al utilizar gdb y para realizar el backtrace del coredump, podemos tener un volcado minucioso del error y saber exacto qué es lo que debemos buscar.