ITADN

Battery module crashes waybar with std::terminate when a HID++ device reconnects mid-poll (race in refreshBatteries())

#5019ClosedAniAggarwal 创建于 2026-05-04
bugcustomclockbatteryinhibitorbluetooth
A
AniAggarwalcommented
# 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 条评论