ReadModifyWriteLockTooLate
warning native on by defaultGroup: 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
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
flockcall that ends before the earliest feeding read takes an exclusive lock (LOCK_EX, optionally withLOCK_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_SHdoes not serialize writers and never counts. - A release is
flockwith a known non-exclusive operation (LOCK_UN, or a downgrade toLOCK_SH) orfcloseon 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, afinallyblock) 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
lockwithout containingunlock, 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:
{
"rules": {
"ReadModifyWriteLockTooLate": {
"enabled": false
}
}
}Suppress
Before the statement or declaration (or the first statement of the file):
// @custos-ignore ReadModifyWriteLockTooLate