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):
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:
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
- Pair a Logitech MX Master 4 (or any HID++ Bluetooth device) so it shows up as
/sys/class/power_supply/hidpp_battery_N.
- Run waybar with the battery module enabled (a typical config; minimal example below).
- Force the device to reconnect repeatedly:
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
- Within a few cycles, waybar dies;
coredumpctl info $(pgrep -f waybar) shows the trace above.
Minimal config:
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!
Battery module crashes waybar with
std::terminatewhen a HID++ device reconnects mid-poll (race inrefreshBatteries())Summary
waybar::modules::Battery::refreshBatteries()performs a non-atomic walk of/sys/class/power_supply/(fs::directory_iteratorfollowed byfs::exists/std::ifstreamon children). When a Logitech HID++ device (e.g. MX Master 4) reconnects over Bluetooth, the kernel destroys the oldhidpp_battery_Nentry and creates a new one. If this happens between the directory listing and the per-entry stat/open,fs::filesystem_erroris caught and rethrown asstd::runtime_error(battery.cpp:154–155), but the rethrown exception escapes the worker thread (thread_timer_/thread_battery_update_inBattery::Battery), triggeringstd::terminateand 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
hidpp_battery_Nadd/remove on reconnect). Any device using thelogitech-hidpp-devicedriver should reproduce.Crash signature
All four crashes show the same throw site (
waybar+0x3296e) on a non-main thread, propagating through the C++ runtime tostd::terminate: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):There is a TOCTOU window between [1] and any of [2]–[5]. When
hidpp_battery_Nis removed by the kernel mid-iteration:fs::is_directory(node)andfs::exists(... / "uevent")may throwfs::filesystem_error(ENOENT).std::ifstream(... / "type")doesn't throw on its own, butinotify_add_watchon a vanished path will fail and the explicitthrow std::runtime_error(...)at battery.cpp:142 fires.The
catchat [6] rethrows asstd::runtime_error, but the calling worker lambdas (thread_timer_andthread_battery_update_, set up in theBatteryconstructor) wrap no try/catch:Result: a single missed sysfs entry kills the whole bar.
Why HID++ devices trigger this so reliably
logitech-hidpp-deviceregisters apower_supplyfor each connection. Every Bluetooth disconnect/reconnect tears down the oldhidpp_battery_Nand 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 observehidpp_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-entrycat ueventduring 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
/sys/class/power_supply/hidpp_battery_N.coredumpctl info $(pgrep -f waybar)shows the trace above.Minimal config:
{ "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-failureso 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!