Skip to content

ReadModifyWriteLockTooLate ​

warning native on by default

Group: Probable bugs

Detect file_get_contents of a path feeding a transformed value into file_put_contents on the same proven path with LOCK_EX but no enclosing lock spanning the read.

Example ​

php
<?php
function bad($path) { $n = (int) file_get_contents($path); file_put_contents($path, $n + 1, LOCK_EX); }

Reported:

  • line 2: Hold the lock across the complete read-modify-write operation.

Detection ​

  • Report file_get_contents of a path feeding a transformed value into file_put_contents on the same proven path with LOCK_EX but no enclosing lock spanning the read.
  • D1a. A lock spans the operation when, in the same function or top-level scope, a builtin flock call that ends before the earliest feeding read takes an exclusive lock (LOCK_EX, optionally with LOCK_NB) on any handle and that handle is not provably released before the write. An operation that cannot be folded to a known value counts as exclusive. LOCK_SH does not serialize writers and never counts.
  • A release is flock with a known non-exclusive operation (LOCK_UN, or a downgrade to LOCK_SH) or fclose on the same handle expression. It ends the lock only when it is a statement completed before the write in a statement list enclosing the write, or when it sits in the statement list that also contains the acquisition. A release inside a branch that leaves (the failed-lock cleanup, a finally block) does not end it.
  • D1c. An application (non-builtin) function, method or static call completed before the earliest read may take the lock itself: no report when its callee text contains lock without containing unlock, or when one of its arguments is the same proven path. Other unknown calls do not silence the report.
  • Resolve builtin calls and named arguments before checking the contract. Project-local callees are considered only when their bodies or immutable summaries establish the required facts. The default is enabled, with positive proof required.
  • For flow-dependent conditions, require a reachable source-to-use path with no intervening invalidation. Unknown calls, ambiguous aliases, recursion or analysis-budget exhaustion cannot establish definite facts.

Exceptions (no report) ​

  • Separate destinations, read-only paths, established spanning locks (D1a–D1c), atomic database alternatives and unknown path identity are excluded.
  • A lock taken after the read, a shared lock, and a lock released or closed before the read or between the read and the write are not spanning locks: the report stays.
  • Unresolved targets, incomplete required facts, suppressed findings and PHP versions lacking the referenced API or language feature produce no report. User declarations shadowing builtin names are not builtin contracts.

Fix ​

No automatic fix: the complete locked read-modify-write protocol must handle partial reads/writes and failures.

PHP versions ​

Apply only on PHP versions supporting the referenced APIs and syntax (within PHP 5.3–8.5). Builtin behavior and argument contracts follow the configured PHP version. Extension-specific calls require resolved extension API symbols; do not treat same-named application functions as extension calls.

Options ​

None. Enable or disable this rule through normal rule configuration.

Configure ​

In custos.json:

json
{
  "rules": {
    "ReadModifyWriteLockTooLate": {
      "enabled": false
    }
  }
}

Suppress ​

Before the statement or declaration (or the first statement of the file):

php
// @custos-ignore ReadModifyWriteLockTooLate

Released under the MIT License. Native rules and rules modeled on Php Inspections (EA Extended); independent clean-room implementation.