--- a/src/shade.hpp +++ b/src/shade.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -34,7 +35,11 @@ { nonstd::observer_ptr deco = nullptr; wayfire_view view; - wf::output_t *output; + /* The output that currently holds pre_hook. Re-resolved from the view and + * migrated when the view changes output, and detached (while the output is + * still alive) when that output is removed, so a cached pointer can never + * dangle into a rem_effect() use-after-free. */ + wf::output_t *hook_output = nullptr; int titlebar_height; wf::option_wrapper_t shade_duration{"pixdecor/shade_duration"}; @@ -136,13 +141,10 @@ pixdecor_shade(wayfire_view view, int titlebar_height) : wf::scene::view_2d_transformer_t(view) { this->view = view; - this->output = view->get_output(); this->titlebar_height = titlebar_height; this->progression.shade.set(0.0, 1.0); - if (output) - { - output->render->add_effect(&pre_hook, wf::OUTPUT_EFFECT_PRE); - } + set_hook_output(view->get_output()); + wf::get_core().output_layout->connect(&on_output_removed); if (auto toplevel = wf::toplevel_cast(view)) { @@ -163,8 +165,55 @@ } } + /* Move pre_hook onto output o (which may be null), removing it from whatever + * output currently holds it. All add/rem of pre_hook goes through here so the + * tracked output stays in sync and is only ever a live pointer. */ + void set_hook_output(wf::output_t *o) + { + if (hook_output == o) + { + return; + } + + if (hook_output) + { + hook_output->render->rem_effect(&pre_hook); + } + + hook_output = o; + if (hook_output) + { + hook_output->render->add_effect(&pre_hook, wf::OUTPUT_EFFECT_PRE); + } + } + + wf::signal::connection_t on_output_removed = + [=] (wf::output_removed_signal *ev) + { + if (ev->output != hook_output) + { + return; + } + + /* Detach while the output is still alive, then re-home onto the view's + * current output if it has already migrated, so the animation can finish + * instead of leaving a stale hook (or dangling pointer) behind. */ + hook_output->render->rem_effect(&pre_hook); + hook_output = nullptr; + if (auto o = view ? view->get_output() : nullptr; o && (o != ev->output)) + { + set_hook_output(o); + } + }; + wf::effect_hook_t pre_hook = [=] () { + /* Follow the view if it has moved to another output since last frame. */ + if (view && view->get_output() && (view->get_output() != hook_output)) + { + set_hook_output(view->get_output()); + } + if (auto toplevel = wf::toplevel_cast(view)) { /* Re-resolve the decorator each frame: a cached observer_ptr dangles @@ -202,11 +251,7 @@ view->damage(); if (!this->progression.running() && !last_direction) { - if (output) - { - output->render->rem_effect(&pre_hook); - } - + set_hook_output(nullptr); pop_transformer(view); } }; @@ -282,10 +327,9 @@ virtual ~pixdecor_shade() { - if (output) - { - output->render->rem_effect(&pre_hook); - } + /* hook_output is kept valid (nulled when its output is removed), so this + * detaches cleanly instead of dereferencing a freed output. */ + set_hook_output(nullptr); } }; }