Battery module crashes waybar with std::terminate when a HID++ device reconnects mid-poll (race in refreshBatteries())
bugcustomclockbatteryinhibitorbluetooth
# Battery module crashes waybar with `std::terminate` when a HID++ device reconnects mid-poll (race in `refreshBatteries()`)
## Summary
`waybar::modules::Battery::refreshBatteries()` performs a non-atomic walk of `/sys/class/power_supply/` (`fs::directory_iterator` followed by `fs::exists` / `std::ifstream` on children). When a Logitech HID++ device (e.g. MX Master 4) reconnects over Bluetooth, the kernel destroys the old `hidpp_battery_N` entry and creates a new one. If this happens between the directory listing and the per-entry stat/open, `fs::filesystem_error` is caught and rethrown as `std::runtime_error` (battery.cpp:154–155), but the rethrown exception escapes the worker thread (`thread_timer_` / `thread_battery_update_` in `Battery::Battery`), triggering `std::terminate` and SIGABRT. Waybar dies completely.
I have hit this on four separate occasions on a stable workstation; every core dump has an identical signature in the worker thread. Unfortunately I haven't been able to reliably recreate this on demand.
## Environment
- waybar 0.15.0-2 (Arch package, no debug symbols)
- Linux 7.0.3-arch1-2
- Compositor: Hyprland (Wayland)
- Affected hardware: Logitech MX Master 4 over Bluetooth (causes `hidpp_battery_N` add/remove on reconnect). Any device using the `logitech-hidpp-device` driver should reproduce.
## Crash signature
All four crashes show the same throw site (`waybar+0x3296e`) on a non-main thread, propagating through the C++ runtime to `std::terminate`:
```
Stack trace of crashing thread:
#3 libstdc++.so.6 + 0x9ac50 # __cxxabiv1::__terminate
#4 libstdc++.so.6 + 0xb673a # std::terminate (no handler)
#5 libstdc++.so.6 + 0x9a5e9 # std::terminate()
#6 libstdc++.so.6 + 0xb69f6 # __cxa_throw
#7 /usr/bin/waybar + 0x3296e # throw inside Battery::refreshBatteries
#8 /usr/bin/waybar + 0xd48e2 # SleeperThread worker invoke
#9 /usr/bin/waybar + 0x923d6 # std::thread shim
#10 libstdc++.so.6 + 0xeb919 # std::thread entry
```
Main thread is parked in `g_application_run`, so the crash is purely in the battery worker.
## Root cause
`src/modules/battery.cpp` (0.15.0):
```cpp
void waybar::modules::Battery::refreshBatteries() {
std::lock_guard<std::mutex> guard(battery_list_mutex_);
...
try {
for (auto& node : fs::directory_iterator(data_dir_)) { // [1]
if (!fs::is_directory(node)) continue; // [2]
...
if (... && fs::exists(node.path() / "uevent") && ...) { // [3]
...
std::ifstream(node.path() / "type") >> type; // [4]
...
auto wd = inotify_add_watch(battery_watch_fd_, event_path.c_str(), IN_ACCESS); // [5]
...
}
}
} catch (fs::filesystem_error& e) {
throw std::runtime_error(e.what()); // [6]
}
}
```
There is a TOCTOU window between [1] and any of [2]–[5]. When `hidpp_battery_N` is removed by the kernel mid-iteration:
- `fs::is_directory(node)` and `fs::exists(... / "uevent")` may throw `fs::filesystem_error` (ENOENT).
- `std::ifstream(... / "type")` doesn't throw on its own, but `inotify_add_watch` on a vanished path will fail and the explicit `throw std::runtime_error(...)` at battery.cpp:142 fires.
The `catch` at [6] rethrows as `std::runtime_error`, but the calling worker lambdas (`thread_timer_` and `thread_battery_update_`, set up in the `Battery` constructor) wrap no try/catch:
```cpp
thread_timer_ = [this] {
refreshBatteries(); // unguarded
dp.emit();
thread_timer_.sleep_for(interval_);
};
thread_battery_update_ = [this] {
...
refreshBatteries(); // unguarded
dp.emit();
};
```
Result: a single missed sysfs entry kills the whole bar.
## Why HID++ devices trigger this so reliably
`logitech-hidpp-device` registers a `power_supply` for each connection. Every Bluetooth disconnect/reconnect tears down the old `hidpp_battery_N` and creates a fresh one with the next index. The MX Master 4 reconnects opportunistically (USB-C plug event, idle wake, and so on), and on this machine I observe `hidpp_battery_0` → `hidpp_battery_1` → … grow over an uptime; one reconnect cycle is enough to crash waybar if it happens to align with the battery worker's tick.
I confirmed the kernel-side race independently with a small shell loop that diffed the listing of `/sys/class/power_supply/` against per-entry `cat uevent` during 15 forced reconnect cycles — 5 entries vanished mid-read in that test (a kernel-level race exists by construction; waybar just doesn't tolerate it).
## Steps to reproduce
1. Pair a Logitech MX Master 4 (or any HID++ Bluetooth device) so it shows up as `/sys/class/power_supply/hidpp_battery_N`.
2. Run waybar with the battery module enabled (a typical config; minimal example below).
3. Force the device to reconnect repeatedly:
```sh
MAC=<your device MAC>
for i in $(seq 1 30); do
bluetoothctl disconnect "$MAC" >/dev/null
sleep 1
bluetoothctl connect "$MAC" >/dev/null
sleep 1
done
```
4. Within a few cycles, waybar dies; `coredumpctl info $(pgrep -f waybar)` shows the trace above.
Minimal config:
```jsonc
{
"modules-right": ["battery"],
"battery": { "interval": 1 }
}
```
A short interval (`interval: 1`) makes the race much more likely to fire; the default interval still hits it eventually under normal use (in my case, ~once per several hours of uptime with a single MX Master 4).
## Workaround for users hitting this
Run waybar under a systemd user unit with `Restart=on-failure` so it respawns on each crash. Doesn't fix the underlying race, but the bar comes back automatically.
## AI Note
I used Claude to analyze the C++ code and write this PR, I am not familiar with C++, but of course did my best to fact check and verify its content to not be another AI slop issue. Please let me know if I missed anything or if you'd like me to run any tests on my system.
Thanks!
关闭于 2026-07-04 4 条评论