CVE-2024-31339 AI-generated analysis
platform/packages/modules/StatsD, statsd native daemon (StatsService / utils/MultiConditionTrigger)
MultiConditionTrigger no longer runs its trigger callback on a detached std::thread; the thread becomes a class member joined in the destructor, and StatsService gains a condition-variable based early-termination request so the delayed onStatsdInitCompleted handler exits promptly on shutdown, termination, destruction or StatsCompanionService death.
Root cause
MultiConditionTrigger (constructor and markComplete) launched `std::thread executorThread([this] { mTrigger(); })` and immediately called detach(). The lambda captures `this` and calls mTrigger, a std::function member. Nothing tied the thread's lifetime to the object. If the MultiConditionTrigger was destroyed while the thread was still running, the thread could access freed memory. In practice the owner is StatsService, which holds it as mBootCompleteTrigger and whose trigger runs StatsService::onStatsdInitCompleted. The window could be long because onStatsdInitCompleted, inside a conditional block, slept for mInitEventDelaySecs (`std::this_thread::sleep_for`) before calling mProcessor->onStatsdInitCompleted.
How the fix works
- statsd/src/utils/MultiConditionTrigger.h: adds `~MultiConditionTrigger()`, private `startExecutorThread()` and member `std::unique_ptr<std::thread> mExecutorThread`. It includes <thread>, and the class doc now says the thread is joined at destructor time.
- statsd/src/utils/MultiConditionTrigger.cpp: both thread-creation sites (the constructor when mCompleted is initially true, and markComplete when the last condition completes) now call startExecutorThread(). That function stores the thread in mExecutorThread instead of detaching it. The new destructor joins mExecutorThread if it is non-null and joinable, so the object cannot be destroyed while the callback is running.
- statsd/src/StatsService.h: adds `std::condition_variable mStatsdInitCompletedHandlerTerminationFlag`, `std::mutex mStatsdInitCompletedHandlerTerminationFlagMutex`, `bool mStatsdInitCompletedHandlerTerminationRequested = false` and the method `onStatsdInitCompletedHandlerTermination()`. The new members are declared immediately before mBootCompleteTrigger, so they are destroyed after it.
- statsd/src/StatsService.cpp: StatsService::onStatsdInitCompleted replaces sleep_for(mInitEventDelaySecs) with `wait_for` on the condition variable, using mStatsdInitCompletedHandlerTerminationRequested as the predicate. If termination is requested during the delay, the handler returns early without calling mProcessor->onStatsdInitCompleted.
- statsd/src/StatsService.cpp: the new StatsService::onStatsdInitCompletedHandlerTermination() sets the flag under the mutex and calls notify_all(). It is called from StatsService::~StatsService (as the first statement), StatsService::informDeviceShutdown, StatsService::Terminate and StatsService::statsCompanionServiceDiedImpl, so the join in ~MultiConditionTrigger does not block for the full delay.
- statsd/tests/utils/MultiConditionTrigger_test.cpp: adds TestTriggerHasSleep, where a dependency goes out of scope after the trigger and the test checks that the join prevents access to the destroyed object. It also adds TestTriggerHasSleepEarlyTermination, which checks that early termination finishes in about 100ms rather than 1s.
Completeness
- Both thread-creation paths in MultiConditionTrigger (the constructor and markComplete) go through startExecutorThread and are joined in the destructor. Only one thread can be created because markComplete returns early under the mutex once mCompleted is set, so a single unique_ptr slot is enough.
- The early-termination check only works while the handler is inside wait_for. If the delay has already elapsed, the thread goes on to mProcessor->onStatsdInitCompleted and the destructor waits for it to finish. Whether mProcessor is still alive at that point depends on member declaration order, which the material does not show.
- The diff never resets mStatsdInitCompletedHandlerTerminationRequested to false after statsCompanionServiceDiedImpl.
- The fix covers every MultiConditionTrigger user generically. The material shows only mBootCompleteTrigger in StatsService as a user.
Behavior changes
- Destroying a MultiConditionTrigger now blocks until the trigger callback finishes (join) instead of returning immediately.
- On device shutdown (informDeviceShutdown), Terminate, StatsService destruction or StatsCompanionService death, a pending delayed boot-complete handler is cancelled. If the delay has not yet elapsed, mProcessor->onStatsdInitCompleted is not called and a VLOG message is emitted.
- The mInitEventDelaySecs delay now uses condition_variable::wait_for instead of sleep_for. Timing is otherwise unchanged.
Per major
- Android 14: same fix: commit cb201622f3d4 in android-security-14.0.0_r10 (cherry-pick of internal commit 30775d2bb2e2, Change-Id I7036eb3d506e8ca88e4a5faa6275dc4cba8020ee). At android-security-14.0.0_r29 the patched region is fully present (8/8 lines).
- Android 15: not affected (not listed by the bulletin); the patched code is present at android-security-15.0.0_r17 (8/8 lines).
- Android 16: not affected (not listed by the bulletin); the patched code is present at android-security-16.0.0_r8 (8/8 lines).
- Android 17: not affected (not listed by the bulletin); the patched code is present at android-security-17.0.0_r1 (8/8 lines).
Unknowns
- How the vulnerable teardown is reached from a lower-privilege context (the bulletin rates it High EoP but the material does not show the trigger path).
- Whether and how the fix was applied to Android 12, 12L and 13 (listed as affected by the bulletin), and whether those branches have the init delay.
- The value and source of mInitEventDelaySecs, the condition that guards the delay, and the declaration order of StatsService members such as mProcessor relative to mBootCompleteTrigger.