Skip to content

CFTimeZone: retain timezones stored in the name cache (use-after-free)#67

Open
DTW-Thalion wants to merge 3 commits into
gnustep:masterfrom
DTW-Thalion:fix/cftimezone-cache-retain
Open

CFTimeZone: retain timezones stored in the name cache (use-after-free)#67
DTW-Thalion wants to merge 3 commits into
gnustep:masterfrom
DTW-Thalion:fix/cftimezone-cache-retain

Conversation

@DTW-Thalion

Copy link
Copy Markdown
Contributor

Summary

CFTimeZoneCreate caches every timezone it builds in _kCFTimeZoneCache, but
the cache is created with NULL value callbacks, so it does not retain the
stored objects:

_kCFTimeZoneCache = CFDictionaryCreateMutable (
  kCFAllocatorSystemDefault, 0,
  &kCFCopyStringDictionaryKeyCallBacks, NULL);   /* values not retained */

A later call returns the cached entry via CFRetain(old). Once the original
owner releases its reference, the timezone is deallocated while the cache
still holds a dangling pointer to it
. The next lookup of the same name returns
freed memory, and the CFRetain on that pointer dereferences it through
CFGetTypeID — a use-after-free.

There is no removal path: CFTimeZoneFinalize does not touch the cache, and
nothing calls CFDictionaryRemoveValue on it. So the only correct lifetime is
for the cache to own a reference to each entry.

Fix

Give the cache kCFTypeDictionaryValueCallBacks so it retains the timezones it
stores. The cache then behaves as an intern table holding one reference per
distinct zone name for the process lifetime (bounded by the number of zone
names). The insert path returns the original creation reference, so the caller
still receives the +1 it expects — no double-retain, no leaked caller
reference.

Reproduction (ThreadSanitizer)

A multi-threaded harness that creates and releases timezones by name aborts with
a heap-use-after-free in CFGetTypeID (reached from CFTimeZoneCreate's
CFRetain of a cached entry). With this change that use-after-free no longer
occurs.

Relationship to #64

This is a distinct bug from the cache-lock race in #64. #64 fixes the
unlocked CFDictionaryGetValue racing a concurrent insert's rehash (a
bucket-array race inside the dictionary); this fixes the lifetime of the stored
objects. They are complementary — with both applied the harness above runs
fully clean under TSan (0 data races, 0 use-after-free, 0 SEGV).

Note: both touch the same CFDictionaryCreateMutable call, so whichever lands
first leaves a trivial one-line rebase for the other (the NULL
&kCFTypeDictionaryValueCallBacks argument on the relocated call).

CFTimeZoneCreate caches each created timezone in _kCFTimeZoneCache, but
the cache was made with NULL value callbacks, so it did not retain the
stored objects.  CFTimeZoneCreate returns the cached entry with
CFRetain(old); once the original owner releases that reference the
timezone is deallocated while the cache still holds a dangling pointer to
it, and the next lookup of the same name returns freed memory (a
use-after-free in CFGetTypeID via the returned CFRetain).

Give the cache kCFTypeDictionaryValueCallBacks so it retains the
timezones it stores.  There is no removal path (CFTimeZoneFinalize does
not touch the cache), so the cache acts as an intern table holding one
reference per distinct zone name for the process lifetime.  The insert
path returns the original creation reference, so the caller still owns
the +1 it expects.
Create a named time zone, release the caller's only reference, then look
the same name up again.  When the cache stored zones without retaining
them the second lookup returned and retained freed memory; under
AddressSanitizer this aborts with a heap-use-after-free in
CFGetTypeID/CFRetain.  With the cache retaining its entries the test
passes.
Comment thread Tests/CFTimeZone/cache_retain.m Outdated

/* Regression test for the time-zone name cache use-after-free.

CFTimeZoneCreate caches every created zone in the global name cache.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I don't think we want to keep this comment around. Its meaningless outside of this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Trimmed to a one-line description of what the test checks (50fbc99).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No, remove the entire comment except for the first line "Regression test..."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — the comment is now just /* Regression test for the time-zone name cache use-after-free. */ (ac6f02e).

Drop the PR-specific bug narrative from cache_retain.m and keep a short
note of what the test checks.
@DTW-Thalion DTW-Thalion force-pushed the fix/cftimezone-cache-retain branch from 50fbc99 to ac6f02e Compare June 30, 2026 14:23
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.

2 participants