Problem/Motivation
`Drupal\Core\Extension\ModuleInstaller` caches container-bound service proxies once, before a
loop or method that itself explicitly anticipates a module's `hook_install()` rebuilding the
service container mid-batch (see the `$module_groups`/container-rebuild handling already present
in `ModuleInstaller::install()`). When that rebuild actually happens, the already-cached proxy
still points at the discarded container. If code later in the same method resolves an OOP
`#[Hook]`-attributed class through that stale proxy via `ClassResolver`, it crashes with:
```
Symfony\Component\DependencyInjection\Exception\RuntimeException: You have requested a
synthetic service ("kernel"). The DIC does not know how to construct this service.
```
This happens because the synthetic `kernel` service is only set on a container by Drupal's real
HTTP/console kernel bootstrap. A container rebuilt mid-request via `ContainerBuilder` never has
it set, so any `ClassResolver`-based hook invocation routed through a stale reference to that
container fails.
Two concrete call sites hit this:
1. **`ModuleInstaller::doInstall()`** caches `\Drupal::service('config.installer')` into a local
`$config_installer` variable once, before looping over the InstallEntities/Optional/
SiteOptional passes that call each module's `hook_install()`. Any of those `hook_install()`
calls can itself trigger a container rebuild (for example, one that uses
`\Drupal::entityTypeManager()` to load or create a config entity during install). A later
pass that reuses the stale `$config_installer` to install a subsequent module's optional
config then crashes if that path invokes any OOP `#[Hook]` handler through the stale proxy's
injected services.
2. **`ModuleInstaller::install()`** ends with
`$this->moduleHandler->invokeAll('modules_installed', [$module_list, $sync_status])`, using
the `ModuleHandler` injected into this `ModuleInstaller` instance at construction time. If any
earlier module in the same `$module_list` triggered a container rebuild via its own
`hook_install()`, `$this->moduleHandler` is now stale. Resolving any OOP
`#[Hook('modules_installed')]` implementer through that stale handler's `ClassResolver`
crashes with the same synthetic-`kernel` error.
Both are instances of the same underlying pattern: a cached, container-bound proxy is reused
across a loop/method that its own code already accounts for as rebuild-prone, but the proxy
itself is never refreshed afterward.
This is likely to recur whenever a new or upgraded module first introduces an OOP
`#[Hook]`-attributed implementation of a hook invoked during module installation, since a plain
procedural hook implementation never needs `ClassResolver`/the container and so never surfaces
this. We've only confirmed and fixed these two call sites; there may be others following the
same pattern elsewhere in the module-install code path.
Steps to reproduce
Reproduced live on 2026-09-08 against Drupal core 11.4.6:
1. Enable a module whose `hook_install()` (or a co-installed module's) triggers a container
rebuild, e.g. one that calls `\Drupal::entityTypeManager()` to load or create a config entity
during install.
2. In the same install batch (a profile `drush site:install`, or a multi-module `drush en`),
also enable a module implementing `hook_modules_installed()` via the OOP `#[Hook]` attribute
system rather than a procedural function — for example `drupal/extlink` `^3.0`'s
`Drupal\extlink\Hook\ExtlinkHooks::modulesInstalled()`.
3. The install crashes at the very end, after every module has otherwise logged "installed":
```
[Symfony\Component\DependencyInjection\Exception\RuntimeException]
You have requested a synthetic service ("kernel"). The DIC does not know how to construct this service.
Drupal\Core\DependencyInjection\ClassResolver->getInstanceFromDefinition() at ClassResolver.php:28
Drupal\Core\Utility\CallableResolver->getCallableFromDefinition() at CallableResolver.php:100
Drupal\Core\Extension\ModuleHandler->getHookImplementationList() at ModuleHandler.php:751
Drupal\Core\Extension\ModuleHandler->invokeAllWith() at ModuleHandler.php:338
Drupal\Core\Extension\ModuleHandler->invokeAll() at ModuleHandler.php:388
Drupal\Core\Extension\ModuleInstaller->install() at ModuleInstaller.php:257
install_install_profile() at install.core.inc:1699
```
(Call site #1, the `config.installer` instance, was found and fixed independently in an earlier
round against an older core minor version; it wasn't re-verified with a fresh stack trace in
this same session, but the fix follows the identical pattern.)
Proposed resolution
e-fetch each container-bound service fresh from the current container at each use site inside
these loops/methods, rather than caching a single instance before a loop or method that can
itself trigger a rebuild:
- In `doInstall()`: replace the single `$config_installer = \Drupal::service('config.installer');`
cached before the loops with a fresh `\Drupal::service('config.installer')` call at each use
site, inside every pass (InstallEntities, Optional, SiteOptional).
- In `install()`: replace `$this->moduleHandler->invokeAll('modules_installed', ...)` with
`\Drupal::moduleHandler()->invokeAll('modules_installed', ...)`, so the trailing
`hook_modules_installed()` invocation always uses a handler bound to whatever container is
current at that point, not the one injected when this `ModuleInstaller` instance was
constructed.
Both fixes have been running as composer patches in production for several months in the
Mukurtu CMS Drupal distribution:
- https://github.com/MukurtuCMS/Mukurtu-CMS/blob/main/patches/module-insta...
- https://github.com/MukurtuCMS/Mukurtu-CMS/blob/main/patches/module-insta...
Remaining tasks
See the tags
User interface changes
None,
API changes
None to public signatures. Internal behavior change only: two call sites re-fetch a service from
the container instead of reusing a cached reference.
Comments
Comment #2
quietone commentedHi, thanks for the report.
Issues for Drupal core should be targeted to the 'main' branch, our primary development branch. Changes are made on the main branch first, and are then back ported as needed according to the Core change policies. The version the problem was discovered on should be stated in the issue summary Problem/Motivation section. Thanks.
Also, the title should be a description of what is being fixed or improved. The title is used as the git commit message so it should be meaningful and concise. See List of issue fields.
Finally, the issue summary could use some formatting so it is easier for reviewers to read. It looks like it is Markdown but should be HTML and is rather verbose. In fact, this reads like prose from an AI. If so, in Drupal core, we would like concise summaries that are in the reporter's own words. Of course, giving allowance for anyone who is not a native English speaker. See the orange warning box in the policy on the use of AI when contributing to Drupal.
Comment #3
quietone commentedAdding possible related issue, remove if I have misunderstood this.
Comment #4
mwynne commentedThanks for getting back to me with that. Yes it is a largely Claude-generated report (I'm not really a developer, just hoping to pass along our issue and fixes), and I will try to update the issue in line with your notes and the policies.
Comment #5
nicxvan commentedPlease follow the ai policy: https://www.drupal.org/docs/develop/issues/issue-procedures-and-etiquett...