Shared Mutable State

ID

php.shared_mutable_state

Severity

low

Remediation Complexity

medium

Remediation Risk

medium

Remediation Effort

medium

Resource

Synchronization

Language

Php

Tags

ASVS50:v15.4.1, CWE:362, CWE:567, NIST.SP.800-53, PCI-DSS:6.5.6

Description

Detects state that outlives the request that writes it, written outside initialization code.

Under PHP-FPM every request starts from a fresh process state: a static property, a global or a function’s static variable dies with the request, and the rule does not report it. Two kinds of state do survive the request:

  • Under a long-running runtime — Laravel Octane, RoadRunner, Swoole or OpenSwoole, ReactPHP, AMPHP, Workerman, FrankenPHP’s worker mode — a static property, a global and a function’s static variable live as long as the worker. Every request the worker serves reads and writes the same one, and coroutines interleave within one worker. The runtime is read from the project: a composer.json requiring one of its packages, or its configuration file (.rr.yaml, config/octane.php).

  • Under any runtime, an APCu entry or a shared-memory variable is read by every worker of the pool. A read-modify-write of one loses the update another worker made in between.

When the state is assigned a value built from a function parameter, one caller’s data reaches the next, and that finding is raised to high.

One issue is reported per piece of state, at its worst write.

Rationale

A static array a controller appends to, under Octane or RoadRunner:

class SearchHistory
{
    private static array $recent = [];

    public function remember(Request $request): void
    {
        self::$recent[] = $request->input('q');   // every request of the worker writes the same array
    }
}

A function’s static variable used as a cache:

function price(string $sku, callable $lookup): float
{
    static $prices = [];
    $prices[$sku] = $lookup($sku);                // served to every later request of the worker
    return $prices[$sku];
}

A read-modify-write of an APCu entry, under any runtime:

apcu_store('hits:' . $page, apcu_fetch('hits:' . $page) + 1);   // two workers both read 41 and store 42

Remediation

  1. Eliminate Shared State: keep request data in arguments and return values, in the request, or in a service the container builds per request.

  2. Use Atomic Operations: apcu_inc, apcu_dec, apcu_cas or apcu_entry for APCu counters, Swoole\Table and Swoole\Atomic for data shared between Swoole workers.

  3. Synchronize Access: take a lock before the write, and release it after:

    $lock = $this->locks->createLock('hits');
    $lock->acquire(true);
    try {
        self::$hits++;
    } finally {
        $lock->release();
    }

Not Reported

  • A static property, a global or a static variable under PHP-FPM, where it dies with the request.

  • A write in a constructor, in a service provider’s boot() or register(), in a test’s set-up, or in a callback a server runs once when the worker starts ($server→on('workerStart', …)).

  • A write after a lock is taken in the same function ($lock→acquire(), sem_acquire(), flock()).

  • A lazy initialization, self::$x ??= … or a write under if (self::$x === null), whose value is not a parameter.

  • A thread-safe holder (Swoole\Table, Swoole\Atomic, a coroutine channel).

  • An APCu write that is atomic (apcu_inc, apcu_cas, apcu_entry), or that stores a value not read from the same key.

  • Test code.

© 2026 Xygeni. All rights reserved