Fix CFI icall violation in Future callbacks - #1914
Conversation
Integration test with FLAKINESS (succeeded after retry)Requested by @a-maurice on commit 126a8dd
Add flaky tests to go/fpl-cpp-flake-tracker |
There was a problem hiding this comment.
Code Review
This pull request refactors the Future<T>::OnCompletion and Future<T>::AddOnCompletion implementations to avoid unsafe reinterpret_cast on callbacks, introducing a typed callback data structure and trampoline functions instead. The review feedback highlights critical safety issues regarding null or empty callbacks: passing a null callback can cause a crash if the future is already completed, and wrapping an empty std::function without validation can trigger a std::bad_function_call exception or crash. Suggestions are provided to handle these cases safely by using dummy callbacks or adding validity checks.
| if (callback == nullptr) { | ||
| api_->AddCompletionCallback(handle_, nullptr, nullptr, nullptr, | ||
| /*clear_existing_callbacks=*/true); |
There was a problem hiding this comment.
If callback is nullptr and the future is already completed, passing nullptr to api_->AddCompletionCallback will cause a crash (null pointer dereference) in ReferenceCountedFutureImpl::RunCallback when it attempts to execute the callback. To prevent this, pass a dummy callback that does nothing instead of nullptr.
| if (callback == nullptr) { | |
| api_->AddCompletionCallback(handle_, nullptr, nullptr, nullptr, | |
| /*clear_existing_callbacks=*/true); | |
| if (callback == nullptr) { | |
| api_->AddCompletionCallback(handle_, [](const FutureBase&, void*){}, nullptr, nullptr, | |
| /*clear_existing_callbacks=*/true); |
| FutureBase::OnCompletion([callback](const FutureBase& future) { | ||
| callback(static_cast<const Future<ResultType>&>(future)); | ||
| }); |
There was a problem hiding this comment.
If callback is an empty std::function (i.e., !callback), executing it inside the lambda will throw std::bad_function_call or cause an immediate crash if exceptions are disabled. To prevent this, check if the callback is valid. If it is empty, safely clear the existing callback using a dummy callback.
if (!callback) {
FutureBase::OnCompletion([](const FutureBase&, void*){}, nullptr);
} else {
FutureBase::OnCompletion([callback](const FutureBase& future) {
callback(static_cast<const Future<ResultType>&>(future));
});
}| return FutureBase::AddOnCompletion([callback](const FutureBase& future) { | ||
| callback(static_cast<const Future<ResultType>&>(future)); | ||
| }); |
There was a problem hiding this comment.
If callback is an empty std::function (i.e., !callback), executing it inside the lambda will throw std::bad_function_call or cause an immediate crash if exceptions are disabled. To prevent this, check if the callback is valid and return an empty CompletionCallbackHandle if it is empty.
if (!callback) {
return CompletionCallbackHandle();
}
return FutureBase::AddOnCompletion([callback](const FutureBase& future) {
callback(static_cast<const Future<ResultType>&>(future));
});
Description
Eliminate unsafe reinterpret_cast of TypedCompletionCallback and std::function completion callbacks to FutureBase::CompletionCallback in Future::OnCompletion and Future::AddOnCompletion.
Introduce type-safe trampoline functions and wrapper data structures (TypedCompletionCallbackTrampoline and TypedCompletionCallbackData) in firebase::detail to bridge the typed callback to the base callback signature expected by ReferenceCountedFutureImpl::RunCallback, avoiding undefined behavior and runtime CFI type check aborts.
Testing
Type of Change
Place an
xthe applicable box:Notes
Release Notessection ofrelease_build_files/readme.md.