ConfigManager can lose the ordering of dependencies

Created on 22 April 2020, over 4 years ago
Updated 4 September 2024, 4 months ago

Drupal\Core\Config\ConfigManager uses array_merge() to merge arrays of dependencies whose order is important, but array_merge() will lose the ordering of the dependencies.

Problem

The method getConfigEntitiesToChangeOnDependencyRemoval() returns a list of entities to change, and the interface for the return array from that says "The order of the deletes is significant and must be processed in the returned order.".

However, getConfigEntitiesToChangeOnDependencyRemoval() sequentially processes an array of dependencies returned from findConfigEntityDependentsAsEntities(), which makes use of an array of dependencies from findConfigEntityDependents(). Neither of these document that they return ordered arrays, and both use array_merge() which can confuse array order.

This bug has not been previously discovered because it requires an unsual combination of dependencies to surface it. If getConfigEntitiesToChangeOnDependencyRemoval() is invoked with dependencies that will trigger array merging, the returned array will not have the right ordering dependencies.

Case 1: findConfigEntityDependentsAsEntities()

public function findConfigEntityDependentsAsEntities($type, array $names, ConfigDependencyManager $dependency_manager = NULL) {
$dependencies = $this->findConfigEntityDependents($type, $names, $dependency_manager);
$entities = [];
$definitions = $this->entityTypeManager->getDefinitions();
foreach ($dependencies as $config_name => $dependency) {
...
$entities[$entity_type_id][] = ...
...
}
$entities_to_return = [];
foreach ($entities as $entity_type_id => $entities_to_load) {
...
$entities_to_return = array_merge($entities_to_return, array_values($storage->loadMultiple($entities_to_load)));
}
return $entities_to_return;
}

Something like this would work:

    $entities=[];
    foreach ($entities_to_return as $configEntity){
      $entities[$configEntity->getConfigDependencyName()]=$configEntity;
    }
    $entities_to_return=array_merge(array_intersect_key($dependencies, $entities),$entities);
    return $entities_to_return;

Case 2: findConfigEntityDependents()

public function findConfigEntityDependents($type, array $names, ConfigDependencyManager $dependency_manager = NULL) {
...
$dependencies = [];
foreach ($names as $name) {
$dependencies = array_merge($dependencies, $dependency_manager->getDependentEntities($type, $name));
}
return $dependencies;
}

The problem with array_merge()

Suppose we have two dependent arrays:

$a=['a'=>'1','b'=>2,'c'=>3,'d'=>4];
$b=['x'=>'1','b'=>2,'c'=>3,'d'=>4];

In $a, a depends on b, b depends on c, and c depends on d.
In $b, and again, x depends on b, b depends on c, and c depends on d.
The result of array_merge($a,$b) is:

['a'=>'1','b'=>2,'c'=>3,'d'=>4,'x'=>'1'];

This is clearly wrong ,The correct result should be:

['a'=>'1','x'=>'1','b'=>2,'c'=>3,'d'=>4];

or:

['x'=>'1', 'a'=>'1','b'=>2,'c'=>3,'d'=>4];

We need an algorithm to merge dependent arrays.

Remaining tasks

  1. Devise a configuration dependency structure that is capable of surfacing this bug. Likely this will involve something that mixes dependencies from different types, as that will trigger the array_merge issue in findConfigEntityDependents().
  2. Create a Unit test that calls getConfigEntitiesToChangeOnDependencyRemoval() with the problematic depenency structure.
  3. Devise a fix.
  4. Implement a fix
🐛 Bug report
Status

Needs work

Version

11.0 🔥

Component
Configuration 

Last updated about 17 hours ago

Created by

🇨🇳China yunke

