The bug was in production for eleven months before anyone found it.
A payments platform had an authorization check:
if not user.is_suspended or user.has_override:
process_payment(user, amount)The intent was: “process if the user is not suspended, OR if they have a manual override.” The code reads like that intent. But it does not implement it.
The correct logic is: “process if (not suspended) OR (has override).”
The code reads not user.is_suspended or user.has_override,
which, by operator precedence, evaluates as
(not user.is_suspended) or (user.has_override).
Those are the same thing. So where is the bug?
The bug was in a different function — one that computed
is_suspended by combining two flags:
# Original intent: suspended if account_frozen AND payment_blocked
user.is_suspended = account_frozen and payment_blockedA developer needed to change the policy: “suspended if account_frozen OR payment_blocked.” They updated the assignment:
user.is_suspended = account_frozen or payment_blockedThen the authorization check, which negated
is_suspended, silently changed meaning. The original check
read: “not (frozen AND blocked).” By De Morgan’s Law, this equals “(not
frozen) OR (not blocked)” — which is True for almost every user. The
intent was never this permissive. Accounts that were frozen but not
blocked could process payments.
Eleven months. The fix was one line. The damage was regulatory.
De Morgan’s Laws are not an academic curiosity. They are the source of real authorization failures.