Operating System:
Ubuntu 22.04.5 (arm64) and macOS 26.5.2 (arm64)
ROS version or commit hash:
Humble from apt (launch 1.0.15, osrf_pycommon 2.1.6) and Jazzy from RoboStack (launch 3.4.11, osrf_pycommon 2.1.7). Rolling has the same code.
RMW implementation (if applicable):
rmw_fastrtps_cpp
RMW Configuration (if applicable):
Defaults on Ubuntu, FASTDDS_BUILTIN_TRANSPORTS=UDPv4 on macOS
Client library (if applicable):
rclcpp, rclpy
'ros2 doctor --report' output
n/a
Steps to reproduce issue
Loading a component makes launch create its own node, even before the container process starts. Save this as leak.launch.py:
from launch import LaunchDescription
from launch.actions import TimerAction
from launch_ros.actions import ComposableNodeContainer, Node
from launch_ros.descriptions import ComposableNode
def generate_launch_description():
container = ComposableNodeContainer(
name='container', namespace='', package='rclcpp_components',
executable='component_container',
composable_node_descriptions=[ComposableNode(
package='tf2_ros', plugin='tf2_ros::StaticTransformBroadcasterNode',
name='stf_component',
parameters=[{'frame_id': 'a', 'child_frame_id': 'b'}])])
late = TimerAction(period=4.0, actions=[Node(
package='tf2_ros', executable='static_transform_publisher',
arguments=['--frame-id', 'a', '--child-frame-id', 'c'])])
return LaunchDescription([container, late])
-
ros2 launch leak.launch.py
-
After 5 s, in a second terminal:
L=$(pgrep -f 'ros2 launch leak.launch.py'); s() { ls -l /proc/$1/fd | grep -o 'socket:\[[0-9]*\]' | sort; }; for c in $(pgrep -P $L); do echo "$(cat /proc/$c/comm) holds $(comm -12 <(s $L) <(s $c) | wc -l) of launch's $(s $L | wc -l) sockets"; done
On macOS, compare the DEVICE column of lsof -a -p <pid> -i UDP instead.
Expected behavior
Each child holds only its own sockets, so step 2 prints holds 0 for both.
Actual behavior
On Ubuntu, step 2 prints:
component_conta holds 8 of launch's 12 sockets
static_transfor holds 8 of launch's 12 sockets
All 5 of launch's UDP sockets are among them. On macOS, both children hold all 8 of launch's UDP sockets.
Additional information
ExecuteLocal starts processes with osrf_pycommon's async_execute_process, which passes close_fds=False (impl.py#L40-L47). Fast DDS opens its sockets without close-on-exec, so once launch creates its own node, every process it starts inherits that node's sockets. On macOS, when the kernel shuts down a child's sockets after a network-buffer shortage, launch's DDS goes down too, because the sockets are shared.
I'll open an osrf_pycommon PR that adds a close_fds option, so ExecuteLocal could pass close_fds=True.
Assisted-by: Claude Opus 5.5
Operating System:
Ubuntu 22.04.5 (arm64) and macOS 26.5.2 (arm64)
ROS version or commit hash:
Humble from apt (launch 1.0.15, osrf_pycommon 2.1.6) and Jazzy from RoboStack (launch 3.4.11, osrf_pycommon 2.1.7). Rolling has the same code.
RMW implementation (if applicable):
rmw_fastrtps_cpp
RMW Configuration (if applicable):
Defaults on Ubuntu,
FASTDDS_BUILTIN_TRANSPORTS=UDPv4on macOSClient library (if applicable):
rclcpp, rclpy
'ros2 doctor --report' output
n/a
Steps to reproduce issue
Loading a component makes launch create its own node, even before the container process starts. Save this as
leak.launch.py:ros2 launch leak.launch.pyAfter 5 s, in a second terminal:
On macOS, compare the DEVICE column of
lsof -a -p <pid> -i UDPinstead.Expected behavior
Each child holds only its own sockets, so step 2 prints
holds 0for both.Actual behavior
On Ubuntu, step 2 prints:
All 5 of launch's UDP sockets are among them. On macOS, both children hold all 8 of launch's UDP sockets.
Additional information
ExecuteLocalstarts processes with osrf_pycommon'sasync_execute_process, which passesclose_fds=False(impl.py#L40-L47). Fast DDS opens its sockets without close-on-exec, so once launch creates its own node, every process it starts inherits that node's sockets. On macOS, when the kernel shuts down a child's sockets after a network-buffer shortage, launch's DDS goes down too, because the sockets are shared.I'll open an osrf_pycommon PR that adds a
close_fdsoption, soExecuteLocalcould passclose_fds=True.Assisted-by: Claude Opus 5.5