diff --git a/lib/wp/core.c b/lib/wp/core.c index c2542a41..2d344623 100644 --- a/lib/wp/core.c +++ b/lib/wp/core.c @@ -397,7 +397,21 @@ static void wp_core_dispose (GObject * obj) { WpCore *self = WP_CORE (obj); + GPtrArray *objects = self->registry.objects; + guint i; + /* Notify all plugins to prepare for shutdown before the registry is cleared. + * This allows the Lua scripting plugin to close its lua_State while all + * GObjects are still alive, preventing use-after-free in finalizers. */ + if (objects) { + for (i = 0; i < objects->len; i++) { + gpointer item = objects->pdata[i]; + if (WP_IS_PLUGIN (item)) + wp_plugin_prepare_shutdown (WP_PLUGIN (item)); + } + } + + /* Now it is safe to clear the registry */ wp_registry_clear (&self->registry); wp_object_update_features (WP_OBJECT (self), 0, WP_CORE_FEATURE_COMPONENTS); diff --git a/lib/wp/plugin.c b/lib/wp/plugin.c index e0da5548..ea9caa3f 100644 --- a/lib/wp/plugin.c +++ b/lib/wp/plugin.c @@ -213,6 +213,21 @@ wp_plugin_get_name (WpPlugin * self) return g_quark_to_string (priv->name_quark); } +/*! + * \brief Called during core shutdown before registry is cleared. + * + * \ingroup wpplugin + * \param self the plugin + * \returns void + */ +void +wp_plugin_prepare_shutdown (WpPlugin * self) +{ + g_return_if_fail (WP_IS_PLUGIN (self)); + if (WP_PLUGIN_GET_CLASS (self)->prepare_shutdown) + WP_PLUGIN_GET_CLASS (self)->prepare_shutdown (self); +} + /** * \var _WpPluginClass::enable * diff --git a/lib/wp/plugin.h b/lib/wp/plugin.h index 813cae60..36bddd82 100644 --- a/lib/wp/plugin.h +++ b/lib/wp/plugin.h @@ -36,9 +36,12 @@ struct _WpPluginClass void (*enable) (WpPlugin * self, WpTransition * transition); void (*disable) (WpPlugin * self); + /* New virtual method: called during core shutdown before registry is cleared. + Uses one of the padding slots to preserve ABI. */ + void (*prepare_shutdown) (WpPlugin * self); /*< private >*/ - WP_PADDING(6) + WP_PADDING(5) }; WP_API @@ -47,6 +50,9 @@ WpPlugin * wp_plugin_find (WpCore * core, const gchar * plugin_name); WP_API const gchar * wp_plugin_get_name (WpPlugin * self); +WP_API +void wp_plugin_prepare_shutdown (WpPlugin * self); + G_END_DECLS #endif diff --git a/modules/module-lua-scripting/module.c b/modules/module-lua-scripting/module.c index df64922d..35f1e3c4 100644 --- a/modules/module-lua-scripting/module.c +++ b/modules/module-lua-scripting/module.c @@ -21,6 +21,8 @@ struct _WpLuaScriptingPlugin { WpPlugin parent; lua_State *L; + GPtrArray *scripts; /* List of all loaded WpLuaScript objects */ + gboolean is_shutting_down; /* Flag to avoid double cleanup */ }; static int @@ -90,6 +92,8 @@ G_DEFINE_TYPE_WITH_CODE (WpLuaScriptingPlugin, wp_lua_scripting_plugin, static void wp_lua_scripting_plugin_init (WpLuaScriptingPlugin * self) { + self->scripts = g_ptr_array_new_with_free_func (g_object_unref); + self->is_shutting_down = FALSE; } static void @@ -112,12 +116,49 @@ wp_lua_scripting_plugin_enable (WpPlugin * plugin, WpTransition * transition) wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0); } +static void +wp_lua_scripting_plugin_prepare_shutdown (WpPlugin *plugin) +{ + WpLuaScriptingPlugin *self = WP_LUA_SCRIPTING_PLUGIN (plugin); + guint i; + + /* Avoid double execution */ + if (self->is_shutting_down) + return; + self->is_shutting_down = TRUE; + + /* 1. Close the lua_State early. This will run all pending __gc finalizers + * while all GObjects are still alive. */ + if (self->L) + g_clear_pointer (&self->L, wplua_unref); + + /* 2. Clear the lua_State pointer from every WpLuaScript that this plugin + * has loaded. This prevents them from trying to close the same state + * again in their finalize handlers during registry cleanup. */ + for (i = 0; i < self->scripts->len; i++) { + WpLuaScript *script = g_ptr_array_index (self->scripts, i); + if (WP_IS_LUA_SCRIPT (script)) + wp_lua_script_clear_lua_state (script); + } +} + static void wp_lua_scripting_plugin_disable (WpPlugin * plugin) { WpLuaScriptingPlugin * self = WP_LUA_SCRIPTING_PLUGIN (plugin); + if (self->is_shutting_down) + return; - g_clear_pointer (&self->L, wplua_unref); + if (self->L) + g_clear_pointer (&self->L, wplua_unref); +} + +static void +wp_lua_scripting_plugin_finalize (GObject *object) +{ + WpLuaScriptingPlugin *self = WP_LUA_SCRIPTING_PLUGIN (object); + g_clear_pointer (&self->scripts, g_ptr_array_unref); + G_OBJECT_CLASS (wp_lua_scripting_plugin_parent_class)->finalize (object); } static gboolean @@ -187,6 +228,7 @@ wp_lua_scripting_plugin_load (WpComponentLoader * cl, WpCore * core, "arguments", args, NULL); + g_ptr_array_add (self->scripts, g_object_ref (script)); g_task_return_pointer (task, g_steal_pointer (&script), g_object_unref); } @@ -204,9 +246,12 @@ static void wp_lua_scripting_plugin_class_init (WpLuaScriptingPluginClass * klass) { WpPluginClass *plugin_class = (WpPluginClass *) klass; + GObjectClass *object_class = G_OBJECT_CLASS (klass); plugin_class->enable = wp_lua_scripting_plugin_enable; plugin_class->disable = wp_lua_scripting_plugin_disable; + plugin_class->prepare_shutdown = wp_lua_scripting_plugin_prepare_shutdown; + object_class->finalize = wp_lua_scripting_plugin_finalize; } static void diff --git a/modules/module-lua-scripting/script.c b/modules/module-lua-scripting/script.c index b4fdf2c2..be1fddd7 100644 --- a/modules/module-lua-scripting/script.c +++ b/modules/module-lua-scripting/script.c @@ -61,11 +61,15 @@ wp_lua_script_finalize (GObject * object) { WpLuaScript *self = WP_LUA_SCRIPT (object); - wp_lua_script_cleanup (self); - g_clear_pointer (&self->L, wplua_unref); - g_clear_pointer (&self->filename, g_free); - g_clear_pointer (&self->args, wp_spa_json_unref); + /* If the lua_State has already been cleared by the plugin's prepare_shutdown, + * skip closing it again, but still free other resources. */ + if (self->L != NULL) { + wp_lua_script_cleanup (self); + g_clear_pointer (&self->L, wplua_unref); + } + g_clear_pointer (&self->filename, g_free); + g_clear_pointer (&self->args, wp_spa_json_unref); /* if args exists */ G_OBJECT_CLASS (wp_lua_script_parent_class)->finalize (object); } @@ -286,3 +290,12 @@ wp_lua_script_class_init (WpLuaScriptClass * klass) WP_TYPE_SPA_JSON, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); } + +void +wp_lua_script_clear_lua_state (WpLuaScript *self) +{ + g_return_if_fail (WP_IS_LUA_SCRIPT (self)); + /* Only clear the pointer; the lua_State is owned and destroyed by the + * WpLuaScriptingPlugin. Setting it to NULL prevents double closing. */ + self->L = NULL; +} diff --git a/modules/module-lua-scripting/script.h b/modules/module-lua-scripting/script.h index 826ea017..62cfb128 100644 --- a/modules/module-lua-scripting/script.h +++ b/modules/module-lua-scripting/script.h @@ -17,6 +17,12 @@ G_BEGIN_DECLS #define WP_TYPE_LUA_SCRIPT (wp_lua_script_get_type ()) G_DECLARE_FINAL_TYPE (WpLuaScript, wp_lua_script, WP, LUA_SCRIPT, WpPlugin) +/* Clear the internal lua_State pointer to prevent the finalizer from + * attempting to close the already-destroyed state. Called by the Lua + * scripting plugin during prepare_shutdown. */ +WP_API +void wp_lua_script_clear_lua_state (WpLuaScript *self); + G_END_DECLS #endif