Skip to content

Fix infinite loop / duplicate rows on cyclic ESE leaf-page chain#2208

Open
uziel110 wants to merge 1 commit into
fortra:masterfrom
uziel110:fix/ese-leaf-page-cycle-detection
Open

Fix infinite loop / duplicate rows on cyclic ESE leaf-page chain#2208
uziel110 wants to merge 1 commit into
fortra:masterfrom
uziel110:fix/ese-leaf-page-cycle-detection

Conversation

@uziel110

Copy link
Copy Markdown

Summary

ESENT_DB.getNextRow() walks an ESE table's leaf pages by following each page's NextPageNumber. A healthy leaf chain is acyclic and terminates at NextPageNumber == 0.

However, a database in a dirty-shutdown / inconsistent state (e.g. an ntds.dit whose B-tree pointers were never committed) can contain a leaf chain whose NextPageNumber links back to an already-visited page. The current code follows that back-edge unconditionally, so it loops forever — re-emitting every row once per lap. In practice this hangs secretsdump.py LOCAL parsing and produces endless duplicate hashes.

Fix

Track the set of leaf pages already visited for each cursor (VisitedPages, plus CurrentPageNumber for accurate logging). When NextPageNumber points to a page that has already been walked, stop the table read and log an error noting the database may be inconsistent.

Because a valid ESE leaf chain never revisits a page, this only triggers on corrupt input and is a no-op for healthy databases. At the point the cycle is detected, every reachable page has already been read exactly once, so the table walk is complete.

Testing

  • Verified against a dirty-shutdown ntds.dit that previously caused secretsdump.py -ntds ... LOCAL to loop and emit duplicate machine/user hashes — it now completes cleanly with the cycle-detection error logged.
  • Healthy databases parse identically to before (no behavior change on the non-cyclic path).

🤖 Generated with Claude Code

A dirty-shutdown or inconsistent ESE database (e.g. an ntds.dit whose
B-tree pointers were never committed) can contain a leaf-page chain whose
NextPageNumber links back to an already-visited page. getNextRow() would
follow that back-edge forever, re-emitting every row once per lap and
hanging secretsdump's LOCAL parsing.

Track visited leaf pages per cursor and stop the table walk when
NextPageNumber points to a page already seen, logging an error that the
database may be in a dirty-shutdown / inconsistent state. A healthy ESE
leaf chain is acyclic and terminates at NextPageNumber == 0, so this only
triggers on corrupt input and never affects valid databases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@anadrianmanrique anadrianmanrique added the in review This issue or pull request is being analyzed label Jun 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

in review This issue or pull request is being analyzed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants