Skip to content

Crash on config reload during output layout reconfiguration #2951

@killown

Description

@killown

Wayfire version

0.11.0-44d058af (Jan 5 2026) branch master wlroots-0.19.2

GPU / Driver

deviceName         = AMD Radeon RX 9060 XT (RADV GFX1200)
driverID           = DRIVER_ID_MESA_RADV
driverName         = radv
driverInfo         = Mesa 26.0.0-devel (git-6d07a56c6a)

Describe the bug

Wayfire crash during config reload while reconfiguring outputs, triggered via IPC set_config_options.

To Reproduce

nvim wf-output-ctl-py

save the following:

#!/usr/bin/env python3

"""
Wayfire output toggle utility.

Supports toggling individual outputs, interactive selection,
disabling all outputs except the currently focused one,
and reloading outputs.

Output modes are detected via wlr-randr and persisted in
~/.config/wayfire_output_config.json for restoration.
"""

from wayfire import WayfireSocket
from subprocess import Popen, check_output
import json
import os
import sys
import re
import time

CONFIG_PATH = os.path.expanduser("~/.config/wayfire_output_config.json")


def load_config():
    """
    Load stored output modes from disk.
    """
    if not os.path.exists(CONFIG_PATH):
        return {}
    with open(CONFIG_PATH, "r") as f:
        return json.load(f)


def save_config(data):
    """
    Persist output mode configuration to disk.
    """
    os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
    with open(CONFIG_PATH, "w") as f:
        json.dump(data, f, indent=2)


def list_known_outputs(sock):
    """
    Return all known outputs, including disabled ones.
    """
    active = {o["name"] for o in sock.list_outputs()}
    stored = set(load_config().keys())
    return sorted(active | stored)


def ask_output(sock):
    """
    Ask the user to select an output interactively.
    """
    outputs = list_known_outputs(sock)

    for i, name in enumerate(outputs):
        print(f"{i}: {name}")

    try:
        idx = int(input("Select output: "))
    except KeyboardInterrupt:
        print("\nCancelled.")
        sys.exit(0)

    return outputs[idx]


def get_current_mode_from_wlr_randr(output_name):
    """
    Detect the current mode of an output using wlr-randr.

    Returns WIDTHxHEIGHT@HZ or None.
    """
    lines = check_output(["wlr-randr"], text=True).splitlines()
    inside = False

    for line in lines:
        if line.startswith(output_name + " "):
            inside = True
            continue

        if inside and re.match(r"^[A-Z].*-\d+", line):
            break

        if inside and "(current)" in line:
            m = re.search(r"(\d+x\d+)\s+px,\s+([\d.]+)\s+Hz", line)
            if m:
                res, hz = m.groups()
                return f"{res}@{hz.replace('.', '')}"

    return None


def turn_off_output(sock, output_name):
    """
    Disable an output and store its current mode.
    """
    config = load_config()
    mode = get_current_mode_from_wlr_randr(output_name)

    if mode:
        config[output_name] = mode
        save_config(config)

    Popen(["wlopm", "--off", output_name])
    sock.set_option_values({f"output:{output_name}": {"mode": "off"}})
    print(f"Turned off {output_name}")


def turn_on_output(sock, output_name):
    """
    Enable an output using the stored or detected mode.
    """
    config = load_config()
    mode = config.get(output_name)

    if not mode:
        mode = get_current_mode_from_wlr_randr(output_name)

    if not mode:
        print(f"No mode available for {output_name}, aborting.")
        return

    sock.set_option_values({f"output:{output_name}": {"mode": mode}})
    print(f"Turned on {output_name} with mode {mode}")


def get_output_state(sock, output_name):
    """
    Return whether an output is currently on or off.
    """
    try:
        mode = sock.get_option_value(f"output:{output_name}/mode")["value"]
    except Exception:
        return "off"

    return "off" if mode == "off" or not mode else "on"


def toggle_output(sock, output_name):
    """
    Toggle the state of an output.
    """
    state = get_output_state(sock, output_name)

    if state == "off":
        turn_on_output(sock, output_name)
    else:
        turn_off_output(sock, output_name)


def disable_except_focused(sock):
    """
    Disable all outputs except the currently focused one.
    """
    focused = sock.get_focused_output()
    focused_name = focused["name"]

    outputs = list_known_outputs(sock)

    for name in outputs:
        if name != focused_name and get_output_state(sock, name) == "on":
            turn_off_output(sock, name)

    print(f"Focused output preserved: {focused_name}")


def reload_output(sock, output_name):
    """
    Reload a single output by disabling and re-enabling it.
    """
    was_on = get_output_state(sock, output_name) == "on"

    if was_on:
        turn_off_output(sock, output_name)
        time.sleep(0.3)

    turn_on_output(sock, output_name)


