We found that after migrating our Drupal content management system to PHP 7.3, we experienced some issue related to the unserialize failure of a serialized input batch object. This batch object comes from a multi-step input form, which has following data structure

batch (array)
'sets'=> array,
...
'form_state' => Drupal\Core\Form\FormState
....
'complete_form'=>....
'values' => .....
....//Other fields

In above example, the Batch is an array of objects, inside it has a "form_state" object (Drupal FormState), inside "form_state", it has another array of the "complete_form", which encapsulate all the request form from UI.

To simple test, you can just run

unserialize(serialize($batch)

it will return false in above case.

Following are findings by playing around the unserialize(serialize($batch)
1: The reason "unserialize(serialize($batch)" return false is because "unserialize(serialize($batch['form_state'])" return false
2: If we create an empty FormState, like "$batch['form_state'] = new FormState()", unserialize(serialize($batch) will return as an array
3: If we assign some nested array of object inside 'form_state', like the "complete_form" above inside, keep the original nested structure of the $batch object as above, then unserialize(serialize($batch) return false
4: If we run "unserialize(serialize($batch['form_state'])", it will return correctly with a nested array of objects

This makes us to workaround the serialize/unserialize issue of $Batchby combing 3 and 4 above into an array, and then serialize()/unserialize it in in the BatchStorage to make sure it workaround the problem we have. But this change of data structure of the input batch object, is just an additional catch of the problem we have. This issue should be addressed in the Drupal core in a more generic solution, for example:
1: Make sure the input batch object is always serializable/deserialize correctly, which may not be easy since the input form data can be configured based on specific request, we need to make sure each nested object inside the batch object has correctly serialization implemented (either through the Serializable interface or method pair of __sleep() and __awake().
2: Add some validation in the batch processing, or change the logic of batch processing not using serialize/unserialize pair, instead, use something like json_encode/json_decode (need to reconfig the PHP object from the decode), or
3: Add certain validation before persisting to database, and validate the return after "unserialize" from database return.

CommentFileSizeAuthor
#20 drupal-batchstorage-unserialize-formstate-3055287-19.patch1.17 KBHenry Tran
#18 interdiff_16-18.txt456 bytesphilltran
#18 drupal-batchstorage-unserialize-formstate-3055287-18.patch5.77 KBphilltran
#17 interdiff_14-16.txt5.21 KBphilltran
#17 drupal-batchstorage-unserialize-formstate-3055287-16.patch5.72 KBphilltran
#14 drupal-batchstorage-unserialize-formstate-3055287-14.patch1.02 KBchOP
php73uploadfileform.patch5.69 KBwangchunzhang
#3 unserialize-php-73-batch-failure-3055287-2.patch5.69 KBndrake86
#4 unserialize-php-73-batch-failure-3055287-4.patch6.2 KBndrake86
#11 drupal-batch-storage-fails-3055287-11.patch7.26 KBHenry Tran
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

wangchunzhang created an issue. See original summary.

wangchunzhang’s picture

Issue summary: View changes
ndrake86’s picture

Attaching the patch to a comment so the test bot can run it. This is the same patch as uploaded in the body.

ndrake86’s picture

Uploading a patch for @Wangchun he is having issues because of his account:

"As a part of our continued efforts to make Drupal.org a productive place to visit and work, we’ve made it harder for spammers to post on the site. People who post valuable content will be confirmed and won’t see this message."

ndrake86’s picture

Status: Active » Needs review

Testbot doesn't have an issue w/ it moving to needs review.

The last submitted patch, php73uploadfileform.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

kevin.dutra’s picture

Status: Needs review » Needs work
Issue tags: -batch process serialization deserialization PHP 7.3

There are a few problems here:

  1. To even start discussion here, we need a way to reliably reproduce the issue. (The issue summary does not adequately explain how to produce the issue.) Ideally you'd start with an automated test that will consistently reproduce the issue, which will both (1) help give a better understanding of the issue as well as (2) prove that a potential fix is actually working when it gets attached.
  2. The provided patch is, at best, a workaround for one use case. If the problem really is at a lower level with serialization, there are likely multiple places that need to be adjusted to adequately cope with the issue, and we'd want to make a change that is generic instead of a hacky one-off for the FormState class.
  3. And that kind of leads to the last issue, which is that if this is really a problem with the native PHP serialization, then the problem needs to be fixed in PHP, not in Drupal. (But it's still not clear what the actual issue is yet.)

Also, please read the issue tagging guidlines before adding tags to issues. Thanks!

ndrake86’s picture

1. Yes, a test would be great. The test we have that identified the issue is all based on custom code, one of the main drivers to putting the ticket on D.O was for the test bot in core to run the tests around batch and hopefully to spark some conversation that anyone else saw around serialization of the form_state. I will see if I can carve out some time to write a test based only on core code that reproduces the issue.
2. Yes, this is a super specific patch. Luckily the other workd-arounds for for PHP 73. I actually think this post shows something similarly wrong w/ batch -> https://www.drupal.org/project/drupal/issues/3011684
3. Its a known PHP change, they "fixed" a bug in 7.3 that made unserialize stricter, from the changelog (https://www.php.net/ChangeLog-7.php):

Fixed unserialize(), to disable creation of unsupported data structures through manually crafted strings.

there isn't an issue associated w/ that change so don't really know what spawned the change. If you read through some of the other issues here about serialize and unserialize you can see find a link to the conversation on the PHP email list where the PHP maintainers explains some of the issues w/ serialize unserialize:

I plan to propose and implement a new custom object serialization mechanism for PHP 7.4, to replace the Serializable interface and all the problems that come with it.
> For now, all I can suggest is to rewrite your code in a way that does not use parent::serialize(). I don’t think there is anything we can do to fix Serializable itself, unfortunately.

wangchunzhang’s picture

The correct fix, based on my investigation, can be let the input batch go through some Temp Store (like SharedTempStore) before the real serialize/unserialize call in BatchStorage, I noticed that in Drupal core, the BatchStorage is in a position equal to PHPSerialize. but a lot of calls into PHPSerialize comes from object in SharedTempStore & PrivateTempStore, while BatchStorage doesn't, it directly come from input form state, for example, the FormState in this case.

There is a workaround applied in SharedTempStore (https://www.drupal.org/files/issues/2019-04-23/3049437-6.patch) to workaround PHP7.3 serialize/unserialize issue. Also, workaround is already in Drupal Core (8.6.16) for PrivateTempStore, etc. this is the reason for above fix suggestion.

Of course, the ultimate fix should also include the fix to make the PHP serialize/unserialize more robust. Based on the discussion, it is obvious they are not as good as it should be.

daffie’s picture

Can we do it more like:

$batch_serialized = $this->serialization($batch);

And use the value of $batch_serialized in the insert/update query.
The method testSerialization() should not do the inserting or updating.
Let the method serialization() do everything that testSerialization() without the insert/update.

chOP’s picture

chOP’s picture

Tagging against topical issue tag for PHP 7.3 as this issue impacts sites running PHP 7.3.0 or later. Updating to version to 8.8.x-dev

We may also want to link this solution to other PHP 7.3 Workarounds #3001920: Investigate PHP 7.3 workarounds

The first step should be to write a test that can reproduce the problem.

The solution patch provided so far reads as too specific and hacky. It might be better to follow the patterns used when solving this in #3049437: SharedTempStore not serializable on PHP 7.3 and #3011684: Properly serialize \Drupal\Core\TempStore\PrivateTempStore where they leverage the DependencySerializationTrait to serialize/unserialize

chOP’s picture

Find attached a new solution that is allows FormState to be serialized intact, so it can be used in batch Form submissions. This solution implements the Drupal\Core\DependencyInjection\DependencySerializationTrait

When returning the list of properties to serialize, we remove the build_info as this contains Symfony Routes and Requests and other services that shouldn't be serialized and unserialized again. By removing them for serialization it seems to have fixed the problem.

I've yet to find a simple way to write a test for this. Was looking at how FormState unit tests are set-up for clues.

Status: Needs review » Needs work
philltran’s picture

@chOP

Thanks for patch #14. It seems to solve the issue I was having with runnint batch processes from a form on PHP 7.3.
However, previewing a node errors out with:
The form object has not yet been stored on the form state by the form builder

philltran’s picture

Realized that the error message I mentioned was from a patch on Issue 2897557

Here is a patch for this issue that fixed the preview error on my local. Please test it.

Thanks.

Status: Needs review » Needs work

The last submitted patch, 18: drupal-batchstorage-unserialize-formstate-3055287-18.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Henry Tran’s picture

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

larowlan’s picture

larowlan’s picture

Issue tags: +Needs tests

Thanks for working on this, as its a bug, we need a new test demonstrating the bug

  1. +++ b/core/lib/Drupal/Core/Form/FormState.php
    @@ -449,6 +454,24 @@ class FormState implements FormStateInterface {
    +    if ($obj_vars['build_info']['args'] && is_object($obj_vars['build_info']['args'][0])) {
    

    shouldn't we be using array_filter with 'is_object' on $obj_vars['build_info']['args'] in case the second, third or subsequent argument is an object?

  2. +++ b/core/lib/Drupal/Core/Form/FormState.php
    @@ -449,6 +454,24 @@ class FormState implements FormStateInterface {
    +      return $vars;
    

    no need for the else here as we return earlier

hoanns’s picture

Hello,

this is the simplest form I could write to reproduce the problem. Maybe someone can write a test with that. Or this isn't correlated and is a bug in requestStack?

<?php

namespace Drupal\qd_mailing_entity\Form;


use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;

class FailedMailingRequeueForm extends ConfigFormBase {

  public $oRequest;

  protected function getEditableConfigNames() {
    return [];
  }

  public function getFormId() {
    return 'failed_mailing_requeue_form';
  }

  public function buildForm(array $form, FormStateInterface $form_state) {
    $this->oRequest = \Drupal::requestStack()->getCurrentRequest();
    return parent::buildForm($form, $form_state);
  }

  public function submitForm(array &$form, FormStateInterface $form_state) {
    unserialize(serialize($form_state));
   // batch_set() would fail here, too
  }

}

This works when you use the form normally in the browser, but when you serialize it won't work.

To fix serialize for this form you could use either \Drupal::request() or set $oRequest to private. But I just wrote this form to help reproduce the problem. Hope someone more knowledgeable can understand the underlying problem :)

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

refman1073’s picture

I discovered that injecting the logger.factory service into a class also causes batch to fail because of serialization. Digging a bit deeper, logger uses the RequestStack, which is at the root of the problem.

In looking at the serialized data in the batch table, the unserialize is failing because the request parameter _access_result value is r:xxx where the xxx reference is not defined (I don't fully understand the r syntax so I cannot give greater detail.

I hope this helps.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

alexdoma’s picture

Confirm that removing logger.factory service fix the problem

andrewsuth’s picture

Just to confirm that the patch from #20 works.

InspiredNotion’s picture

I have just installed a fresh latest version of drupal 9.3X andi am getting this message on opening and logging into the site. anyone know why this is still here in the latest version and is there a fix. thanks

Daniel

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

hidabe’s picture

The last path (#20) not is working for me,

I have need add another condition for remove build_info field, so:

  /**
   * {@inheritdoc}
   */
  public function __sleep() {
    $obj_vars = get_object_vars($this);
    $vars = $this->defaultSleep();
    if (
      $obj_vars['build_info']['args'] && is_object($obj_vars['build_info']['args'][0])
      || ($obj_vars['build_info']['callback_object'])
    ) {
      $unserializable = [
        'build_info',
      ];
      return array_diff($vars, $unserializable);
    }
    else {
      return $vars;
    }

  }

In Drupal 9 all work ok about this issue.

sergiur’s picture

RE: #26 also confirming that's the cause -- removing the logger factory service fixed my issue as well. I've been struggling with it for longer than I care to admit, thank you @refman1073.

With the logger factory in (and since PHP7.3 and up) the form_state on my Configurable Action (buildConfigurationForm) errors on serialization. Without it, no errors!

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

belatrix’s picture

Last patch (#20) with changes from comment #32 worked for me on project with Drupal 9.
Also decision from #33 worked, looks like problem in logger factory.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.