ID:               48745
 Updated by:       theta...@php.net
 Reported By:      theta...@php.net
 Status:           Verified
 Bug Type:         MySQL related
 Operating System: *
 PHP Version:      5.3.0
 Assigned To:      mysql
 New Comment:

Thanks! I understand the problem and that it is deprecated. The
important thing is: it should *not* SIGSEGV. So the best idea would be
to simply disable the whole function, if it is not working with mysqlnd
and you are not willing to support it (something like: "deprecated
functions work with libmysqlclient but not with mysqlnd". They should
simply return false or throw an error or should removed at all). Because
of the sigsegv it was hard to find out, where the error really occurred
in this thousands of lines of foreign PHP code.

But if Andrew is able to fix it, let's wait for it.

> compile ext/mysql against libmysql (like ever since) and try
> again in four weeks

My problem was, that this does not work easily with PHP 32bit on
Solaris x64 using /opt/csw libs (mysql_config only returns 64bit libs,
includes strange not GCC compatible CFLAGS and so on). But this is
another problem. It was also broken in 5.2 (but you were able to fix
it), but with 5.3 it now produces hard compilation errors (and can only
be fixed by replacing mysql_config with a "dummy" that returns correct
CFLAGS and LIB paths). But Linux users can always compile against
libmysql this.

For Solaris users mysqlnd is the best for problem-less installation!

The easiest workaround was to use mysqli in our case :) For me this bug
is obsolete, I only want to keep it open because of the SIGSEGV.

Uwe


Previous Comments:
------------------------------------------------------------------------

[2009-07-02 07:01:42] u...@php.net

Thanks. As said, its fixable, I know the cause, I may know a hack to
fix it but the call is deprecated.  There are zillions of MySQL 4.0
users but we don't support MySQL 4.0 any more. There may be many
list_fields users, but list_fields is deprecated. 

At some point you simply have to stop giving old, deprecated calls the
highest priority.

Apart from that, I don't want to hack with the packet decoders in the
absence of Andrey if the workaround is as easy as: compile ext/mysql
against libmysql (like ever since) and try again in four weeks.

Ulf

------------------------------------------------------------------------

[2009-07-01 17:14:33] theta...@php.net

As I noted before: This is not my code and there may be a lot of users
installing software using the old mysql extension and also this
function.

For me the workaround was to enable the mysqli configuration option in
Contenido. The only problem is, that it is not enabled per default in
this CMS (although it says: PHP 5 only). Contenido uses a PHP-written
database abstraction (from where I copied the functions in this bug
report) and it was easy to exchange by the other one. It was also
fixable by doing a dummy query "select * from table limit 1" and looking
into the field descriptions for the result set (this is how the
abstraction layer fr mysqli does it in Contenido).

So many thanks for looking at it, I will post a note in the Contenido
mailing list about this problem and the workaround.

------------------------------------------------------------------------

[2009-07-01 16:54:38] u...@php.net

The trouble goes back to an over optimized COM_LIST_FIELDS
communication package and a guess that mysqlnd makes. mysqlnd makes a
guess on the number of fields that the deprecated API call
mysql_list_fields() may return. The guess is never adjusted to the
actual number of fields returned by the Server. As a result
mysql_num_fields(), when invoked with a result set handle returned by
mysql_list_fields(), may return a faulty number of fields. If a users
tries to loop over all fields using the number of fields returned by
mysql_num_fields() as a stop condition, he may try to access fields that
don't exist. The out-of-bound access is not detected because the
internal security check also gets the wrong number of fields reported. 

Workaround:

 - don't use deprecated functions, don't use mysql_list_fields()..
(there is a reason why we want to deprecate COM_LIST_FIELDS!)
 - use libmysql until its fixed


I don't want to play with the packet decoders and the protocol decoding
in the absence of Andrey. I know roughly how to fix, but packet decoding
is for Andrey. Andrey is on vacation until August.

As its easy to work around, I think it can wait.

------------------------------------------------------------------------

[2009-06-30 20:10:52] theta...@php.net

Here is code to reproduce the bug, it crashes CLI, too:

Just create any database and a table in mysql and run this code on it:

the problematic function is mysql_list_fields() which is deprecated and
does no longer seem to work correct with mysql_field_*() functions.
After I cahnged the code in the Contenido database abstraction to
instead create a dummy query (select * from table limit 1), like it was
done in the Contenido database code for mysqli, it works also with
mysql.

Maybe, if mysqlnd does not support this, mysql_list_fields() should
simply do the same dummy command.

<?php
$res = array();
mysql_connect('localhost', 'contenido', 'contenido');
$id = mysql_list_fields('contenido', 'con_data');
$count = @mysql_num_fields($id);

for ($i=0; $i<$count; $i++) {
  $res[$i]["table"] = @mysql_field_table ($id, $i);
  $res[$i]["name"]  = @mysql_field_name  ($id, $i);
  $res[$i]["type"]  = @mysql_field_type  ($id, $i);
  $res[$i]["len"]   = @mysql_field_len   ($id, $i);
  $res[$i]["flags"] = @mysql_field_flags ($id, $i);
}
mysql_free_result($id);

print_r($res);
?>


------------------------------------------------------------------------

[2009-06-30 18:11:17] theta...@php.net

Hi Johannes,

I am still working on a simple test script. But what I can say is, that
when I changed Contenido's config to use mysqli, it does not crash
anymore.