def reload_all_outputs(sock):
    """
    Reload all known outputs.
    """
    outputs = list_known_outputs(sock)

    for name in outputs:
        reload_output(sock, name)


if __name__ == "__main__":
    sock = WayfireSocket()

    if "--disable-except-focused" in sys.argv:
        disable_except_focused(sock)
        sys.exit(0)

    if "--reload-all" in sys.argv:
        reload_all_outputs(sock)
        sys.exit(0)

    if "--reload" in sys.argv:
        idx = sys.argv.index("--reload")
        try:
            output = sys.argv[idx + 1]
        except IndexError:
            print("Missing output name for --reload")
            sys.exit(1)
        reload_output(sock, output)
        sys.exit(0)

    if len(sys.argv) > 1:
        output = sys.argv[1]
    else:
        output = ask_output(sock)

    toggle_output(sock, output)

nvim binding-test.py

save the following:


from wayfire import WayfireSocket

sock = WayfireSocket()

sock.register_binding(
    "<super> KEY_U",
    command="python wf-output-ctl.py --reload-all",
    exec_always=True,
    mode="normal",
)

Press the new keybind registered fast

Expected behavior

No crashes

Screenshots / Videos / Stacktrace

AddressSanitizer:DEADLYSIGNAL
=================================================================
==98458==ERROR: AddressSanitizer: SEGV on unknown address 0x000000002c52 (pc 0x7f22ccb784a6 bp 0x7ffce093a950 sp 0x7ffce093a0b8 T0)
==98458==The signal is caused by a READ memory access.
    #0 0x7f22ccb784a6 in __sanitizer::internal_strlen(char const*) /usr/src/debug/gcc/gcc/libsanitizer/sanitizer_common/sanitizer_libc.cpp:176
    #1 0x7f22ccb4ac1d in strdup /usr/src/debug/gcc/gcc/libsanitizer/asan/asan_interceptors.cpp:589
    #2 0x7f22cc371e7b in wlr_xcursor_theme_load (/usr/lib/libwlroots-0.19.so+0xc9e7b) (BuildId: f756bc23f5d7e65dcba214a62afaab2914429b6a)
    #3 0x7f22cc36956d in wlr_xcursor_manager_load (/usr/lib/libwlroots-0.19.so+0xc156d) (BuildId: f756bc23f5d7e65dcba214a62afaab2914429b6a)
    #4 0x7f22cc3492db  (/usr/lib/libwlroots-0.19.so+0xa12db) (BuildId: f756bc23f5d7e65dcba214a62afaab2914429b6a)
    #5 0x7f22cd22e48d in wl_signal_emit_mutable (/usr/lib/libwayland-server.so.0+0x848d) (BuildId: eaeed0645478c33a1a975d52ebd375751b26c19e)
    #6 0x7f22cc35c238 in wlr_output_layout_add_auto (/usr/lib/libwlroots-0.19.so+0xb4238) (BuildId: f756bc23f5d7e65dcba214a62afaab2914429b6a)
    #7 0x55fb6a4c827c in wf::output_layout_t::impl::ensure_noop_output() ../src/core/output-layout.cpp:1260
    #8 0x55fb6a4d476c in wf::output_layout_t::impl::apply_configuration(std::map<wlr_output*, wf::output_state_t, std::less<wlr_output*>, std::allocator<std::pair<wlr_output* const, wf::output_state_t> > > const&) ../src/core/output-layout.cpp:1631
    #9 0x55fb6a4cd432 in wf::output_layout_t::impl::reconfigure_from_config() ../src/core/output-layout.cpp:1386
    #10 0x55fb6a4c13a7 in wf::output_layout_t::impl::on_config_reload::{lambda(wf::reload_config_signal*)#1}::operator()(wf::reload_config_signal*) const ../src/core/output-layout.cpp:1055
    #11 0x55fb6a54e7c4 in void std::__invoke_impl<void, wf::output_layout_t::impl::on_config_reload::{lambda(wf::reload_config_signal*)#1}&, wf::reload_config_signal>(std::__invoke_other, wf::output_layout_t::impl::on_config_reload::{lambda(wf::reload_config_signal*)#1}&, wf::reload_config_signal&&) (/usr/bin/wayfire+0x20577c4) (BuildId: 4b216497133f4761bed65c14272bc3b4c30930f5)
    #12 0x55fb6a53d162 in std::enable_if<is_invocable_r_v<void, wf::output_layout_t::impl::on_config_reload::{lambda(wf::reload_config_signal*)#1}&, wf::reload_config_signal*>, void>::type std::__invoke_r<void, wf::output_layout_t::impl::on_config_reload::{lambda(wf::reload_config_signal*)#1}&, wf::reload_config_signal*>(wf::output_layout_t::impl::on_config_reload::{lambda(wf::reload_config_signal*)#1}&, wf::reload_config_signal*&&) /usr/include/c++/15.2.1/bits/invoke.h:113
    #13 0x55fb6a520fb2 in std::_Function_handler<void (wf::reload_config_signal*), wf::output_layout_t::impl::on_config_reload::{lambda(wf::reload_config_signal*)#1}>::_M_invoke(std::_Any_data const&, wf::reload_config_signal*&&) (/usr/bin/wayfire+0x2029fb2) (BuildId: 4b216497133f4761bed65c14272bc3b4c30930f5)
    #14 0x7b22c62a1ecd in std::function<void (wf::reload_config_signal*)>::operator()(wf::reload_config_signal*) const /usr/include/c++/15.2.1/bits/std_function.h:593
    #15 0x7b22c629df37 in wf::signal::connection_t<wf::reload_config_signal>::emit(wf::reload_config_signal*) ../src/api/wayfire/signal-provider.hpp:104
    #16 0x7b22c62981a5 in wf::signal::provider_t::emit<wf::reload_config_signal>(wf::reload_config_signal*)::{lambda(wf::signal::connection_base_t*)#1}::operator()(wf::signal::connection_base_t*) const ../src/api/wayfire/signal-provider.hpp:151
    #17 0x7b22c62a73ba in void std::__invoke_impl<void, wf::signal::provider_t::emit<wf::reload_config_signal>(wf::reload_config_signal*)::{lambda(wf::signal::connection_base_t*)#1}&, wf::signal::connection_base_t*>(std::__invoke_other, wf::signal::provider_t::emit<wf::reload_config_signal>(wf::reload_config_signal*)::{lambda(wf::signal::connection_base_t*)#1}&, wf::signal::connection_base_t*&&) /usr/include/c++/15.2.1/bits/invoke.h:63
    #18 0x7b22c62a4e8a in std::enable_if<is_invocable_r_v<void, wf::signal::provider_t::emit<wf::reload_config_signal>(wf::reload_config_signal*)::{lambda(wf::signal::connection_base_t*)#1}&, wf::signal::connection_base_t*>, void>::type std::__invoke_r<void, wf::signal::provider_t::emit<wf::reload_config_signal>(wf::reload_config_signal*)::{lambda(wf::signal::connection_base_t*)#1}&, wf::signal::connection_base_t*>(wf::signal::provider_t::emit<wf::reload_config_signal>(wf::reload_config_signal*)::{lambda(wf::signal::connection_base_t*)#1}&, wf::signal::connection_base_t*&&) /usr/include/c++/15.2.1/bits/invoke.h:113
    #19 0x7b22c62a205f in std::_Function_handler<void (wf::signal::connection_base_t*), wf::signal::provider_t::emit<wf::reload_config_signal>(wf::reload_config_signal*)::{lambda(wf::signal::connection_base_t*)#1}>::_M_invoke(std::_Any_data const&, wf::signal::connection_base_t*&&) /usr/include/c++/15.2.1/bits/std_function.h:292
    #20 0x55fb6a5fed37 in std::function<void (wf::signal::connection_base_t*)>::operator()(wf::signal::connection_base_t*) const /usr/include/c++/15.2.1/bits/std_function.h:593
    #21 0x55fb6a5f8498 in void std::__invoke_impl<void, std::function<void (wf::signal::connection_base_t*)>&, wf::signal::connection_base_t*&>(std::__invoke_other, std::function<void (wf::signal::connection_base_t*)>&, wf::signal::connection_base_t*&) /usr/include/c++/15.2.1/bits/invoke.h:63
    #22 0x55fb6a5ef61d in std::enable_if<is_invocable_r_v<void, std::function<void (wf::signal::connection_base_t*)>&, wf::signal::connection_base_t*&>, void>::type std::__invoke_r<void, std::function<void (wf::signal::connection_base_t*)>&, wf::signal::connection_base_t*&>(std::function<void (wf::signal::connection_base_t*)>&, wf::signal::connection_base_t*&) /usr/include/c++/15.2.1/bits/invoke.h:113
    #23 0x55fb6a5e7f14 in std::_Function_handler<void (wf::signal::connection_base_t*&), std::function<void (wf::signal::connection_base_t*)> >::_M_invoke(std::_Any_data const&, wf::signal::connection_base_t*&) /usr/include/c++/15.2.1/bits/std_function.h:292
    #24 0x55fb6a5e589c in std::function<void (wf::signal::connection_base_t*&)>::operator()(wf::signal::connection_base_t*&) const /usr/include/c++/15.2.1/bits/std_function.h:593
    #25 0x55fb6a5e0052 in wf::safe_list_t<wf::signal::connection_base_t*>::for_each(std::function<void (wf::signal::connection_base_t*&)>) /usr/include/wayfire/nonstd/safe-list.hpp:73
    #26 0x55fb6a5d8ca1 in wf::signal::provider_t::for_each_connection(std::type_index, std::function<void (wf::signal::connection_base_t*)>) ../src/core/object.cpp:40
    #27 0x7b22b97e0eec in wf::ipc_rules_utility_methods_t::set_config_options::{lambda(wf::json_t const&)#1}::operator()(wf::json_t const&) const (/home/neo/.local/lib/wayfire/libipc-rules.so+0x50eec) (BuildId: 4fcb6d39e567b0b69b6baeebe70c8b8e94e6e2fe)
    #28 0x7b22b97e25fa in std::_Function_handler<wf::json_t (wf::json_t), wf::ipc_rules_utility_methods_t::set_config_options::{lambda(wf::json_t const&)#1}>::_M_invoke(std::_Any_data const&, wf::json_t&&) (/home/neo/.local/lib/wayfire/libipc-rules.so+0x525fa) (BuildId: 4fcb6d39e567b0b69b6baeebe70c8b8e94e6e2fe)
    #29 0x7b22baca1b29 in std::_Function_handler<wf::json_t (wf::json_t, wf::ipc::client_interface_t*), wf::ipc::method_repository_t::register_method(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::function<wf::json_t (wf::json_t)>)::{lambda(wf::json_t const&, wf::ipc::client_interface_t*)#1}>::_M_invoke(std::_Any_data const&, wf::json_t&&, wf::ipc::client_interface_t*&&) (/home/neo/.local/lib/wayfire/libstipc.so+0xeb29) (BuildId: c7b476bdfe1dddcad4d0f3232efa5f65038d92a2)
    #30 0x7b22bac7d737 in wf::ipc::method_repository_t::call_method(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, wf::json_t, wf::ipc::client_interface_t*) (/home/neo/.local/lib/wayfire/libcommand.so+0x1a737) (BuildId: 5fe0f7a698c53fd2d8efb04345f6f30bd29196c3)
    #31 0x7b22bac4c884 in wf::ipc::server_t::handle_incoming_message(wf::ipc::client_t*, wf::json_t) (/home/neo/.local/lib/wayfire/libipc.so+0xa884) (BuildId: d5566ec1cee538497b0223a125762f1c514aa4db)
    #32 0x7b22bac501a6 in wf::ipc::client_t::handle_fd_incoming(unsigned int) (/home/neo/.local/lib/wayfire/libipc.so+0xe1a6) (BuildId: d5566ec1cee538497b0223a125762f1c514aa4db)
    #33 0x7b22bac4b4ad in wl_loop_handle_ipc_client_fd_event(int, unsigned int, void*) (/home/neo/.local/lib/wayfire/libipc.so+0x94ad) (BuildId: d5566ec1cee538497b0223a125762f1c514aa4db)
    #34 0x7f22cd230641 in wl_event_loop_dispatch (/usr/lib/libwayland-server.so.0+0xa641) (BuildId: eaeed0645478c33a1a975d52ebd375751b26c19e)
    #35 0x7f22cd232776 in wl_display_run (/usr/lib/libwayland-server.so.0+0xc776) (BuildId: eaeed0645478c33a1a975d52ebd375751b26c19e)
    #36 0x55fb6a3faff0 in main ../src/main.cpp:514
    #37 0x7f22cae27b8a  (/usr/lib/libc.so.6+0x27b8a) (BuildId: 33141ef99aedd072bb910451a386a13eaf504222)
    #38 0x7f22cae27c4a in __libc_start_main (/usr/lib/libc.so.6+0x27c4a) (BuildId: 33141ef99aedd072bb910451a386a13eaf504222)
    #39 0x55fb6a3eec24 in _start (/usr/bin/wayfire+0x1ef7c24) (BuildId: 4b216497133f4761bed65c14272bc3b4c30930f5)

==98458==Register values:
rax = 0x0000000000000000  rbx = 0x0000000000002c52  rcx = 0x00007b52c8d05ab0  rdx = 0x0000000000000020  
rdi = 0x0000000000002c52  rsi = 0x0000000000000000  rbp = 0x00007ffce093a950  rsp = 0x00007ffce093a0b8  
 r8 = 0x00007f22caf98ec0   r9 = 0x0000000000000000  r10 = 0x00007b52c8d05aa0  r11 = 0x00007f22ccee4240  
r12 = 0x00007b52c8d05ab0  r13 = 0x00007ffce093a980  r14 = 0x000000003f800000  r15 = 0x00007c82c782a068  
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV (/usr/lib/libwlroots-0.19.so+0xc9e7b) (BuildId: f756bc23f5d7e65dcba214a62afaab2914429b6a) in wlr_xcursor_theme_load
==98458==ABORTING

Additional context

You must press the keybind a few times to trigger this crash

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions