Skip to content

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

@AniAggarwal

Description

@AniAggarwal

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_0hidpp_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:
    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:

{
  "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!

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions