Skip to content

plan2net/safe-config-manager

Repository files navigation

Safe Configuration Manager for TYPO3 CMS

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).

Background

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 require of 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.

Features

  • 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 inherited enableFeature()/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.

Installation

composer require plan2net/safe-config-manager

TYPO3 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.php and registers no service that would take effect on its own; if the patch is not applied, TYPO3 keeps using the unprotected core ConfigurationManager and 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 LTS
  • patches/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"
      }
    }
  }
}

Configuration

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.

Requirements and limitations

  • 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 the LockFactory for a strategy that supports shared, exclusive, and non-blocking locking at once; of TYPO3's shipped strategies only FileLockStrategy does. This is TYPO3's default. SemaphoreLockStrategy (exclusive only) and SimpleLockStrategy (no shared lock) cannot satisfy the request — do not raise their priority above FileLockStrategy, or lock creation will fail.
  • Single-server scope. flock coordinates processes on one host, with the lock files under var/lock/. If several web nodes share config/system/settings.php over a network filesystem but keep var/ local, the locks do not span nodes and cross-node races remain. (flock behavior on NFS is itself unreliable.) The atomic write still prevents torn reads on each node.

Trade-offs

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 lockTimeout seconds and then fail with a LockAcquireException (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.

License

GPL-2.0-or-later

Author

plan2net GmbH — https://www.plan2.net

About

Thread-safe configuration manager for TYPO3 CMS that extends the core ConfigurationManager with built-in locking mechanisms

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages