Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:118827 Return-Path: Delivered-To: mailing list internals@lists.php.net Received: (qmail 45441 invoked from network); 15 Oct 2022 21:37:04 -0000 Received: from unknown (HELO php-smtp4.php.net) (45.112.84.5) by pb1.pair.com with SMTP; 15 Oct 2022 21:37:04 -0000 Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 16AF6180054 for ; Sat, 15 Oct 2022 14:37:04 -0700 (PDT) X-Spam-Checker-Version: SpamAssassin 3.4.2 (2018-09-13) on php-smtp4.php.net X-Spam-Level: X-Spam-Status: No, score=-2.1 required=5.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,SPF_HELO_NONE,SPF_PASS, T_SCC_BODY_TEXT_LINE autolearn=no autolearn_force=no version=3.4.2 X-Spam-ASN: AS24940 176.9.0.0/16 X-Spam-Virus: No X-Envelope-From: Received: from chrono.xqk7.com (chrono.xqk7.com [176.9.45.72]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by php-smtp4.php.net (Postfix) with ESMTPS for ; Sat, 15 Oct 2022 14:37:03 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=bastelstu.be; s=mail20171119; t=1665869822; bh=5GelssGntxrXMtwrJD3x29rZwA/gpO12fCx4uPyGn6I=; h=Date:From:Subject:To:Cc:References:In-Reply-To:From; b=Lw1O+8DK588HcGeUYI2bdPw6sPuxeaEeG77dfwTmuoxhw4f7bWoWDA9kbhAbejM1C eWUQKvBfbg2P+ZDbU4cOMFsMTWCsc1QKQbrwKf7Cu0l1Cpyu5GVqUiyMpg0jBx5DPD Z3eDC/2k3KMkW/zWRyMUe+acPGvHoIxYPmuMZVfIW+qrec6GU3dbIL12zCByXmItZS BnDWs0pTLH2hBIj1DrnBwKL5myf3Vd3T8+sGmaPtND8iYQ1kuhjDTc2mY9aR0kqKLa EzAeCR0QiuHLpgypL3g1LRbf7gMYVgMio9KyajjdHrAEEwU/1wjJZEYAmGEpr9bxEw c2/zZhGuzAiXA== Message-ID: Date: Sat, 15 Oct 2022 23:37:01 +0200 MIME-Version: 1.0 To: Nicolas Grekas Cc: PHP internals References: <22177032-fe72-c39b-63fe-fa4368a70852@bastelstu.be> Content-Language: en-US In-Reply-To: Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 8bit Subject: Re: [PHP-DEV] [VOTE] Improve unserialize() error handling From: tim@bastelstu.be (=?UTF-8?Q?Tim_D=c3=bcsterhus?=) Hi Resending the message without attachments, because the mailing list rejected the original due to exceeding 30000 bytes: > ezmlm-reject: fatal: Sorry, I don't accept messages larger than 30000 bytes (#5.2.3) You can find the attachments at: https://gist.github.com/TimWolla/376dd162f7684daef38f76a07254871c Original message below: ------------------ On 10/15/22 11:06, Nicolas Grekas wrote: >> I'm not surprised by the “no” on the first vote based on the previous >> discussion. I am surprised however that you also disagree with raising >> the E_NOTICE to E_WARNING for consistency. >> >> I don't plan on repeating all the previous discussion points with you, >> but as you mention the Symfony tests specifically: Please find the patch >> attached. Do you believe the expectations in this new test would a >> reasonable expectation by a Symfony user? >> > > Since the beginning my point is not that the RFC doesn't have merits. It's > that the proposed approach breaks BC in a way that will affect the > community significantly. We have policies that say we should avoid BC > breaks and they should apply here. > > To anyone wondering about the reality of the BC break if you're not > convinced there is one because the original code is broken anyway (which is > your main argument Tim IIUC): please have a look at the failures I reported Yes, you understood this correctly: I consider the existing implementation to be broken. Unfortunately you did not answer my question whether the attached patch would be a reasonable expectation by a Symfony user. Let's presume it is: If I use the 'PhpSerializer' then I expect to receive exactly the 'MessageDecodingFailedException' whenever the message fails to decode. When decoding an erroneous DateTime (or an erroneous SplDoublyLinkedList or whatever) a different Exception will be thrown and not caught, thus the contract by the 'PhpSerializer' is violated. Let's fix this using test driven development. We first add the failing test cases. This is step1.diff (patches are in https://gist.github.com/TimWolla/376dd162f7684daef38f76a07254871c). When running the tests we get the following output: > There were 2 failures: > > 1) Symfony\Component\Messenger\Tests\Transport\Serialization\PhpSerializerTest::testDecodingFailsWithBadDateTimeData > Failed asserting that exception of type "Error" matches expected exception "Symfony\Component\Messenger\Exception\MessageDecodingFailedException". Message was: "Invalid serialization data for DateTime object" at > /app/src/Symfony/Component/Messenger/Transport/Serialization/PhpSerializer.php:95 > /app/src/Symfony/Component/Messenger/Transport/Serialization/PhpSerializer.php:54 > /app/src/Symfony/Component/Messenger/Tests/Transport/Serialization/PhpSerializerTest.php:99 > . > > 2) Symfony\Component\Messenger\Tests\Transport\Serialization\PhpSerializerTest::testDecodingFailsWithBadDoublyLinkedListData > Failed asserting that exception of type "UnexpectedValueException" matches expected exception "Symfony\Component\Messenger\Exception\MessageDecodingFailedException". Message was: "Incomplete or ill-typed serialization data" at > /app/src/Symfony/Component/Messenger/Transport/Serialization/PhpSerializer.php:95 > /app/src/Symfony/Component/Messenger/Transport/Serialization/PhpSerializer.php:54 > /app/src/Symfony/Component/Messenger/Tests/Transport/Serialization/PhpSerializerTest.php:110 > . The tests fails (as expected) and we need to fix the code. To fix this we add a new 'catch(Throwable)' to the existing 'try'. Within the catch we throw a new 'MessageDecodingFailedException' that wraps the Throwable we caught using the '$previous' parameter. You can find the change in step2.diff. Let me re-run the tests: > There was 1 failure: > > 1) Symfony\Component\Messenger\Tests\Transport\Serialization\PhpSerializerTest::testDecodingFailsWithBadClass > Failed asserting that exception message 'Could not decode message using PHP serialization: O:13:"ReceivedSt0mp":0:{}.' matches '/class "ReceivedSt0mp" not found/'. Okay, now the Exception message changed. Personally I do not consider this a BC break: I believe Exception messages are meant for human consumption, not for programs. Otherwise fixing a typo in the message would be a BC break. If the code wants to learn about the cause, it should either use the '$code' or different types of Exception should be thrown to clarify the cause by entering a different catch() block. There are two options here: We could fix the expectation (that's what I would do, see step3a.diff), or we could use the message of the original Exception as the message of the wrapper Exception (step3b.diff), or we could rethrow the original exception, if it already is of the correct class (step3c.diff). In all three variants of Step 3 we now fixed the bug for DateTime and SplDoublyLinkedList. PhpSerializer will now consistently throw MessageDecodingFailedException in all cases, just the message might have changed (in variant 'a'). > above and wonder about the changes the RFC would impose. I cannot think > about one that would not imply some sort of "if the version of PHP is >= > 8.3 then A, else B". This is the fact that highlights the BC break. > Now that we fixed the implementation of PhpSerializer for PHP 8.2, let's have a look at what changes with my RFC. Let's take step3a.diff as the basis. Let me first write a userland implementation of the new unserialize, as this makes it easier to demonstrate the behavior to folks without knowledge about the internals. I'll put the implementation into PhpSerializer.php to keep things simple. You can find the implementation in rfc.diff, but I'll repeat it here: > class UnserializationFailedException extends \Exception {} > > function unserialize_php83(string $data, array $options = []): mixed > { > try { > return \unserialize($data, $options); > } catch (\Throwable $e) { > throw new UnserializationFailedException( > 'An Exception was thrown during unserialization', > 0, > $e > ); > } > } Now, let us use the custom implementation, instead of unserialize() directly. To do so we replace unserialize() with unserialize_php83(). You can find the result in rfc-use.diff. What happens if I run the tests? > PHPUnit 9.5.25 #StandWithUkraine > > Testing Symfony\Component\Messenger\Tests\Transport\Serialization\PhpSerializerTest > ......... 9 / 9 (100%) > > Time: 00:00.030, Memory: 8.00 MB > > OK (9 tests, 14 assertions) All green! We **did not have to change anything** after fixing the implementation in step3a.diff for PHP 8.2 and earlier! Okay, let's assume the actual Exception message must not change, e.g. using the solution in step3b.diff and step3c.diff. Again there are multiple solutions: 1. The implementation of the RFC could be adjusted, such that the wrapper exception implicitly uses the original Exception message or original code. So the native implementation could do something similar to step3b. I would be willing to do this, if you believe that would reduce the BC impact. 2. Alternatively a little change to the code is required, let me demonstrate how that would look. The solution is based on 'step3c.diff': We just need to add these three lines to the catch block: > if ($e instanceof UnserializationFailedException && $e->getPrevious()) { > $e = $e->getPrevious(); > } This will unwrap the original Exception that is stored in the '$previous' parameter. You can find the full patch in 'rfc-use-alternative.diff'. Of course this is fully compatible with PHP 8.2 and earlier: The instanceof will simply not match, because 'UnserializationFailedException' does not exist. ------------------- I believe with this example I have sufficiently demonstrated how fixing the code for *the existing PHP versions* will cause the test failures to go away automatically. If you consider the Exception message to be relevant for backwards compatibility, then just three simple lines need to be added - or alternatively the RFC implementation which currently just is a proof of concept (!) could be adjusted. At no point is it necessary to check for the PHP version. I believe the changes to the other classes with failing tests are equally simple based on my demonstration. Feel free to use my attached patches to make the necessary changes in Symfony. There is no need to credit me in any way. Alternatively I would be happy to send a pull request. Just let me know which of the 3 alternatives of step 3 you prefer and I'll send a PR. Best regards Tim Düsterhus