NestedArray::setValue() without $force can cause WSOD.

Created on 10 August 2017, almost 7 years ago
Updated 28 January 2024, 4 months ago

If NestedArray::setValue() is called with an array structure that does not support adding the value at the given position, the page crashes with a

Warning: Illegal string offset 'id' in Drupal\Component\Utility\NestedArray::setValue()

and

Fatal error: Cannot create references to/from string offsets nor overloaded objects in /[..]/core/lib/Drupal/Component/Utility/NestedArray.php

The method does have a $force parameter which can prevent such situations.
But in some cases it is called without this parameter.

E.g. in FormBuilder::handleInputElement() we find this code:

          NestedArray::setValue($form_state->getUserInput(), $element['#parents'], NULL);

For me this blew up when I changed a form structure in PHP, and then resubmitted the form.
So it could happen when a user resubmits a form at the moment of a deployment.
It's a bit rare maybe but still..

A way to artificially reproduce this with custom code:

$array = ['x' => 'X'];
$parents = ['x', 'y'];
$value = 5;
$force = false;
\Drupal\Component\Utility\NestedArray::setValue($array, $parents, $value, $force);

My main point here is that the NestedArray::setValue() method is not programmed in a robust way.
It has no well-defined behavior in a case like above.

My first solution would be to let it throw an exception instead.
E.g. like this:


  public static function setValue(array &$array, array $parents, $value, $force = FALSE) {
    $ref = &$array;
    foreach ($parents as $i => $parent) {
      // PHP auto-creates container arrays and NULL entries without error if $ref
      // is NULL, but throws an error if $ref is set, but not an array.
      if (NULL !== $ref && !is_array($ref)) {
        if ($force) {
          $ref = [];
        }
        else {
          $conflict_parents = [];
          foreach ($parents as $j => $theparent) {
            if ($i === $j) {
              break;
            }
            $conflict_parents[] = $theparent;
          }
          $parents_str = '$[' . implode('][', $parents) . ']';
          $conflict_parents_str = '$[' . implode('][', $conflict_parents) . ']';
          $type_at_ref = gettype($ref);
          throw new \RuntimeException("Tried to set nested value at $parents_str, but found a $type_at_ref at $conflict_parents_str.");
        }
      }
      $ref = &$ref[$parent];
    }
    $ref = $value;
  }

This may be a bit overkill, but most of the added logic is only executed if the exception is going to be thrown.
The logic takes into account that $parents might not be cleanly indexed.

In the above artificial example, the output would be:

RuntimeException: Tried to set nested value at $[x][y], but found a string at $[x]. in Drupal\Component\Utility\NestedArray::setValue()

This is already better, isn't it?

πŸ› Bug report
Status

Needs work

Version

11.0 πŸ”₯

Component
FormΒ  β†’

Last updated about 3 hours ago

Created by

πŸ‡©πŸ‡ͺGermany donquixote

Live updates comments and jobs are added and updated live.
  • PHP 8.0

    The issue particularly affects sites running on PHP version 8.0.0 or later.

Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

Production build 0.69.0 2024