PHPOffice/PhpSpreadsheet · error · PhpOffice\PhpSpreadsheet\Reader\Exception

Detected loop while iterating blocks

Error message

Detected loop while iterating blocks

What it means

Thrown by OLERead::catchLoop() when a block identifier repeats while walking the OLE block chains (small-block depot / directory streams). Compound documents link blocks in acyclic chains terminated by a special END marker; revisiting a block number proves the chain is cyclic, and parsing stops rather than looping forever.

Source

Thrown at src/PhpSpreadsheet/Shared/OLERead.php:164

            $pos = ($sbdBlock + 1) * self::BIG_BLOCK_SIZE;

            $this->smallBlockChain .= substr($this->data, $pos, 4 * $bbs);
            $pos += 4 * $bbs;

            $sbdBlock = self::getInt4d($this->bigBlockChain, $sbdBlock * 4);
        }

        // read the directory stream
        $block = $this->rootStartBlock;
        $this->entry = $this->readData($block);

        $this->readPropertySets();
    }

    private function catchLoop(int $sbdBlock): void
    {
        if (in_array($sbdBlock, $this->possibleLoop, true)) {
            throw new ReaderException('Detected loop while iterating blocks');
        }
        $this->possibleLoop[] = $sbdBlock;
    }

    /**
     * Extract binary stream data.
     */
    public function getStream(?int $stream): ?string
    {
        if ($stream === null) {
            return null;
        }

        $streamData = '';

        if ($this->props[$stream]['size'] < self::SMALL_BLOCK_THRESHOLD) {
            /** @var int */
            $temp = $this->props[$this->rootentry]['startBlock'];

View on GitHub (pinned to 65b080eef4)

Solutions

  1. Retry from the original source and compare checksums; a cycle almost always means the copy is damaged.
  2. Open the file in Excel/LibreOffice to confirm the workbook itself is readable; if not, have it re-exported.
  3. Quarantine uploads that trigger this and alert — cyclic chains in user uploads are a strong corruption/tamper signal.
  4. Keep PhpSpreadsheet current; loop-detection and malformed-container handling improve across releases.

Example fix

// before
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load('upload.xls');
// Detected loop while iterating blocks

// after
try {
    $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load('upload.xls');
} catch (\PhpOffice\PhpSpreadsheet\Reader\Exception $e) {
    if (str_contains($e->getMessage(), 'Detected loop')) {
        quarantine('upload.xls'); // keep the bad file for analysis
    }
    throw new RuntimeException('Damaged workbook — please re-export and re-upload.', 0, $e);
}
Defensive patterns

Strategy: try-catch

Try / catch

try { $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file); }
catch (\PhpOffice\PhpSpreadsheet\Reader\Exception $e) {
    if (str_contains($e->getMessage(), 'Detected loop')) {
        quarantine($file);
        return ['error' => 'Damaged workbook — please re-export and re-upload.'];
    }
    throw $e;
}

Prevention

When it happens

Trigger: Reading a .xls whose FAT/SAT or directory block chain contains a cycle — corruption, truncation, or a deliberately crafted file. Encountered during Reader\Xls load via OLERead::read()/readData() when following rootStartBlock and dependent chains.

Common situations: Corrupted downloads or partially-written files; hostile uploads built to hang naive parsers (the guard exists precisely for them); storage-level bit rot on archived .xls files.

Related errors


AI-assisted analysis of PHPOffice/PhpSpreadsheet@65b080eef4 (2026-08-17). Data as JSON: /api/errors/b5a2d93278396eeb. Report an issue: GitHub.