I found the place where the mysql_field_table is called, but I cannot
see any problems there (this is from the included conlib):

  /* public: return table metadata */
  function metadata($table = "", $full = false) {
    $count = 0;
    $id    = 0;
    $res   = array();

    /*
     * Due to compatibility problems with Table we changed the
behavior
     * of metadata();
     * depending on $full, metadata returns the following values:
     *
     * - full is false (default):
     * $result[]:
     *   [0]["table"]  table name
     *   [0]["name"]   field name
     *   [0]["type"]   field type
     *   [0]["len"]    field length
     *   [0]["flags"]  field flags
     *
     * - full is true
     * $result[]:
     *   ["num_fields"] number of metadata records
     *   [0]["table"]  table name
     *   [0]["name"]   field name
     *   [0]["type"]   field type
     *   [0]["len"]    field length
     *   [0]["flags"]  field flags
     *   ["meta"][field name]  index of field named "field name"
     *   This last one could be used if you have a field name, but no
index.
     *   Test:  if (isset($result['meta']['myfield'])) { ...
     */

    // if no $table specified, assume that we are working with a query
    // result
    if ($table) {
      $this->connect();
      $id = @mysql_list_fields($this->Database, $table);
      if (!$id) {
        $this->halt("Metadata query failed.");
        return false;
      }
    } else {
      $id = $this->Query_ID;
      if (!$id) {
        $this->halt("No query specified.");
        return false;
      }
    }

    $count = @mysql_num_fields($id);

    // made this IF due to performance (one if is faster than $count
if's)
    if (!$full) {
      for ($i=0; $i<$count; $i++) {
        $res[$i]["table"] = @mysql_field_table ($id, $i);
        $res[$i]["name"]  = @mysql_field_name  ($id, $i);
        $res[$i]["type"]  = @mysql_field_type  ($id, $i);
        $res[$i]["len"]   = @mysql_field_len   ($id, $i);
        $res[$i]["flags"] = @mysql_field_flags ($id, $i);
      }
    } else { // full
      $res["num_fields"]= $count;

      for ($i=0; $i<$count; $i++) {
        $res[$i]["table"] = @mysql_field_table ($id, $i);
        $res[$i]["name"]  = @mysql_field_name  ($id, $i);
        $res[$i]["type"]  = @mysql_field_type  ($id, $i);
        $res[$i]["len"]   = @mysql_field_len   ($id, $i);
        $res[$i]["flags"] = @mysql_field_flags ($id, $i);
        $res["meta"][$res[$i]["name"]] = $i;
      }
    }

    // free the result only if we were called on a table
    if ($table) {
      @mysql_free_result($id);
    }
    return $res;
  }

The same code in the other mysqli looks like this and works:

        /* public: return table metadata */
        function metadata($table = "", $full = false)
        {
                global $mysqli_type;

                $count = 0;
                $id = 0;
                $res = array ();

                /*
                 * Due to compatibility problems with Table we changed the 
behavior
                 * of metadata();
                 * depending on $full, metadata returns the following values:
                 *
                 * - full is false (default):
                 * $result[]:
                 *   [0]["table"]  table name
                 *   [0]["name"]   field name
                 *   [0]["type"]   field type
                 *   [0]["len"]    field length
                 *   [0]["flags"]  field flags
                 *
                 * - full is true
                 * $result[]:
                 *   ["num_fields"] number of metadata records
                 *   [0]["table"]  table name
                 *   [0]["name"]   field name
                 *   [0]["type"]   field type
                 *   [0]["len"]    field length
                 *   [0]["flags"]  field flags
                 *   ["meta"][field name]  index of field named "field name"
                 *   This last one could be used if you have a field name, but 
no
index.
                 *   Test:  if (isset($result['meta']['myfield'])) { ...
                 */

                // if no $table specified, assume that we are working with a 
query
                // result
                if ($table)
                {
                        $this->connect();
                        $id = mysqli_query($this->Link_ID, sprintf("SELECT * 
FROM %s LIMIT
1", $table));
                        if (!$id)
                        {
                                $this->halt("Metadata query failed.");
                                return false;
                        }
                } else
                {
                        $id = $this->Query_ID;
                        if (!$id)
                        {
                                $this->halt("No query specified.");
                                return false;
                        }
                }

                $count = mysqli_num_fields($id);

                // made this IF due to performance (one if is faster than $count
if's)
                if (!$full)
                {
                        for ($i = 0; $i < $count; $i ++)
                        {
                                $finfo = mysqli_fetch_field($id);
                                $res[$i]["table"] = $finfo->table;
                                $res[$i]["name"] = $finfo->name;
                                $res[$i]["type"] = $mysqli_type[$finfo->type];
                                $res[$i]["len"] = $finfo->max_length;
                                $res[$i]["flags"] = $finfo->flags;
                        }
                } else
                { // full
                        $res["num_fields"] = $count;

                        for ($i = 0; $i < $count; $i ++)
                        {
                                $finfo = mysqli_fetch_field($id);
                                $res[$i]["table"] = $finfo->table;
                                $res[$i]["name"] = $finfo->name;
                                $res[$i]["type"] = $finfo->type;
                                $res[$i]["len"] = $finfo->max_length;
                                $res[$i]["flags"] = $finfo->flags;
                                $res["meta"][$res[$i]["name"]] = $i;
                        }
                }

                // free the result only if we were called on a table
                if ($table)
                {
                        mysqli_free_result($id);
                }
                return $res;
        }


You can download the whole project Contenido from www.contenido.org (it
is quite famous in Germany).

Just install and try to login using the default setting (mysql database
extension) and mysqlnd installed.

Uwe

------------------------------------------------------------------------

The remainder of the comments for this report are too long. To view
the rest of the comments, please view the bug report online at
    http://bugs.php.net/48745

-- 
Edit this bug report at http://bugs.php.net/?id=48745&edit=1

Reply via email to