Environment
- Robot: Unitree G1 (dual Revo2 Touch, capacitive, connected via dual RS-485 / dual USB-serial)
- SDK:
bc_stark_sdk / libbc_stark_sdk.so v2.0.2 (linux ARM64)
- ROS2: Foxy,
ros2_stark_controller package (based on this repo's ros2_stark_ws)
- Left hand: SN
BCXTL2361J250002F, FW 1.0.16.U
- Right hand: SN
BCXTR2361J250001A, FW 1.0.16.U
- Protocol: Modbus RTU, baud 460800
Symptom
stark_node process RSS grows to ~2GB+ within roughly 10-30 seconds of starting, and is killed (either by the kernel OOM killer or a cgroup memory cap we added as a mitigation). Reproduced on 7 consecutive runs under systemd with Restart=always, every single one died the same way.
Kernel OOM killer log from one occurrence (no memory cap in place at the time):
dockerd invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=-500
oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/system.slice/stark_node.service,task=stark_node,pid=1854,uid=1000
Out of memory: Killed process 1854 (stark_node) total-vm:31320256kB, anon-rss:13234216kB, file-rss:0kB, shmem-rss:16kB, UID:1000 pgtables:41340kB oom_score_adj:0
oom_reaper: reaped process 1854 (stark_node), now anon-rss:0kB, file-rss:0kB, shmem-rss:24kB
~13.2GB resident in well under a minute of runtime. This is severe enough that it starved unrelated processes on the machine (note dockerd is what invoked the OOM killer here, not our process).
Key evidence this is in the SDK, not our integration code
In one run, bad_alloc was thrown three times before our first log line printed before modbus_open() was ever called, before our ROS2 poll timer (50ms) was even constructed:
[INFO] [stark_node-1]: process started with pid [3158]
[stark_node-1] bad_alloc caught: std::bad_alloc
[stark_node-1] bad_alloc caught: std::bad_alloc
[stark_node-1] bad_alloc caught: std::bad_alloc
... (dies ~12s later, exit code -9)
Since nothing in our code has executed yet at that point, this rules out a leak in the ROS2 node integration and points at SDK-internal state (possibly a background thread spawned on library load or modbus_open).
We also see it happen after a fully clean initialization (no read timeouts, both hands report device info successfully, touch sensors enabled on both):
[LEFT] SN=BCXTL2361J250002F FW=1.0.16.U HW=11
[LEFT] Touch sensors enabled — waiting 1 s…
touch_sensor_setup: slave_id=126, bits=0b11111
touch_sensor_enable: slave_id=126, address=0x0FA0, bits=0b11111, values=[1, 1, 1, 1, 1]
[RIGHT] SN=BCXTR2361J250001A FW=1.0.16.U HW=11
[RIGHT] Touch sensors enabled — waiting 1 s…
touch_sensor_setup: slave_id=127, bits=0b11111
touch_sensor_enable: slave_id=127, address=0x0FA0, bits=0b11111, values=[1, 1, 1, 1, 1]
... (dies ~11s later, exit code -9)
So the leak isn't tied to the read-timeout/retry path either, it also happens in steady-state polling after a clean startup.
Our integration code (for reference, every returned pointer is freed)
void publish_motor(DeviceHandler* handle, uint8_t slave_id,
rclcpp::Publisher<ros2_stark_interfaces::msg::MotorStatus>::SharedPtr& pub,
const char* label) {
auto* data = stark_get_motor_status(handle, slave_id);
if (!data) { /* ... */ return; }
auto msg = ros2_stark_interfaces::msg::MotorStatus();
std::copy(data->positions, data->positions + 6, msg.positions.begin());
std::copy(data->speeds, data->speeds + 6, msg.speeds.begin());
std::copy(data->currents, data->currents + 6, msg.currents.begin());
std::copy(data->states, data->states + 6, msg.states.begin());
pub->publish(msg);
free_motor_status_data(data);
}
void publish_touch(DeviceHandler* handle, uint8_t slave_id,
rclcpp::Publisher<ros2_stark_interfaces::msg::TouchStatus>::SharedPtr& pub,
const char* label) {
auto* data = stark_get_touch_status(handle, slave_id);
if (!data) { /* ... */ return; }
auto msg = ros2_stark_interfaces::msg::TouchStatus();
for (int i = 0; i < 5; ++i) { /* copy fields */ }
pub->publish(msg);
free_touch_finger_data(data);
}
Both are called from a 50ms wall_timer, alongside stark_get_voltage on a 30s timer (returns a scalar, nothing to free).
Repro steps
- Dual-hand Modbus RTU setup, baud 460800, one hand per USB-serial adapter.
- Call
stark_set_hardware_type, stark_get_device_info, stark_enable_touch_sensor(handle, slave_id, 0x1F) on each hand at startup.
- Poll
stark_get_motor_status + stark_get_touch_status every 50ms per hand, freeing each returned pointer after use.
- Observe process RSS via
systemctl status <service> or ps grows unbounded within seconds, process is killed within 10-30s.
Questions
- Is there a newer SDK build past v2.0.2 (linux ARM64) that addresses this?
- Does
bc_stark_sdk spawn any internal background threads on modbus_open() or library load that could account for allocation happening before the caller's own API calls return?
- Is there a known workaround (e.g. a required cleanup/flush call we're missing, or a known-bad code path when both hands share one process)?
Happy to provide valgrind/heaptrack output against stark_node if that would help narrow down the allocation site.
Environment
bc_stark_sdk/libbc_stark_sdk.sov2.0.2 (linux ARM64)ros2_stark_controllerpackage (based on this repo'sros2_stark_ws)BCXTL2361J250002F, FW1.0.16.UBCXTR2361J250001A, FW1.0.16.USymptom
stark_nodeprocess RSS grows to ~2GB+ within roughly 10-30 seconds of starting, and is killed (either by the kernel OOM killer or a cgroup memory cap we added as a mitigation). Reproduced on 7 consecutive runs under systemd withRestart=always, every single one died the same way.Kernel OOM killer log from one occurrence (no memory cap in place at the time):
~13.2GB resident in well under a minute of runtime. This is severe enough that it starved unrelated processes on the machine (note
dockerdis what invoked the OOM killer here, not our process).Key evidence this is in the SDK, not our integration code
In one run,
bad_allocwas thrown three times before our first log line printed beforemodbus_open()was ever called, before our ROS2 poll timer (50ms) was even constructed:Since nothing in our code has executed yet at that point, this rules out a leak in the ROS2 node integration and points at SDK-internal state (possibly a background thread spawned on library load or
modbus_open).We also see it happen after a fully clean initialization (no read timeouts, both hands report device info successfully, touch sensors enabled on both):
So the leak isn't tied to the read-timeout/retry path either, it also happens in steady-state polling after a clean startup.
Our integration code (for reference, every returned pointer is freed)
Both are called from a 50ms
wall_timer, alongsidestark_get_voltageon a 30s timer (returns a scalar, nothing to free).Repro steps
stark_set_hardware_type,stark_get_device_info,stark_enable_touch_sensor(handle, slave_id, 0x1F)on each hand at startup.stark_get_motor_status+stark_get_touch_statusevery 50ms per hand, freeing each returned pointer after use.systemctl status <service>orpsgrows unbounded within seconds, process is killed within 10-30s.Questions
bc_stark_sdkspawn any internal background threads onmodbus_open()or library load that could account for allocation happening before the caller's own API calls return?Happy to provide
valgrind/heaptrackoutput againststark_nodeif that would help narrow down the allocation site.