Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PHP Unserialize Removing Object Property

I have code that is serializing a PDOException, sending it over the wire, and then unserializing it later on. When I unserialize it, the $code property appears to be missing. The rest of the object appears unchanged.

My code is running against a PostgreSQL database. Use the following DDL:

CREATE TABLE test (
    id INTEGER
);

Use the following code to reproduce my issue (substituting your own PostgeSQL connection values):

<?php

$dsn = "pgsql: dbname=postgres;host=/var/run/postgresql;port=5432";
$user = "postgres";
$password = "";
try
{
    $pdo = new PDO($dsn, $user, $password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $res = $pdo->exec("INSERT INTO test (id) VALUES (999999999999999)");
}
catch (PDOException $e)
{
    var_dump((array) $e);
    print "\n";
    print $e->getCode();
    print "\n";
    $s = serialize($e);
    print $s;
    print "\n";
    $d = unserialize($s);
    var_dump((array) $d);
    print "\n";
    print $d->getCode();
    print "\n";
    print serialize($e->getCode());
    print "\n";
}

?>

In my output, the $code property is missing from the final output. Additionally I get the following notice:

PHP Notice: Undefined property: PDOException::$code in /home/developer/test_serialize.php on line 20

I'm finding that I actually have to execute a failing SQL statement in order to see this issue. In particular if I pick, for example, the wrong port number, then I'll get a PDOException but it will retain the $code property after unserialize call.

Note that the serialized string appears to have the code property there, so I'm assuming this is an issue with the unserialize function.

Any insight would be appreciated - am I misunderstanding something fundamental here? Is this a PHP bug? Something else? I'm on the following PHP version:

PHP 7.1.6 (cli) (built: Jun 18 2018 12:25:10) ( ZTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies

Edit - Adding Print Output

The following is the output of the reproduction script. Note that I've modified is slightly to add some newlines for readability, and replacing print_r with var_dump:

array(8) {
  ["*message"]=>
  string(75) "SQLSTATE[22003]: Numeric value out of range: 7 ERROR:  integer out of range"
  ["Exceptionstring"]=>
  string(0) ""
  ["*code"]=>
  string(5) "22003"
  ["*file"]=>
  string(34) "/home/developer/test_serialize.php"
  ["*line"]=>
  int(10)
  ["Exceptiontrace"]=>
  array(1) {
    [0]=>
    array(6) {
      ["file"]=>
      string(34) "/home/developer/test_serialize.php"
      ["line"]=>
      int(10)
      ["function"]=>
      string(4) "exec"
      ["class"]=>
      string(3) "PDO"
      ["type"]=>
      string(2) "->"
      ["args"]=>
      array(1) {
        [0]=>
        string(73) "INSERT INTO km_role (role_id, role_name) VALUES (999999999999999, 'test')"
      }
    }
  }
  ["Exceptionprevious"]=>
  NULL
  ["errorInfo"]=>
  array(3) {
    [0]=>
    string(5) "22003"
    [1]=>
    int(7)
    [2]=>
    string(28) "ERROR:  integer out of range"
  }
}

22003
O:12:"PDOException":8:{s:10:"*message";s:75:"SQLSTATE[22003]: Numeric value out of range: 7 ERROR:  integer out of range";s:17:"Exceptionstring";s:0:"";s:7:"*code";s:5:"22003";s:7:"*file";s:34:"/home/developer/test_serialize.php";s:7:"*line";i:10;s:16:"Exceptiontrace";a:1:{i:0;a:6:{s:4:"file";s:34:"/home/developer/test_serialize.php";s:4:"line";i:10;s:8:"function";s:4:"exec";s:5:"class";s:3:"PDO";s:4:"type";s:2:"->";s:4:"args";a:1:{i:0;s:73:"INSERT INTO km_role (role_id, role_name) VALUES (999999999999999, 'test')";}}}s:19:"Exceptionprevious";N;s:9:"errorInfo";a:3:{i:0;s:5:"22003";i:1;i:7;i:2;s:28:"ERROR:  integer out of range";}}
array(7) {
  ["*message"]=>
  string(75) "SQLSTATE[22003]: Numeric value out of range: 7 ERROR:  integer out of range"
  ["Exceptionstring"]=>
  string(0) ""
  ["*file"]=>
  string(34) "/home/developer/test_serialize.php"
  ["*line"]=>
  int(10)
  ["Exceptiontrace"]=>
  array(1) {
    [0]=>
    array(6) {
      ["file"]=>
      string(34) "/home/developer/test_serialize.php"
      ["line"]=>
      int(10)
      ["function"]=>
      string(4) "exec"
      ["class"]=>
      string(3) "PDO"
      ["type"]=>
      string(2) "->"
      ["args"]=>
      array(1) {
        [0]=>
        string(73) "INSERT INTO km_role (role_id, role_name) VALUES (999999999999999, 'test')"
      }
    }
  }
  ["Exceptionprevious"]=>
  NULL
  ["errorInfo"]=>
  array(3) {
    [0]=>
    string(5) "22003"
    [1]=>
    int(7)
    [2]=>
    string(28) "ERROR:  integer out of range"
  }
}

PHP Notice:  Undefined property: PDOException::$code in /home/developer/test_serialize.php on line 24

s:5:"22003"

In the example where the PDOException is thrown via incorrect port number, the serialized $e->getCode() is:

i:7;
like image 756
Rob Gwynn-Jones Avatar asked Jul 02 '18 05:07

Rob Gwynn-Jones


2 Answers

Blackbam's answer is exceptional, but the PDO object being unserializable is a red herring. The problem with your code really is due to the type of the $code property, as discussed in the comments to his post. The exception is initialized with a string representation of the error code instead of an integer in some circumstances. This breaks deserialization, which very reasonably decides to discard properties with invalid types.

The comments on the PDOException documentation page are almost all talking about problems caused by the error code being created as a string instead of an int.

You can set the protected value to an integer using reflection. See below:

try
{
    $pdo = new PDO($dsn, $user, $password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $res = $pdo->exec("INSERT INTO test_schema.test (id) VALUES (999999999999999)");
}
catch (PDOException $e)
{
    // the new bit is here
    if (!is_int($e->getCode())) {
        $reflectionClass = new ReflectionClass($e);
        $reflectionProperty = $reflectionClass->getProperty('code');
        $reflectionProperty->setAccessible(true);
        $reflectionProperty->setValue($e, (int)$reflectionProperty->getValue($e));    
    }
    // the rest is the same
    var_dump((array) $e);
    print "\n";
    print $e->getCode();
    print "\n";
    $s = serialize($e);
    print $s;
    print "\n";
    $d = unserialize($s);
    var_dump((array) $d);
    print "\n";
    print $d->getCode();
    print "\n";
    print serialize($e->getCode());
    print "\n";
}

Of course, you will lose information if the code contains an alphanumeric value rather than an integer cast as a string. The message may duplicate the error number, but that may not be great to rely on.

The unserializable stack trace Blackbam is concerned with can become a problem if you tweak your code just a little bit:

function will_crash($pdo) {
    $res = $pdo->exec("INSERT INTO test_schema.test (id) VALUES (999999999999999)");
}

try
{
    $pdo = new PDO($dsn, $user, $password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    will_crash($pdo);
}
catch (PDOException $e)
{
    $s = serialize($e);
    // PHP Fatal error:  Uncaught PDOException: You cannot serialize or unserialize PDO instances in...
}

Whoops.

So Blackbam's answer, and the approach in his link of creating a serializable exception class, is probably the way to go. That allows you to serialize an exception's data but not the stack trace.

Then again, at that point you may as well just use json_encode and json_decode to pass around/store exception information.

like image 87
jstur Avatar answered Sep 28 '22 16:09

jstur


First of all have a look at the PDOException class:

PDOException extends RuntimeException {
  /* Properties */
  public array $errorInfo ;
  protected string $code ;

  /* Inherited properties */
  protected string $message ;
  protected int $code ;
  protected string $file ;
  protected int $line ;

  /* Inherited methods */
  final public string Exception::getMessage ( void )
  final public Throwable Exception::getPrevious ( void )
  final public mixed Exception::getCode ( void )
  final public string Exception::getFile ( void )
  final public int Exception::getLine ( void )
  final public array Exception::getTrace ( void )
  final public string Exception::getTraceAsString ( void )
  public string Exception::__toString ( void )
  final private void Exception::__clone ( void )
}

The getCode() method in PHP is implemented just like this (ref: https://github.com/php/php-src/blob/cd953269d3d486f775f1935731b1d6d44f12a350/ext/spl/spl.php):

/** @return the code passed to the constructor
 */
final public function getCode()
{
    return $this->code;
}

This is the constructor of a PHP Exception:

/** Construct an exception
 *
 * @param $message Some text describing the exception
 * @param $code    Some code describing the exception
 */
function __construct($message = NULL, $code = 0) {
    if (func_num_args()) {
        $this->message = $message;
    }
    $this->code = $code;
    $this->file = __FILE__; // of throw clause
    $this->line = __LINE__; // of throw clause
    $this->trace = debug_backtrace();
    $this->string = StringFormat($this);
}

What does that tell us? The $code property of an Exception can only be populated when the Exception is generated and it should be zero if no $code was passed.

So is this a PHP Bug? I guess not, after some research I have found the following great article: http://fabien.potencier.org/php-serialization-stack-traces-and-exceptions.html

In essence it says:

When PHP serializes an exception, it serializes the exception code, the exception message, but also the stack trace.

The stack trace is an array containing all functions and methods that have already been executed at this point of the script. The trace contains the file name, the line in the file, the function name, and an array of all arguments passed to the function. Do you spot the problem?

The stack trace contains a reference to the PDO instance, as it was passed to the will_crash() function, and as PDO instances are not serializable, an exception is thrown when PHP serializes the stack trace.

Whenever a non-serializable object is present in the stack trace, the exception won't be serializable.

And I guess this is the reason why our serialize()/unserialize() process is failing - because the Exception is not serializable.

Solution:

Write a Serializable Exception which extends Exception

class SerializableException extends Exception implements Serializable {
// ... go ahead :-)
}
like image 38
Blackbam Avatar answered Sep 28 '22 14:09

Blackbam