Repository navigation
Conversation
Guard raw allocations while constructing copies for owning intra-process buffers. After successful construction, transfer ownership to the existing message deleter. Cover custom and ROS owning buffers with throwing-copy regression cases. Assisted-by: Codex:gpt-6-sol Signed-off-by: TinyGeek <geek950519@gmail.com>
ahcorde
left a comment
There was a problem hiding this comment.
Thanks for tackling #3320! The guard fixes the two copy sites in add_owned_msg_to_buffers, but the same allocate-then-construct leak exists in a few more places, so I don't think this fully closes the issue yet.
TypeAdapter branch is still unguarded (intra_process_manager.hpp ~L654-662, outside this diff): ROSMessageTypeAllocatorTraits::allocate + construct, and then convert_to_ros_message (user code), all run before the unique_ptr takes ownership. If the default constructor or the conversion throws, the storage leaks (and in the conversion case the constructed message is never destroyed either). Could this branch get the same treatment plus a test case?
Same pattern elsewhere:
rclcpp/include/rclcpp/experimental/buffers/intra_process_buffer.hpp(~L193add_shared_impl, ~L239consume_unique, ~L303)rclcpp/include/rclcpp/any_subscription_callback.hpp(~L499, ~L521)rclcpp/include/rclcpp/publisher.hpp(~L558, ~L567, ~L583)
With the CopyMessage type from this test, e.g. a shared publish into a UniquePtr buffer, or consume_unique on a shared buffer, still leaks one message per throw.
| // Copy the message since we have additional subscriptions to serve | ||
| Deleter deleter = message.get_deleter(); | ||
| auto ptr = MessageAllocTraits::allocate(allocator, 1); | ||
| auto deallocate = [&allocator](MessageT * allocation) { |
There was a problem hiding this comment.
Rather than inlining this guard (here and again at L679-685, where the two copies already differ slightly), what about adding one exception-safe helper next to AllocRebind in rclcpp/allocator/allocator_common.hpp, something like allocate_and_construct<Traits>(alloc, args...) that deallocates if construction throws? That would let all the other sites listed in the review summary be fixed the same way, without copying this 6-line block around.
| @@ -628,9 +628,15 @@ class IntraProcessManager | |||
| // Copy the message since we have additional subscriptions to serve | |||
| Deleter deleter = message.get_deleter(); | |||
There was a problem hiding this comment.
Question (pre-existing, but the guard makes it visible): here the copy is allocated with allocator, and the guard deallocates with allocator, but on the success path ownership goes to a deleter copied from message that is never re-pointed. The ROS branch below does call allocator::set_allocator_for_deleter(&deleter, &allocator). With a stateful allocator whose instance differs from the one in message's deleter, wouldn't the success path free through a different allocator than the one that allocated? Should this branch call set_allocator_for_deleter too?
| #include <stdexcept> | ||
| #include <type_traits> | ||
| #include <utility> | ||
| #include "rclcpp/rclcpp.hpp" |
There was a problem hiding this comment.
nit: could this include the granular headers it actually uses (rclcpp/node.hpp, rclcpp/experimental/intra_process_manager.hpp, the intra-process buffer headers, ...) instead of the umbrella rclcpp/rclcpp.hpp? We're moving rclcpp tests away from the umbrella header to cut compile time.
| }; | ||
| struct AllocationCounts | ||
| { | ||
| std::size_t allocated = 0; |
There was a problem hiding this comment.
nit: std::size_t is used here without #include <cstddef>; it only compiles via transitive includes.
| EXPECT_EQ(2u, counts->allocated); | ||
| EXPECT_EQ(counts->allocated, counts->deallocated); | ||
| } | ||
| INSTANTIATE_TEST_SUITE_P( |
There was a problem hiding this comment.
The cases cover only plain data buffers and ROS-message buffers. Could you add:
- a TypeAdapter publisher → ROS-message subscription case (
convert_to_ros_messagethrowing), which would catch the remaining leak in that branch, and - a case where
provide_intra_process_*throws after the guard has released ownership, so a future reordering ofrelease()vs. constructingcopied_messageis caught?
|
@ahcorde Thanks for the detailed review and the helper suggestion. I found the leak documented in #3320 while investigating #1860, which I set aside when the optimization grew too broad. After your review, I reproduced the remaining TypeAdapter failure and related exception-safety failures at 13 other allocation sites on unmodified Jazzy and Rolling. I haven't changed this PR yet. I understand Would you be comfortable keeping #3321 focused on I can share the reproducers and sanitizer logs, and adjust I've also noted the two include-related comments and both requested regression cases, including the throwing |
fujitatomoya
left a comment
There was a problem hiding this comment.
i think this fixes the part of the original problem.
saying that TypeAdapter publisher to ROS-message subscription still leaks on throw?
Yes. I replicated this with a real TypeAdapter publisher and a ROS-message intra-process subscription with a convert_to_ros_message() implementation that throws. ROS message allocation in IntraProcessManager::add_owned_msg_to_buffers() before calling the converter. Total subscriptions: 1 Intra-process subscriptions: 1 LeakSanitizer reports a 32-byte leak on both the unmodified Jazzy/Rolling and the current PR-patched code. The leak reproduces in 10/10 runs on each of the unmodified distributions and 9/10 runs on each of the patched distributions. The two non-leaking runs reported 2 total subscriptions / 1 intra-process subscripton, which made the publisher take a different path. This TypeAdapter branch is not covered by the current copy-constructor guards. I have the standalone reproducer, sanitizer logs and conditions under which it was run. |
Use an exception-safe allocator helper for owning message copies and take ownership of adapted ROS messages before invoking conversion code. Cover construction, conversion, and delivery exceptions in regression tests. Refs ros2#3320 Assisted-by: Codex:gpt-6-sol Signed-off-by: TinyGeek <geek950519@gmail.com>
|
@ahcorde @fujitatomoya
|
ABI Compliance Check❌ Verdict: incompatible
✅
|
Signed-off-by: TinyGeek <geek950519@gmail.com>
|
@ahcorde @fujitatomoya |
Description
When
IntraProcessManager::add_owned_msg_to_buffers()copies an owning message, a throwing constructor must release the newly allocated storage. The same manager also constructs a ROS message for TypeAdapter delivery before calling user conversion code; if construction or conversion throws, that storage remains allocated. On the unmodified Jazzy and Rolling sources, a throwing TypeAdapter conversion leaks one 32-byte ROS message in the tested intra-process path.This update adds
allocator::allocate_and_construct<Traits>()besideAllocRebindto deallocate storage when construction throws, and uses it at the manager's three allocation sites. In the TypeAdapter branch, the completed ROS message enters its existingunique_ptrbeforeconvert_to_ros_message()runs, so conversion exceptions destroy and deallocate it. The copy-delivery and tracepoint flow remains the same. Existing public method signatures and class layouts are unchanged; the helper is an additive template in an installed header.Refs #3320
Is this user-facing behavior change?
Yes, on the exception paths: the existing exception still propagates, while the unfinished allocation or completed adapted ROS message is cleaned up. Normal owning delivery and the last subscriber's original message are preserved.
Did you use Generative AI?
Yes. OpenAI Codex CLI (model
gpt-6-sol) assisted with the helper, manager changes, regression tests, local sanitizer runs, and this description.Assisted-by: Codex:gpt-6-sol
Additional Information
provide_intra_process_message()after ownership transfer.copyrightandcpplintpassed on both distributions.uncrustifyinitially requested one template-spacing change and passed after that change.0e5589a), a same-rootfs Debug (-g -Og,BUILD_TESTING=OFF)abidiffagainst that upstream revision exited 4: two added weakallocate_and_constructinstantiations, with no removed or changed functions or variables. This local comparison does not replace the upstream ABI workflow. An earlier supplemental Jazzy build'sabidiffexited 12 for one removed and one added weakParameterValue::getsymbol outside the changed code; Jazzy ABI was not rechecked after the latest changes, so Jazzy ABI compatibility is not claimed.