Live updates comments and jobs are added and updated live.
  • Needs tests

    The change is currently missing an automated test that fails when run with the original code, and succeeds when the bug has been fixed.

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.

  • 🇪🇪Estonia rang501 Viljandi

    I can say this issue is still happening.

    We seem to have an issue during module uninstall, where it pulls some extra configuration in through role configuration and then just deletes multiple views and workflow configurations. Applying the change in case 1 almost solved it (one configuration still was marked as deleted).

    Not sure how to reproduce it - it is environment specific (probably depends how dependencies were calculated).

  • 🇪🇪Estonia rang501 Viljandi

    Created patch based on case 1

  • 🇪🇪Estonia rang501 Viljandi

    Patch had path issue.

  • Status changed to Needs review almost 2 years ago
  • Status changed to Needs work almost 2 years ago
  • 🇺🇸United States smustgrave

    Next step will be to add test cases.

    Updating credit.

    =====

    Just FYI to help get the message out there.

    Starting March 2023, simple rerolls, rebases, or merges will no longer receive issue credit. Only rerolls that address a merge conflict will be credited, and the merge conflict that was resolved must be documented in the text of an issue comment.

    Example
    error: patch failed: core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module:77
    error: core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module: patch does not apply

    To receive credit for contributing to this issue, assist with other outstanding tasks or unaddressed feedback.
    See the issue credit guidelines for more information.

  • 🇪🇪Estonia rang501 Viljandi

    I can describe one situation, when the problem happens:

    I have two configurations, the first is workflow configuration, which depends (enforced) on the workbench_email configuration, which depends on the role configuration. Now, uninstalling one module which provides some permissions for that role (in my case user_shortcut module), it does the following:

    • it updates role configuration (correct)
    • it updates workbench_email configuration (correct)
    • it deletes workflow configuration (wrong)

    It does not explain, why it happens on some environments (it is database related, because I was able to reproduce it with dump). The differences are hard to track as there is no config sync, so environments in my case are somewhat independent.

  • 🇺🇸United States emb03

    I am running into an issue where the Manage Permissions Tab is returning an error on some nodes. Could this possibly be related?

    Error:
    Drupal\Component\Plugin\Exception\PluginNotFoundException: The "" entity type does not exist. in Drupal\Core\Entity\EntityTypeManager->getDefinition() (line 139 of /code/web/core/lib/Drupal/Core/Entity/EntityTypeManager.php).

    Backtrace:

    #0 /code/web/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php(589): Drupal\Core\Entity\EntityTypeManager->getDefinition(NULL)
    #1 /code/web/core/lib/Drupal/Core/Field/FieldConfigBase.php(265): Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::onDependencyRemoval(Object(Drupal\Core\Field\Entity\BaseFieldOverride), Array)
    #2 /code/web/core/lib/Drupal/Core/Config/ConfigManager.php(479): Drupal\Core\Field\FieldConfigBase->onDependencyRemoval(Array)
    #3 /code/web/core/lib/Drupal/Core/Config/ConfigManager.php(342): Drupal\Core\Config\ConfigManager->callOnDependencyRemoval(Object(Drupal\Core\Field\Entity\BaseFieldOverride), Array, 'config', Array)
    #4 /code/web/core/modules/user/src/Form/EntityPermissionsForm.php(88): Drupal\Core\Config\ConfigManager->getConfigEntitiesToChangeOnDependencyRemoval('config', Array)
    #5 /code/web/core/modules/user/src/Form/UserPermissionsForm.php(180): Drupal\user\Form\EntityPermissionsForm->permissionsByProvider()
    #6 /code/web/core/modules/user/src/Form/EntityPermissionsForm.php(133): Drupal\user\Form\UserPermissionsForm->buildForm(Array, Object(Drupal\Core\Form\FormState))
    #7 [internal function]: Drupal\user\Form\EntityPermissionsForm->buildForm(Array, Object(Drupal\Core\Form\FormState), 'node_type', 'page')
    #8 /code/web/core/lib/Drupal/Core/Form/FormBuilder.php(536): call_user_func_array(Array, Array)
    #9 /code/web/core/lib/Drupal/Core/Form/FormBuilder.php(283): Drupal\Core\Form\FormBuilder->retrieveForm('user_admin_perm...', Object(Drupal\Core\Form\FormState))
    #10 /code/web/core/lib/Drupal/Core/Controller/FormController.php(73): Drupal\Core\Form\FormBuilder->buildForm(Object(Drupal\user\Form\EntityPermissionsForm), Object(Drupal\Core\Form\FormState))
    #11 [internal function]: Drupal\Core\Controller\FormController->getContentResult(Object(Symfony\Component\HttpFoundation\Request), Object(Drupal\Core\Routing\RouteMatch))
    #12 /code/web/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(123): call_user_func_array(Array, Array)
    #13 /code/web/core/lib/Drupal/Core/Render/Renderer.php(627): Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}()
    #14 /code/web/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(124): Drupal\Core\Render\Renderer->executeInRenderContext(Object(Drupal\Core\Render\RenderContext), Object(Closure))
    #15 /code/web/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(97): Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext(Array, Array)
    #16 /code/vendor/symfony/http-kernel/HttpKernel.php(181): Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}()
    #17 /code/vendor/symfony/http-kernel/HttpKernel.php(76): Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object(Symfony\Component\HttpFoundation\Request), 1)
    #18 /code/web/core/lib/Drupal/Core/StackMiddleware/Session.php(58): Symfony\Component\HttpKernel\HttpKernel->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #19 /code/web/core/lib/Drupal/Core/StackMiddleware/KernelPreHandle.php(48): Drupal\Core\StackMiddleware\Session->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #20 /code/web/core/lib/Drupal/Core/StackMiddleware/ContentLength.php(28): Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #21 /code/web/core/modules/ban/src/BanMiddleware.php(50): Drupal\Core\StackMiddleware\ContentLength->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #22 /code/web/modules/contrib/shield/src/ShieldMiddleware.php(270): Drupal\ban\BanMiddleware->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #23 /code/web/modules/contrib/shield/src/ShieldMiddleware.php(137): Drupal\shield\ShieldMiddleware->bypass(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #24 /code/web/core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php(48): Drupal\shield\ShieldMiddleware->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #25 /code/web/core/lib/Drupal/Core/StackMiddleware/NegotiationMiddleware.php(51): Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #26 /code/web/core/lib/Drupal/Core/StackMiddleware/AjaxPageState.php(36): Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #27 /code/web/core/lib/Drupal/Core/StackMiddleware/StackedHttpKernel.php(51): Drupal\Core\StackMiddleware\AjaxPageState->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #28 /code/web/core/lib/Drupal/Core/DrupalKernel.php(704): Drupal\Core\StackMiddleware\StackedHttpKernel->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #29 /code/web/index.php(19): Drupal\Core\DrupalKernel->handle(Object(Symfony\Component\HttpFoundation\Request))
    #30 {main}
Production build 0.71.5 2024