-
Notifications
You must be signed in to change notification settings - Fork 34
fix(launch_manager): fix ControlProvider leak via unique_ptr #654
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a2d7f38
06678be
28b0c01
ab61cb6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| /******************************************************************************** | ||
| * Copyright (c) 2026 Contributors to the Eclipse Foundation | ||
| * | ||
| * See the NOTICE file(s) distributed with this work for additional | ||
| * information regarding copyright ownership. | ||
| * | ||
| * This program and the accompanying materials are made available under the | ||
| * terms of the Apache License Version 2.0 which is available at | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| ********************************************************************************/ | ||
|
|
||
| #include "score/mw/launch_manager/control/control_provider.hpp" | ||
| #include "score/mw/launch_manager/process_group_manager/irun_target_control.hpp" | ||
|
|
||
| #include "score/mw/com/runtime.h" | ||
| #include "score/string_manipulation/arguments/arguments.h" | ||
|
|
||
| #include <gtest/gtest.h> | ||
|
|
||
| #include <memory> | ||
| #include <type_traits> | ||
| #include <utility> | ||
|
|
||
| namespace score::mw::lifecycle::internal | ||
| { | ||
| namespace | ||
| { | ||
|
|
||
| // Locks in the contract from the class comment in control_provider.hpp: the registered callbacks | ||
| // capture the ControlProvider's own address, so it must stay neither movable nor copyable, and | ||
| // is handed out as a `std::unique_ptr` instead. A regression here (e.g. someone defaulting the | ||
| // move ops) would silently reintroduce a dangling-callback bug. | ||
| static_assert(!std::is_move_constructible_v<ControlProvider>); | ||
| static_assert(!std::is_move_assignable_v<ControlProvider>); | ||
| static_assert(!std::is_copy_constructible_v<ControlProvider>); | ||
| static_assert(!std::is_copy_assignable_v<ControlProvider>); | ||
|
|
||
| // `Create()` never calls back into `graph` before returning, so a no-op stub is sufficient for | ||
| // `getActiveRunTarget`/`setRequestedRunTarget`. `registerActiveRunTargetCallback` is captured so | ||
| // the test below can invoke it directly, as if a real transition had just completed. | ||
| class FakeRunTargetControl : public IRunTargetControl | ||
| { | ||
| public: | ||
| [[nodiscard]] score::Result<IdentifierHash> getActiveRunTarget() const noexcept override | ||
| { | ||
| return MakeUnexpected(ExecErrc::kCommunicationError); | ||
| } | ||
|
|
||
| [[nodiscard]] score::Result<void> setRequestedRunTarget(IdentifierHash /*run_target*/) noexcept override | ||
| { | ||
| return {}; | ||
| } | ||
|
|
||
| void registerActiveRunTargetCallback(ActivationCallbackT callback) noexcept override | ||
| { | ||
| callback_ = std::move(callback); | ||
| } | ||
|
|
||
| void TriggerActivation(IdentifierHash state, RunTargetActivationSource source) | ||
| { | ||
| ASSERT_TRUE(static_cast<bool>(callback_)) << "registerActiveRunTargetCallback was never called"; | ||
| callback_(state, source); | ||
| } | ||
|
|
||
| private: | ||
| ActivationCallbackT callback_; | ||
| }; | ||
|
|
||
| class ControlProviderUT : public ::testing::Test | ||
| { | ||
| protected: | ||
| FakeRunTargetControl graph_; | ||
| }; | ||
|
|
||
| TEST_F(ControlProviderUT, StaticAssertionsCompiled) | ||
| { | ||
| // The interesting assertions above run at compile time; this keeps the test target from | ||
| // being empty and gives CI something to report. | ||
| SUCCEED(); | ||
| } | ||
|
|
||
| TEST_F(ControlProviderUT, CallbacksDispatchAfterOwnershipTransfer) | ||
| { | ||
| RecordProperty( | ||
| "Description", | ||
| "After the std::unique_ptr returned by Create() is moved to a different owner, the " | ||
| "registered callbacks still dispatch through the same, still-alive ControlProvider."); | ||
|
|
||
| const IdentifierHash run_target_id{"control_provider_ut_run_target"}; | ||
|
|
||
| std::unique_ptr<ControlProvider> owner; | ||
| { | ||
| Result<std::unique_ptr<ControlProvider>> create_result = ControlProvider::Create(&graph_); | ||
| ASSERT_TRUE(create_result.has_value()); | ||
|
|
||
| // Only the pointer moves; the ControlProvider the callbacks captured stays where it is. | ||
| owner = std::move(create_result).value(); | ||
| } | ||
| ASSERT_NE(owner, nullptr); | ||
|
|
||
| // Doesn't crash and doesn't trip ASan/UBSan iff the callback dispatches through the | ||
| // still-alive ControlProvider that `owner` now holds. | ||
| graph_.TriggerActivation(run_target_id, RunTargetActivationSource::kStateManagerRequest); | ||
| } | ||
|
|
||
| TEST_F(ControlProviderUT, DestructionReleasesService) | ||
| { | ||
| RecordProperty( | ||
| "Description", | ||
| "Destroying the ControlProvider returned by Create() releases the offered service, so " | ||
| "a later Create() for the same instance succeeds again."); | ||
|
|
||
| Result<std::unique_ptr<ControlProvider>> first = ControlProvider::Create(&graph_); | ||
| ASSERT_TRUE(first.has_value()); | ||
| first.value().reset(); | ||
|
|
||
| FakeRunTargetControl second_graph; | ||
| Result<std::unique_ptr<ControlProvider>> second = ControlProvider::Create(&second_graph); | ||
| EXPECT_TRUE(second.has_value()); | ||
| } | ||
|
|
||
| } // namespace | ||
| } // namespace score::mw::lifecycle::internal | ||
|
|
||
| int main(int argc, char** argv) | ||
| { | ||
| ::testing::InitGoogleTest(&argc, argv); | ||
| score::mw::com::runtime::InitializeRuntime( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I noticed that we are not mocking mw::com. We should mock it however all the other unit tests also don't mock. I will make a Issue for this. #688 |
||
| score::string_manipulation::GetArguments(argc, const_cast<const char**>(argv))); | ||
| return RUN_ALL_TESTS(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| { | ||
| "serviceTypes": [ | ||
| { | ||
| "serviceTypeName": "/score/mw/lifecycle/LmControlService", | ||
| "version": { | ||
| "major": 1, | ||
| "minor": 0 | ||
| }, | ||
| "bindings": [ | ||
| { | ||
| "binding": "SHM", | ||
| "serviceId": 7101, | ||
| "events": [ | ||
| { | ||
| "eventName": "ActivationResult", | ||
| "eventId": 1 | ||
| } | ||
| ], | ||
| "methods": [ | ||
| { | ||
| "methodName": "ActivateRunTarget", | ||
| "methodId": 2 | ||
| }, | ||
| { | ||
| "methodName": "GetActiveRunTarget", | ||
| "methodId": 3 | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "serviceInstances": [ | ||
| { | ||
| "instanceSpecifier": "LaunchManager/StateManager/Instance", | ||
| "serviceTypeName": "/score/mw/lifecycle/LmControlService", | ||
| "version": { | ||
| "major": 1, | ||
| "minor": 0 | ||
| }, | ||
| "instances": [ | ||
| { | ||
| "instanceId": 1, | ||
| "asil-level": "QM", | ||
| "binding": "SHM", | ||
| "events": [ | ||
| { | ||
| "eventName": "ActivationResult", | ||
| "numberOfSampleSlots": 8, | ||
| "maxSubscribers": 1 | ||
| } | ||
| ], | ||
| "methods": [ | ||
| { | ||
| "methodName": "ActivateRunTarget", | ||
| "queueSize": 1 | ||
| }, | ||
| { | ||
| "methodName": "GetActiveRunTarget", | ||
| "queueSize": 1 | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "global": { | ||
| "asil-level": "QM" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -175,10 +175,19 @@ int run(int argc, const char* argv[]) | |||||||||||||||||
| std::move(watchdog), | ||||||||||||||||||
| config_result.value().takeWatchdog()); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Declared here, outside the `if` block below, and deliberately not destroyed until | ||||||||||||||||||
| // after `deinitialize()` returns: `deinitialize()`'s own comment notes that a worker may | ||||||||||||||||||
| // still be (de)activating a node when it runs, and drains those in flight before | ||||||||||||||||||
| // resetting `graph_`. A drain that completes a transition calls | ||||||||||||||||||
| // Graph::finalizeTransitionSuccess(), which invokes the very callback `ControlProvider` | ||||||||||||||||||
| // registered via `registerActiveRunTargetCallback`. If `ControlProvider` were destroyed | ||||||||||||||||||
| // before that drain finishes -- as it would be if scoped only to the `if` block below -- | ||||||||||||||||||
| // that callback would fire through a dangling pointer. | ||||||||||||||||||
|
Comment on lines
+178
to
+185
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Too verbose, don't think we need to explain anything here, people should know about lifetimes
Suggested change
|
||||||||||||||||||
| std::unique_ptr<ControlProvider> control_provider; | ||||||||||||||||||
|
|
||||||||||||||||||
| if (process_group_manager->initialize()) | ||||||||||||||||||
| { | ||||||||||||||||||
| // Remains active in the background until the ControlProvider is destroyed. | ||||||||||||||||||
| const score::Result<ControlProvider*> control_provider_result = | ||||||||||||||||||
| score::Result<std::unique_ptr<ControlProvider>> control_provider_result = | ||||||||||||||||||
| ControlProvider::Create(process_group_manager.get()); | ||||||||||||||||||
|
|
||||||||||||||||||
| if (!control_provider_result.has_value()) | ||||||||||||||||||
|
|
@@ -187,9 +196,13 @@ int run(int argc, const char* argv[]) | |||||||||||||||||
| << control_provider_result.error().Message(); | ||||||||||||||||||
| exit_code = EXIT_FAILURE; | ||||||||||||||||||
| } | ||||||||||||||||||
| else if (runLCMDaemon(*process_group_manager)) | ||||||||||||||||||
| else | ||||||||||||||||||
| { | ||||||||||||||||||
| exit_code = EXIT_SUCCESS; | ||||||||||||||||||
| control_provider = std::move(control_provider_result).value(); | ||||||||||||||||||
| if (runLCMDaemon(*process_group_manager)) | ||||||||||||||||||
| { | ||||||||||||||||||
| exit_code = EXIT_SUCCESS; | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -198,6 +211,9 @@ int run(int argc, const char* argv[]) | |||||||||||||||||
| process_group_manager->deinitialize(); | ||||||||||||||||||
| process_group_manager.reset(); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Safe to let `control_provider` go out of scope now: `deinitialize()` above has | ||||||||||||||||||
| // already joined every worker and reset `graph_`, so no further callback can arrive. | ||||||||||||||||||
|
Comment on lines
+215
to
+216
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Too verbose don't need it
Suggested change
|
||||||||||||||||||
| } | ||||||||||||||||||
| catch (...) | ||||||||||||||||||
| { | ||||||||||||||||||
|
|
||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a setup to add a test type property for test linkage.
e.g.
lifecycle/score/launch_manager/src/exec_error_domain_UT.cpp
Lines 50 to 54 in bb79e12