Skip to content

Fix infinite loop in NSCFDictionary/NSCFSet fast enumeration#73

Open
rupertdaniel wants to merge 1 commit into
gnustep:masterfrom
rupertdaniel:fix/nscf-fast-enumeration
Open

Fix infinite loop in NSCFDictionary/NSCFSet fast enumeration#73
rupertdaniel wants to merge 1 commit into
gnustep:masterfrom
rupertdaniel:fix/nscf-fast-enumeration

Conversation

@rupertdaniel

Copy link
Copy Markdown

Summary

-[NSCFDictionary countByEnumeratingWithState:objects:count:] and the NSCFSet equivalent created a new enumerator on every call:

NSEnumerator *enuM = [self keyEnumerator];   // fresh enumerator each call
return [enuM countByEnumeratingWithState: state objects: stackbuf count: len];

The for/in fast-enumeration protocol invokes this method repeatedly until it returns 0, relying on NSFastEnumerationState to carry progress between calls. Because a brand-new enumerator restarts from the beginning every time, the method never returns 0 — so enumerating any toll-free-bridged CFDictionary/CFSet (e.g. one created with CFDictionaryCreateMutable) loops forever over the first batch of elements.

Fix

Iterate the backing GSHashTable directly, keeping the resume bucket index in state->extra[0]. A new private helper GSHashTableGetKeysFromCursor() reads live buckets in place, so there is no per-call allocation and no snapshot to free/leak. This mirrors the cursor-in-extra[] technique gnustep-base's own GSDictionary uses for fast enumeration.

  • Source/GSHashTable.{c,h} — add GSHashTableGetKeysFromCursor().
  • Source/NSCFDictionary.m, Source/NSCFSet.m — implement countByEnumeratingWithState:objects:count: on top of it.

NSFastEnumerationState stays confined to the .m bridge files; the C core gains only a plain CFIndex/const void ** iterator, so no Foundation dependency is introduced into GSHashTable.

Testing

Built with the MSVC toolchain and verified that for (id key in dict) over a CFDictionaryCreateMutable-created dictionary now enumerates each element once and terminates, where it previously looped indefinitely.

-[NSCFDictionary countByEnumeratingWithState:objects:count:] and the
NSCFSet equivalent created a fresh enumerator on every call. The for/in
protocol invokes this method repeatedly until it returns 0, relying on
NSFastEnumerationState to carry progress between calls; because a new
enumerator restarted from the beginning each time, the method never
returned 0 and enumeration looped forever over the first batch.

Iterate the backing GSHashTable directly instead, keeping the resume
bucket index in state->extra[0]. Add GSHashTableGetKeysFromCursor(), a
resumable key iterator that reads live buckets in place, so nothing is
allocated per call and there is no snapshot to leak.
@triplef

triplef commented Jul 2, 2026

Copy link
Copy Markdown
Member

@HendrikHuebner do you mind reviewing this one as well? We ran into this in our app.

@HendrikHuebner HendrikHuebner left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Do we have any tests for this?

Comment thread Source/GSHashTable.h
Comment on lines +118 to +120
* bucket *cursor, advances *cursor past them, and returns the number written
* (0 once the table is exhausted). Reads live buckets in place and allocates
* nothing, so it is suitable as a fast-enumeration primitive.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* bucket *cursor, advances *cursor past them, and returns the number written
* (0 once the table is exhausted). Reads live buckets in place and allocates
* nothing, so it is suitable as a fast-enumeration primitive.
* bucket *cursor, advances *cursor past them, and returns the number of written keys
* (0 once the table is exhausted).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants