Skip to content

Processes started by ros2 launch inherit its DDS sockets #1031

Description

@erfanMhi

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])
  1. ros2 launch leak.launch.py

  2. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions