firebase / firebase/firebase-unity-sdk
[Bug] Analytics: SIGABRT (std::bad_alloc) in LogEvent — finalizer frees the SWIG StringList vector while the native wrapper copies it (IL2CPP, missing GC.KeepAlive)
- Dominant language
- C#
- Stars
- 320
- Forks
- 60
- Avg merge
- 13h 32m
- Merged PRs (30d)
- 13
Description
### Description
A production iOS build aborted with an uncaught `std::bad_alloc` inside
`Firebase_Analytics_CSharp_LogEvent__SWIG_5`, on an IL2CPP threadpool worker, ~2.5 s after launch.
Reading the device `.ips` against the SDK binaries shows a use-after-free race built into the
generated binding:
1. `FirebaseAnalytics.LogEvent(string, IEnumerable)` builds a `StringList` /
`VariantList` and calls `FirebaseAnalyticsInternal.LogEvent(name, val, val2)`.
2. The generated proxy passes only `StringList.getCPtr(...)` (`HandleRef`); after that expression
there is **no further managed use of the wrapper objects, and no `GC.KeepAlive` anywhere in
`Firebase.Analytics.dll`**. Under IL2CPP, `HandleRef`'s during-call keep-alive guarantee is not
honored — the generated C++ (clang `-O2`) can drop the last reference before the native call
completes, and Boehm's conservative scan has nothing left to find.
3. Natively, the SWIG helper is declared **by value** (`analytics.i`, current `main`):
`void LogEvent(const char* name, std::vector parameter_names, std::vector parameter_values)`
— so the wrapper *copies* the caller's vector.
4. If a GC lands inside that copy window, `~StringList()` → `delete_StringList` frees the vector
(and its strings) on the finalizer thread, mid-copy. The copy then reads a freed
`basic_string` header, gets a garbage length, and `operator new` throws `std::bad_alloc`,
which is uncaught in native code → `std::terminate` → SIGABRT.
The crash frames match this exactly — the throw is inside the *copy of the source strings*, not a
vector grow:
```
libc++abi.dylib operator new(unsigned long) (.cold.1) ← throws std::bad_alloc
libusd_ms.dylib (basic_string copy-construction chain) ← template code coalesced into a shared-cache image
libusd_ms.dylib std::vector::__construct_at_end(...)
UnityFramework std::vector::__assign_with_size(...)
UnityFramework std::vector::assign(...)
UnityFramework Firebase_Analytics_CSharp_LogEvent__SWIG_5
(thread: "IL2CPP Threadpool worker")
Termination: SIGABRT, abort() called; ASI: "abort() called"
```
Ruled out from the same report:
- **Not OOM** — vmSummary at death: 673 KB written / 259 MB virtual writable. The process was
seconds old; `operator new` was handed an absurd size, not squeezed.
- **Not version skew** — pods uniform at 12.17.0, SWIG export names in `libFirebaseCppAnalytics.a`
match the DLL's `DllImport` entrypoints (`LogEvent__SWIG_0`–`5`).
- `SetDefaultEventParameters(StringList, VariantList)` has the identical shape and is equally
affected.
This is the same failure class as the GC-finalizer destructor crashes long reported for Messaging
(#293, #1091) and quickstart-unity#191, landing on the Analytics marshaling path.
### Reproducing the issue
Field frequency is very low (one event across ~4,000 reporting installs) because it needs a GC to
land inside a microseconds-wide native copy. To force it: call parametered
`FirebaseAnalytics.LogEvent` in a tight loop from `Task.Run`, while a second thread loops
`GC.Collect()`, in an IL2CPP iOS build — under Guard Malloc the use-after-free becomes a
deterministic crash at the free site.
Suggested fix: `GC.KeepAlive(parameterNames); GC.KeepAlive(parameterValues);` after the PINVOKE
call in the generated proxies (the standard mitigation for `HandleRef` under IL2CPP), or copy the
vectors before returning control in a context where the wrapper is provably alive.
### Firebase Unity SDK Version
13.15.0
### Unity editor version
6000.3.22f1
### Installation Method
.unitypackage (Analytics, Crashlytics, Messaging, App)
### Problematic Firebase Component(s)
Analytics
### Targeted Platform(s)
Apple Platforms (iOS 26.6, iPhone 16, App Store / TestFlight build)
### Scripting Runtime
IL2CPP
### Expected Behavior
`LogEvent` marshaling holds its native containers alive for the duration of the native call.
### Actual Behavior
One-off SIGABRT via uncaught `std::bad_alloc` as described; Crashlytics ingests this crash class
with an empty stack, so the evidence above comes from the on-device `.ips` (available on request,
minus PII).
Contributor guide
Assessment
This issue has not been assessed yet.