A client can destroy its xdg_toplevel and keep the xdg_surface. wlroots
keeps the role but sets toplevel to NULL, and xdg_surface->data still
points to the Client freed by destroynotify(), so e.g. a popup created on
that surface made toplevel_from_wlr_surface() read freed memory.
The listener was only removed after the popup's initial commit was handled.
If the popup went away before that (closed before its first commit, no
parent, or destroyed by commitpopup() because its client has no monitor),
it stayed on the surface: wlroots then asserts when the surface is
destroyed, and a commit in between dereferences a NULL popup.
wlr_swapchain_acquire() returns a locked buffer and
wlr_output_state_set_buffer() takes its own lock, so every output enabled
through output management (wlr-randr, kanshi) leaked a buffer. Unlock it
once the state holds it, as wlr_output_swapchain_manager does.
When the session goes away, e.g. seatd being stopped at shutdown, wlroots
destroys the backend by itself (wlr_auto_backend_monitor), and
wlr_backend_finish() aborts since dwl's new_output and new_input listeners
are still attached:
Assertion failed: wl_list_empty(&backend->events.new_input.listener_list)
Detach them and quit instead.