Skip to content

CFAttributedString: fix allocation, use-after-free and OOB bugs - #53

Merged
fredkiefer merged 3 commits into
gnustep:masterfrom
DTW-Thalion:fix/cfattributedstring-alloc
Aug 16, 2026
Merged

fredkiefer merged 3 commits into
gnustep:masterfrom
DTW-Thalion:fix/cfattributedstring-alloc

Conversation

@DTW-Thalion

Copy link
Copy Markdown
Contributor

CFMutableAttributedString was unusable: every operation corrupted memory.

  • CFATTRIBUTESTRING_SIZE / CFMUTABLEATTRIBUTESTRING_SIZE computed the
    instance's extra size as "sizeof(CFRuntimeClass) - sizeof(struct ...)"
    instead of "sizeof(struct ...) - sizeof(CFRuntimeBase)", undersizing
    the object so CFAttributedStringCreateMutable() wrote its own ivars
    past the end of the allocation.

  • CFAttributedStringGetBlankAttribute() created a dictionary, cached a
    copy of it, released the original, and then dereferenced the
    released original (use-after-free). Keep a reference to the cached
    copy instead.

  • InsertAttributesAtIndex() (grow) and RemoveAttributesAtIndex() (shrink)
    passed the element count, not a byte count, to CFAllocatorReallocate()
    and never updated _attribCap, so the attribute array was reallocated
    far too small and overrun.

  • CFAttributedStringCoalesce() compared each entry with its predecessor
    starting at index range.location, reading element [-1] when the range
    began at 0.

The attribute cache relies on CFBag being a counted collection, but
CFBagAddValue() did not increase the count of an already-present value
(GSHashTableAddValue only ever inserts new entries), so uncaching a shared
attribute -- e.g. the blank attribute -- freed it while it was still in
use. Add GSHashTableAddValueCounted() and use it from CFBagAddValue() so
the count tracks the number of references.

With these fixes the CFAttributedString tests pass (creation, attribute
get/set, and growing the attribute array past its initial capacity);
previously they crashed. A separate imbalance in the attribute cache's
reference accounting still leaks some cached attributes; that is left for
a follow-up.

@HendrikHuebner

Copy link
Copy Markdown
Contributor

A test failed here as well.

@DTW-Thalion

Copy link
Copy Markdown
Contributor Author

A test failed here as well.

I will look at these three within the hour - just finishing up something in libs-gui.

@HendrikHuebner

Copy link
Copy Markdown
Contributor

No worries, I think it will take quite a while until all of these are getting reviewed anyways lol. At least there is a proper CI now.

CFMutableAttributedString was unusable: every operation corrupted memory.

  * CFATTRIBUTESTRING_SIZE / CFMUTABLEATTRIBUTESTRING_SIZE computed the
    instance's extra size as "sizeof(CFRuntimeClass) - sizeof(struct ...)"
    instead of "sizeof(struct ...) - sizeof(CFRuntimeBase)", undersizing
    the object so CFAttributedStringCreateMutable() wrote its own ivars
    past the end of the allocation.

  * CFAttributedStringGetBlankAttribute() created a dictionary, cached a
    *copy* of it, released the original, and then dereferenced the
    released original (use-after-free).  Keep a reference to the cached
    copy instead.

  * InsertAttributesAtIndex() (grow) and RemoveAttributesAtIndex() (shrink)
    passed the element count, not a byte count, to CFAllocatorReallocate()
    and never updated _attribCap, so the attribute array was reallocated
    far too small and overrun.

  * CFAttributedStringCoalesce() compared each entry with its predecessor
    starting at index range.location, reading element [-1] when the range
    began at 0.

The attribute cache relies on CFBag being a counted collection, but
CFBagAddValue() did not increase the count of an already-present value
(GSHashTableAddValue only ever inserts new entries), so uncaching a shared
attribute -- e.g. the blank attribute -- freed it while it was still in
use.  Add GSHashTableAddValueCounted() and use it from CFBagAddValue() so
the count tracks the number of references.

With these fixes the CFAttributedString tests pass (creation, attribute
get/set, and growing the attribute array past its initial capacity);
previously they crashed.  A separate imbalance in the attribute cache's
reference accounting still leaks some cached attributes; that is left for
a follow-up.
CFAttributedStringSetAttributes passed the last touched run index to
CFAttributedStringCoalesce as the length of the range to coalesce, so the
scan could run past the end of the attribute array, comparing and then
uncaching runs that were never allocated.  That freed an attribute
dictionary still referenced by a live run and later faulted when the run
was uncached again.  The coalesce scan now stops at the current run count.

While here, refresh the local array pointer after an insertion that may
reallocate the run array, and correct the run-removal shift, which stopped
one element short and dropped the last run.
@DTW-Thalion
DTW-Thalion force-pushed the fix/cfattributedstring-alloc branch from 64a887a to 97af53f Compare July 27, 2026 17:45
@fredkiefer
fredkiefer merged commit 2d297a7 into gnustep:master Aug 16, 2026
3 checks passed
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