mutated × shared — the answer changes between runs
Stale-Cache Heisenbug
Caching a result that depends on hidden mutable state serves the old answer forever — nobody invalidated it.
01the recipe
In the wild
compound ofCache InvalidationImpure FunctionsCWE-1108 Global ReliancecompoundCWE-471 MAID (Modification of Immutable Data)
example.py
# SMELL: cache a value that depends on hidden, mutable state.
# (cache-invalidation x impure-functions)
_cache = {}
def price(item):
if item not in _cache:
_cache[item] = item.base * TAX_RATE # TAX_RATE: a mutable global
return _cache[item]
TAX_RATE = 1.2 # later, far away, this changes -- the cache never hears
# RIGHT: make the function pure; key on every input.
def price(item, tax):
return item.base * tax # same inputs -> same outputThe first call freezes a value computed from a global that later moves. Whether you see fresh or stale output depends on call order and cache warmth.
// observed
stale: first call wins; later TAX_RATE changes ignored right: result tracks its inputs, every call
02weakness catalog
Mapped weaknesses (CWE)
On its own, this defect is catalogued by MITRE as one or more of these weaknesses. The exploitable vulnerability usually appears only when it chains or combines with another.