Skip to content

Long expireDuration causes keys to be deleted immediately (uint32 overflow) #1665

@Fxxxxxx

Description

@Fxxxxxx

Note

Crash issues will be ignored and closed within a week if no logs are provided.

This is not a crash. It is a key-expiration logic bug. Crash logs do not apply.


The language of MMKV

Objective-C / Swift (also reproducible in C++ Core; affects all platforms that use key expiration)


The version of MMKV

v2.4.0 (latest on master as of May 2026). The bug is in Core expire-timestamp logic and likely affects older versions that support enableAutoKeyExpire as well.

Note: For versions older than the latest, please upgrade before posting any issue. We don't have much time for old-version tech support.


The platform of MMKV

iOS (also applicable to Android, macOS, POSIX, etc., since the bug is in Core/MMKV.cpp)


The installation of MMKV

Git clone / CocoaPods (integrated into the app via CocoaPods)


What's the issue?

When enableAutoKeyExpire is enabled, writing a key with a very long expireDuration (e.g. 100 years in seconds) causes the value to disappear immediately on read — as if the key never existed or was already expired.

Root cause: Expire metadata stores an absolute Unix timestamp (now + expireDuration) as uint32_t. When now + expireDuration exceeds UINT32_MAX, the value wraps to a timestamp far in the past (~1990). On read, MMKV treats it as expired and removes the key.

Reproduction (Objective-C):

MMKV *mmkv = [MMKV mmkvWithID:@"test_expire"];
[mmkv enableAutoKeyExpire:MMKVExpireNever]; // per-key expire only

uint32_t hundredYears = 100u * 365u * 24u * 3600u;
[mmkv setInt32:42 forKey:@"key" expireDuration:hundredYears];

int32_t value = [mmkv getInt32ForKey:@"key"];
// Expected: 42
// Actual: 0 (default); key is deleted as "expired"

Expected behavior: An excessively long expiration should behave like permanent storage (MMKVExpireNever / 0), not instant expiration.

Proposed fix: Before computing now + expireDuration, detect overflow (expireDuration > UINT32_MAX - now) and store ExpireNever (0) instead.

Approximate safe limit today: ~80 years before overflow; 100 years reliably fails in 2026.

Rationale: why overflow should mean “never expire”

expireDuration means “expire after N seconds from now.” Callers who pass a very large value (e.g. 100 years) usually intend long retention, not immediate invalidation.

Behavior Effect Matches caller intent?
Overflow → wrapped past timestamp → treat as expired Key gone on next read No — opposite of “long TTL”
Overflow → store ExpireNever (permanent) Key readable as normal Yes — closer to “as long as possible”

When now + duration cannot be represented in uint32_t, treating the key as permanent is more reasonable than treating it as already expired: it avoids silent data loss and aligns with typical use of huge durations as “effectively never expire.”


What's the log of MMKV when that happened?

Set MMKV logging to MMKVLogInfo or higher. When reading the key back, typical logs look like:

[I] <MMKV_IO.cpp:2007::getDataWithoutMTimeForKey> deleting expired key [key] in mmkv [test_expire], due date 638059694

The due date is a wrapped timestamp (~year 1990), much smaller than current now (~1.78e9), which confirms overflow rather than a real past expiry.

Write path (for reference): Core/MMKV.cpp uses getCurrentTimeInSecond() + expireDuration without an overflow check; deletion on read is in getDataWithoutMTimeForKey in Core/MMKV_IO.cpp.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions