Skip to content

fix: Make live instances unusable after deleteMMKV() instead of crashing - #1094

Open
qutrek wants to merge 1 commit into
margelo:mainfrom
qutrek:fix/delete-live-instances
Open

qutrek wants to merge 1 commit into
margelo:mainfrom
qutrek:fix/delete-live-instances

Conversation

@qutrek

@qutrek qutrek commented Oct 2, 2026

Copy link
Copy Markdown

Fixes #1082

Problem

deleteMMKV(id) calls MMKV::removeStorage(id), which closes the file's native instance (MMKV::close() ends in delete this). Every HybridMMKV of that file keeps its raw MMKV*, so any later call on it uses freed memory. The app doesn't even have to touch the object again: the AppState listener added by createMMKV() calls checkContentChanged() on it the next time the app becomes active, which crashes (#1082 on iOS; on Android a SIGSEGV in pthread_mutex_lock under MMKV::checkContentChanged()) or hangs.

Fix

No changes needed in MMKV Core:

  • HybridMMKV keeps a registry of live instances (a mutex-guarded set, added in the constructor, removed in the destructor).
  • deleteMMKV(id) calls HybridMMKV::invalidateInstances(id) before MMKV::removeStorage(id). It finds the native instance of that file (id + root path, resolving an empty path to MMKV's root directory) and unlinks every HybridMMKV that points to it. MMKV caches one native instance per file, so this also covers several createMMKV() calls for the same id.
  • After that:
    • every method throws: The MMKV instance "…" has been deleted with deleteMMKV(...)! Create a new one with createMMKV(...).;
    • checkContentChanged() and trim() do nothing, since the AppState and memory-warning listeners call them;
    • id stays readable.
  • createMMKV() with the same id afterwards works as before.

invalidateInstances() takes an optional root path, so it should fit #1070: deleteMMKV(id, path) would pass its path through.

Tests

New harness test, "should make live instances unusable after deleteMMKV()". It deletes a storage while two instances of it are alive, then checks that:

  • using either instance throws;
  • the listeners' calls don't throw;
  • another storage is unaffected;
  • the id can be created again.

Ran on an Android emulator (API 37, x86_64), the same way as the harness-android workflow:

  • with the fix: all 77 harness tests pass;
  • without it (only the C++ change reverted): the app stops responding during the new test, and the harness reports the bridge stopped responding.

Also: bun run typecheck, bun run lint-ci, bun run test and the clang-format 18 check pass. The Android build has no new warnings.

Not tested on iOS (no Mac here). The change is in shared C++ only.

…shing

`MMKV::removeStorage()` closes and deletes the file's native MMKV instance,
but every `HybridMMKV` of that file kept its raw pointer to it. Any later
call used freed memory, and `createMMKV()`'s AppState listener makes one
(`checkContentChanged()`) the next time the app comes to the foreground: a
crash or a hang.

`HybridMMKV` now keeps a registry of live instances. `deleteMMKV()` unlinks
every instance of the file before deleting it: using one then throws, while
`checkContentChanged()` and `trim()` (called by the listeners) do nothing,
and `id` stays readable.

Fixes margelo#1082

@mrousavy mrousavy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the PR but the static instances is a problem imo

Comment on lines +16 to +17
std::mutex HybridMMKV::_liveInstancesMutex;
std::unordered_set<HybridMMKV*> HybridMMKV::_liveInstances;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I want to avoid keeping track of such static maps. That's the whole point of object-oriented programming to not do that.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

deleteMMKV() crashes on next foreground: AppState listener still calls checkContentChanged() on the destroyed instance

2 participants