A thread-safe drop-in replacement for TYPO3's core ConfigurationManager that
wraps every configuration read and write in locking to prevent race conditions,
torn reads, and lost updates on the system configuration file
(config/system/settings.php).
TYPO3 rewrites config/system/settings.php as a whole on every change, and it
does so unsafely:
-
Writes are not atomic. Core's
GeneralUtility::writeFile()truncates the file in place (fopen($file, 'wb')) and then writes the new content. There is a short window in which the file on disk is empty or partial. -
Reads are a bare
requireof that same file. A read landing in the write window loads a truncated file, which surfaces as errors like:TYPO3\CMS\Core\Configuration\ConfigurationManager::getLocalConfiguration(): Return value must be of type array, int returned -
Read-modify-write cycles are unlocked.
setLocalConfigurationValueByPath()and friends read the whole array, change one value, and write it back. Two concurrent cycles can each read the old state and overwrite each other — a lost update.
Core has never added locking or atomic writes to this file. Concurrent writers
are realistic in production, most notably around deployments: typo3 upgradeWizard:run running its silent configuration upgrade, extension
configuration being synchronized on the first requests after a deploy, or an
administrator using the Admin Tools while the site serves traffic.
This extension closes that gap.
- Shared locks for reads so multiple readers run in parallel.
- Exclusive locks for writes so writers serialize against readers and each other.
- A single exclusive lock around each read-modify-write cycle
(
setLocalConfigurationValueByPath(),setLocalConfigurationValuesByPathValuePairs(),removeLocalConfigurationKeysByPath(), and the inheritedenableFeature()/disableFeature()), so no update is lost. - Atomic writes via a temporary file plus
rename(), followed by opcache invalidation. Leftover temp files from a crashed write are cleaned up on the next write. - Non-blocking acquisition with exponential backoff and jitter, bounded by a configurable timeout.
composer require plan2net/safe-config-managerTYPO3 instantiates the ConfigurationManager directly with new in
Bootstrap::createConfigurationManager() — there is no DI container involvement
— so the replacement cannot be registered through the service container. You
have to patch the core Bootstrap class.
Without the patch the extension is inert. It ships no
ext_localconf.phpand registers no service that would take effect on its own; if the patch is not applied, TYPO3 keeps using the unprotected coreConfigurationManagerand this extension does nothing. This also means it cannot protect a non-Composer (classic/TER) installation, where Composer patches cannot be applied.
The extension ships a ready-to-apply patch per supported TYPO3 major:
patches/typo3-cms-core-13-bootstrap-safe-config-manager.patch— TYPO3 13.4 LTSpatches/typo3-cms-core-14-bootstrap-safe-config-manager.patch— TYPO3 14.3
Both wrap the replacement in a class_exists() check, so the original
ConfigurationManager is used again if the extension is ever removed.
Apply the patch with the
cweagans/composer-patches
plugin. Pick the patch matching your TYPO3 major and reference it from your
project's composer.json:
{
"extra": {
"patches": {
"typo3/cms-core": {
"Use SafeConfigurationManager for thread-safe configuration handling": "vendor/plan2net/safe-config-manager/patches/typo3-cms-core-13-bootstrap-safe-config-manager.patch"
}
}
}
}The lock timeout (in seconds, minimum 1, default 10) bounds how long acquisition
retries before it gives up and throws a LockAcquireException. Set it in
config/system/additional.php:
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['safe_config_manager']['lockTimeout'] = 15;The value is read at lock-acquisition time, so it applies to every operation
except the very first configuration read during bootstrap, which necessarily
runs before TYPO3_CONF_VARS exists and uses the default.
- PHP 8.2+, TYPO3 13.4 LTS or 14.3.
- The Bootstrap patch must be applied (see Installation). Composer-managed installations only.
- Requires
FileLockStrategy(flock). The manager asks theLockFactoryfor a strategy that supports shared, exclusive, and non-blocking locking at once; of TYPO3's shipped strategies onlyFileLockStrategydoes. This is TYPO3's default.SemaphoreLockStrategy(exclusive only) andSimpleLockStrategy(no shared lock) cannot satisfy the request — do not raise their priority aboveFileLockStrategy, or lock creation will fail. - Single-server scope.
flockcoordinates processes on one host, with the lock files undervar/lock/. If several web nodes shareconfig/system/settings.phpover a network filesystem but keepvar/local, the locks do not span nodes and cross-node races remain. (flockbehavior on NFS is itself unreliable.) The atomic write still prevents torn reads on each node.
Replacing the core manager makes configuration access depend on the locking subsystem:
- Every request's bootstrap now takes a shared lock to read configuration, which
needs a writable
var/lock/. If it is not writable, bootstrap fails. - If a process holds the exclusive write lock and hangs, other requests block on
acquisition for up to
lockTimeoutseconds and then fail with aLockAcquireException(flock is released automatically when the holding process dies).
These are the cost of correctness; on a healthy single-server setup they are not observable.
GPL-2.0-or-later
plan2net GmbH — https://www.plan2.net