~/teardowns/TD-036.md
←Back to teardowns
# TD-036MediumRuntime

The Loop That Changed Only the Last Row

A by-reference foreach leaves a dangling reference that corrupts the total

Published Oct 29, 2026

Executive Summary

A reporting script applies VAT with a by-reference foreach and forgets unset($item). The next loop reuses $item, which still points at the last array element, so every iteration overwrites it. The final totals are wrong, and the bug is invisible for small arrays.

The Prompt

“Write a script that applies 20% VAT to each line item and then prints the totals per category.”

The Flaw

After the first loop, $item is still a reference to the last element. When the second foreach reuses the same variable, every iteration writes the current element into that referenced slot — so the last element ends up holding the second-to-last element's value (240 instead of 360 above). No error, no warning; the printed totals are just wrong, and the corruption stays invisible until someone re-adds a row by hand.

Risk & Impact

[Correctness] The reported totals are silently wrong. An invoice, commission, or payout calculation built on this script bills the wrong amount.

Cost: Wrong figures shipped to customers, discovered only when someone manually re-adds a row.

Code Comparison

❌ Vulnerable (AI generated)

report.phpphp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
// report.php
$items = [
['category' => 'a', 'total' => 100],
['category' => 'b', 'total' => 200],
['category' => 'c', 'total' => 300],
];
foreach ($items as &$item) {
$item['total'] *= 1.2;
}
// no unset($item) — $item still references $items[2]
foreach ($items as $item) {
echo $item['category'] . ': ' . $item['total'] . PHP_EOL;
}
?>

The Fix

Avoid by-reference foreach loops — write back by index — or unset($item) immediately after the reference loop so the reference cannot leak.

Prevention

  • [linter] PHP_CodeSniffer / PHPStan rules that flag a foreach-by-reference that is not followed by an unset() on the same variable.
  • [ci] A test that asserts every row's value (not just the count) catches the corrupted last element.
  • [manual] Prefer index-based writes over references when the agent mutates arrays (rule 3), and require tests that check values, not counts (rule 9).

Why This Happened

Root cause: the prompt omitted Rule 3 and Rule 9 — the guardrails that would have caught this. See the full guide →

Sources

Is your codebase vulnerable to this?

Run Diagnostic on My Project →