From 3853d9e31551d7f2311a9aa7ddaa8c6782e93b14 Mon Sep 17 00:00:00 2001 From: Zongyao Chen Date: Wed, 8 Jul 2026 20:35:02 +0800 Subject: [PATCH 1/6] lua-lsm: fix shared dict lifetime on unregister shared[name] caches a Lua userdata that can outlive module unregister. The userdata used to hold a raw kvcache_dict pointer, while unregister freed the backing shared dict before every loaded VM was purged. If unregister left the module busy, later shared-dict access could dereference freed memory. Store a refcounted shared-dict wrapper in the userdata, tombstone shared dicts when unregister starts, and drop the module list reference only once unregister can finish. Allocate the Lua userdata slot before taking the per-userdata shared-dict reference, so allocation failures cannot leak the wrapper ref. Signed-off-by: Zongyao Chen --- security/lua/kvcache.c | 68 ++++++++++++++++++++++++------ security/lua/kvcache.h | 9 +++- security/lua/lsm.c | 95 +++++++++++++++++++++++++++++------------- security/lua/lsm.h | 5 +++ 4 files changed, 132 insertions(+), 45 deletions(-) diff --git a/security/lua/kvcache.c b/security/lua/kvcache.c index 37768761b669..ef75ff302665 100644 --- a/security/lua/kvcache.c +++ b/security/lua/kvcache.c @@ -556,6 +556,30 @@ void kvcache_dict_init(struct kvcache_dict *dict) atomic_set_release(&dict->state, KVCACHE_DICT_READY); } +static void lua_lsm_shdict_free_rcu(struct rcu_head *head) +{ + struct lua_lsm_module_shdict *shdict; + + shdict = container_of(head, struct lua_lsm_module_shdict, rcu); + kvcache_dict_free(&shdict->dict); + kfree(shdict); +} + +void lua_lsm_shdict_get(struct lua_lsm_module_shdict *shdict) +{ + if (shdict) + refcount_acquire(&shdict->refcount); +} + +void lua_lsm_shdict_put(struct lua_lsm_module_shdict *shdict) +{ + if (!shdict) + return; + + if (refcount_release(&shdict->refcount) == 0) + call_rcu(&shdict->rcu, lua_lsm_shdict_free_rcu); +} + /******************************** object cache *******************************/ const int _module_sentinel; @@ -674,28 +698,37 @@ int lua_object_newindex(lua_State *L, struct kvcache_dict *dict) static int shdict_set(lua_State *L) { - struct kvcache_dict *shdict = toshdict(L, 1); + struct lua_lsm_module_shdict *shdict = toshdict(L, 1); - return kvcache_set(L, shdict, NULL); + if (READ_ONCE(shdict->dead)) + return kvcache_result(L, -ESRCH); + + return kvcache_set(L, &shdict->dict, NULL); } static int shdict_get(lua_State *L) { - struct kvcache_dict *shdict = toshdict(L, 1); + struct lua_lsm_module_shdict *shdict = toshdict(L, 1); + + if (READ_ONCE(shdict->dead)) + return kvcache_result(L, -ESRCH); - return kvcache_get(L, shdict, NULL); + return kvcache_get(L, &shdict->dict, NULL); } static int shdict_incr(lua_State *L) { - struct kvcache_dict *shdict = toshdict(L, 1); + struct lua_lsm_module_shdict *shdict = toshdict(L, 1); + + if (READ_ONCE(shdict->dead)) + return kvcache_result(L, -ESRCH); - return kvcache_incr(L, shdict, NULL); + return kvcache_incr(L, &shdict->dict, NULL); } static int shdict_index(lua_State *L) { - struct kvcache_dict *shdict = toshdict(L, 1); + struct lua_lsm_module_shdict *shdict = toshdict(L, 1); /* metatable[key] */ if (lua_getmetatable(L, 1) == 0) { @@ -707,24 +740,33 @@ static int shdict_index(lua_State *L) if (!lua_isnoneornil(L, -1)) return 1; - return kvcache_get(L, shdict, NULL); + if (READ_ONCE(shdict->dead)) + return kvcache_result(L, -ESRCH); + + return kvcache_get(L, &shdict->dict, NULL); } static int shdict_tostring(lua_State *L) { - struct kvcache_dict *shdict = toshdict(L, 1); + struct lua_lsm_module_shdict *shdict = toshdict(L, 1); unsigned long flags; - read_lock_irqsave(&shdict->lock, flags); + if (READ_ONCE(shdict->dead)) { + lua_pushliteral(L, "shdict (dead)"); + return 1; + } + + read_lock_irqsave(&shdict->dict.lock, flags); lua_pushfstring(L, "shdict (%d / %d)", - atomic_read(&shdict->count), shdict->capacity); - read_unlock_irqrestore(&shdict->lock, flags); + atomic_read(&shdict->dict.count), + shdict->dict.capacity); + read_unlock_irqrestore(&shdict->dict.lock, flags); return 1; } static int shdict_gc(lua_State *L) { - __log_info_ratelimited("shdict already freed by unregister\n"); + lua_lsm_shdict_put(toshdict(L, 1)); return 0; } diff --git a/security/lua/kvcache.h b/security/lua/kvcache.h index bd0b1c1cea8a..feb4706218a1 100644 --- a/security/lua/kvcache.h +++ b/security/lua/kvcache.h @@ -27,6 +27,7 @@ enum kvcache_dict_state { }; struct lua_lsm_module; +struct lua_lsm_module_shdict; struct kvcache_node { const char *key; @@ -76,11 +77,15 @@ int lua_object_newindex(lua_State *L, struct kvcache_dict *dict); #define METH_SHARED_DICT "meth_shared_dict" #define newshdict(L) \ - ((struct kvcache_dict **)newcptr((L), METH_SHARED_DICT)) + ((struct lua_lsm_module_shdict **) \ + newcptr((L), METH_SHARED_DICT)) #define toshdictp(L, idx) \ - ((struct kvcache_dict **)luaL_checkudata((L), (idx), METH_SHARED_DICT)) + ((struct lua_lsm_module_shdict **) \ + luaL_checkudata((L), (idx), METH_SHARED_DICT)) #define toshdict(L, idx) (*toshdictp(L, idx)) +void lua_lsm_shdict_get(struct lua_lsm_module_shdict *shdict); +void lua_lsm_shdict_put(struct lua_lsm_module_shdict *shdict); int shdict_init(lua_State *L); #endif /* ! _SECURITY_LUA_LSM_KVCACHE_H */ diff --git a/security/lua/lsm.c b/security/lua/lsm.c index 8aae5933065a..18558105bed5 100644 --- a/security/lua/lsm.c +++ b/security/lua/lsm.c @@ -580,8 +580,10 @@ void lvm_put(lua_State *L) static int lua_shared_index(lua_State *L) { const char *name = luaL_checkstring(L, 2); - struct lua_lsm_module_shdict *shdict, *shtmp; + struct lua_lsm_module_shdict *shdict = NULL, *new = NULL, *shtmp; + struct lua_lsm_module_shdict **slot; struct lua_lsm_module *module; + unsigned long flags; int found = 0; __log_info_ratelimited("READ shared table, [%s] %s\n", @@ -595,29 +597,40 @@ static int lua_shared_index(lua_State *L) return 0; } module = lua_touserdata(L, -1); + if (READ_ONCE(module->state) != LMS_STATE_LIVE) + return 0; + + lua_pushvalue(L, 2); + slot = newshdict(L); - rcu_read_lock(); - list_for_each_entry_rcu(shdict, &module->shdicts, list) { - if (strcmp(shdict->name, name) == 0) { + spin_lock_irqsave(&module->shdict_lock, flags); + list_for_each_entry(shtmp, &module->shdicts, list) { + if (strcmp(shtmp->name, name) == 0) { found = 1; break; } } - rcu_read_unlock(); + if (found && !READ_ONCE(shtmp->dead)) { + lua_lsm_shdict_get(shtmp); + shdict = shtmp; + } + spin_unlock_irqrestore(&module->shdict_lock, flags); - if (!found) { - unsigned long flags; + if (found && !shdict) + goto err_pop_userdata; + + if (!shdict) { size_t l = strlen(name); - shdict = kmalloc(struct_size(shdict, name, l + 1), - lua_lsm_gfp()); - if (!shdict) { + new = kzalloc(struct_size(new, name, l + 1), lua_lsm_gfp()); + if (!new) { __log_err("No memory\n"); - return 0; + goto err_pop_userdata; } - kvcache_dict_init(&shdict->dict); - memcpy(shdict->name, name, l); - shdict->name[l] = '\0'; + refcount_init(&new->refcount, 1); + kvcache_dict_init(&new->dict); + memcpy(new->name, name, l); + new->name[l] = '\0'; spin_lock_irqsave(&module->shdict_lock, flags); list_for_each_entry(shtmp, &module->shdicts, list) { @@ -626,28 +639,37 @@ static int lua_shared_index(lua_State *L) break; } } - if (!found) { + if (found) { + if (!READ_ONCE(shtmp->dead)) { + lua_lsm_shdict_get(shtmp); + shdict = shtmp; + } + } else if (READ_ONCE(module->state) == LMS_STATE_LIVE) { atomic_inc(&module->shdict_count); - list_add_tail_rcu(&shdict->list, &module->shdicts); + list_add_tail_rcu(&new->list, &module->shdicts); + lua_lsm_shdict_get(new); + shdict = new; + new = NULL; } spin_unlock_irqrestore(&module->shdict_lock, flags); - if (found) { - kvcache_dict_free(&shdict->dict); - kfree(shdict); - - shdict = shtmp; - } + lua_lsm_shdict_put(new); } + if (!shdict) + goto err_pop_userdata; + /* shared[name] = shdict */ - lua_pushvalue(L, 2); - *newshdict(L) = &shdict->dict; + *slot = shdict; lua_rawset(L, 1); lua_settop(L, 2); lua_rawget(L, 1); return 1; + +err_pop_userdata: + lua_settop(L, 2); + return 0; } static int lua_shared_newindex(lua_State *L) @@ -743,6 +765,8 @@ static int lua_modules_index(lua_State *L) /* module queries are always run with a read lock */ list_for_each_entry_srcu(module, &lsm_modules, list, srcu_read_lock_held(&modules_ss)) { + if (READ_ONCE(module->state) != LMS_STATE_LIVE) + continue; if (strcmp(module->name, key) != 0) continue; @@ -1288,6 +1312,7 @@ int lua_lsm_module_unregister(const char *name) struct lua_lsm_module *module; struct lua_lsm_module_shdict *shdict, *tmp; int count = 0, nloaded, nbusy; + unsigned long flags; unsigned int cpu; int found = 0; int err; @@ -1319,12 +1344,15 @@ int lua_lsm_module_unregister(const char *name) synchronize_srcu(&modules_ss); - list_for_each_entry_safe(shdict, tmp, &module->shdicts, list) { - list_del(&shdict->list); - kvcache_dict_free(&shdict->dict); - kfree(shdict); - atomic_dec(&module->shdict_count); - } + /* + * Existing Lua userdata may outlive the VM purge attempt. Tombstone + * shared dicts now and drop the module list refs only once unregister + * can complete. + */ + spin_lock_irqsave(&module->shdict_lock, flags); + list_for_each_entry(shdict, &module->shdicts, list) + WRITE_ONCE(shdict->dead, true); + spin_unlock_irqrestore(&module->shdict_lock, flags); kvcache_module_nodes_gc(module); @@ -1397,6 +1425,13 @@ int lua_lsm_module_unregister(const char *name) if (atomic_sub_return(count, &module->nloaded) == 0) { list_del_rcu(&module->list); + spin_lock_irqsave(&module->shdict_lock, flags); + list_for_each_entry_safe(shdict, tmp, &module->shdicts, list) { + list_del_rcu(&shdict->list); + atomic_dec(&module->shdict_count); + lua_lsm_shdict_put(shdict); + } + spin_unlock_irqrestore(&module->shdict_lock, flags); synchronize_srcu(&modules_ss); lua_lsm_module_free(module); err = 0; diff --git a/security/lua/lsm.h b/security/lua/lsm.h index 95082c2e54cb..3fa8ac91dc26 100644 --- a/security/lua/lsm.h +++ b/security/lua/lsm.h @@ -9,6 +9,7 @@ #define _SECURITY_LUA_LSM_LSM_H #include +#include #include #include #include @@ -94,6 +95,10 @@ static inline bool lua_lsm_hook_supported(unsigned int nr) struct lua_lsm_module_shdict { struct list_head list; + /* One ref for the module list, one per cached Lua userdata. */ + atomic_t refcount; + struct rcu_head rcu; + bool dead; struct kvcache_dict dict; char name[]; }; From 0549a883ac2918623e9e53d54d0196571a0882a9 Mon Sep 17 00:00:00 2001 From: Zongyao Chen Date: Wed, 8 Jul 2026 20:35:02 +0800 Subject: [PATCH 2/6] lua-lsm: avoid Lua teardown from task_call_func task_call_func() may run callbacks under pi_lock and, for runnable tasks, the runqueue lock. The unregister path used it to remove modules from other tasks' Lua VMs, which updates Lua tables and can run a full Lua GC. Stop doing remote task VM teardown from task_call_func(). Unregister now purges only the current task and per-cpu Lua VMs directly; other task VMs purge unloading modules lazily when they next enter Lua-LSM. Modules that still have loaded VMs remain zombie and unregister returns -EBUSY until the remaining VMs have purged them. Signed-off-by: Zongyao Chen --- security/lua/lsm.c | 318 ++++++++++++++++++++-------------------- security/lua/lsm_defs.h | 2 +- 2 files changed, 159 insertions(+), 161 deletions(-) diff --git a/security/lua/lsm.c b/security/lua/lsm.c index 18558105bed5..f362830654bc 100644 --- a/security/lua/lsm.c +++ b/security/lua/lsm.c @@ -277,6 +277,7 @@ static void lua_state_free(struct lvm_state *lvm); #define LUA_LVM_POOL_MAX_LIMIT 256 static unsigned int lua_lvm_pool_max __read_mostly = LUA_LVM_POOL_MAX_DEFAULT; +static atomic_t modules_unloading = ATOMIC_INIT(0); static int __init lua_lvm_pool_max_setup(char *str) { @@ -482,6 +483,81 @@ lvm_get_task_state(const struct task_struct *task, bool create) return lvm; } +static int lvm_remove_module(lua_State *L, struct lua_lsm_module *module) +{ + int err = -ENOENT; + int modules_idx; + + /* registry._MODULES[modname] = nil */ + lua_getfield(L, LUA_REGISTRYINDEX, "_MODULES"); + if (lua_istable(L, -1)) { + modules_idx = lua_gettop(L); + lua_pushstring(L, module->name); + lua_rawget(L, modules_idx); + if (lua_istable(L, -1)) { + lua_pop(L, 1); + + lua_pushstring(L, module->name); + lua_pushnil(L); + lua_rawset(L, modules_idx); + + lua_getfield(L, LUA_REGISTRYINDEX, "_LOADED"); + if (lua_istable(L, -1)) { + lua_pushstring(L, module->name); + lua_pushnil(L); + lua_rawset(L, -3); + } + lua_pop(L, 1); + + /* performs a full garbage-collection cycle. */ + lua_gc(L, LUA_GCCOLLECT, 0); + err = 0; + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); + return err; +} + +static int lvm_purge_module(lua_State *L, struct lua_lsm_module *module) +{ + int nloaded; + int err; + + if (!module) + return -ENOENT; + + err = lvm_remove_module(L, module); + if (!err) { + nloaded = atomic_dec_return(&module->nloaded); + WARN_ON_ONCE(nloaded < 0); + + __log_info("<%s>: %d-%d purged module <%s>, nloaded = %d\n", + current->comm, task_tgid_nr(current), + task_pid_nr(current), module->name, nloaded); + } + return err; +} + +static void lvm_purge_unloading(lua_State *L) +{ + struct lua_lsm_module *module; + int idx; + + if (atomic_read(&modules_unloading) == 0) + return; + + idx = srcu_read_lock(&modules_ss); + list_for_each_entry_srcu(module, &lsm_modules, list, + srcu_read_lock_held(&modules_ss)) { + if (READ_ONCE(module->state) == LMS_STATE_GOING || + READ_ONCE(module->state) == LMS_STATE_ZOMBIE) + lvm_purge_module(L, module); + } + srcu_read_unlock(&modules_ss, idx); +} + static lua_State * lvm_get_from_task(const struct task_struct *task, bool require_idle) { @@ -559,12 +635,18 @@ static void lvm_put_to_task(const struct task_struct *task, lua_State *L) lua_State *lvm_get(void) { + lua_State *L; + BUG_ON(in_nmi() || in_hardirq()); - if (in_task()) - return lvm_get_from_task(current, false); - else + if (in_task()) { + L = lvm_get_from_task(current, false); + if (L) + lvm_purge_unloading(L); + return L; + } else { return get_cpu_var(irq_lvms)->L; + } } void lvm_put(lua_State *L) @@ -1022,7 +1104,7 @@ int lua_lsm_module_register(const char *code, size_t len) if (!module) goto err_free_lua; - module->state = LMS_STATE_COMING; + WRITE_ONCE(module->state, LMS_STATE_COMING); __BITMAP_ZERO(&module->hookfuncs); /* traversal the result table */ @@ -1140,6 +1222,7 @@ int lua_lsm_module_register(const char *code, size_t len) goto err_free_module; memcpy(module->chunk, chunk, chunk_len); module->chunk_len = chunk_len; + atomic_set(&module->nloaded, 0); INIT_LIST_HEAD(&module->shdicts); spin_lock_init(&module->shdict_lock); @@ -1162,7 +1245,7 @@ int lua_lsm_module_register(const char *code, size_t len) atomic_inc(&lua_lsm_hook_stats[i].nhooks); } - module->state = LMS_STATE_LIVE; + WRITE_ONCE(module->state, LMS_STATE_LIVE); list_add_tail_rcu(&module->list, &lsm_modules); /* * Once any Lua policy has been loaded, inactive free-hook cleanup @@ -1193,92 +1276,6 @@ int lua_lsm_module_register(const char *code, size_t len) return err; } -static int lvm_remove_module(lua_State *L, struct lua_lsm_module *module) -{ - int err = -ENOENT; - /* registry._MODULES[modname] = nil */ - lua_getfield(L, LUA_REGISTRYINDEX, "_MODULES"); - if (lua_istable(L, -1)) { - lua_pushstring(L, module->name); - lua_rawget(L, -2); - if (lua_istable(L, -1)) { - lua_pop(L, 1); - - lua_pushstring(L, module->name); - lua_pushnil(L); - lua_rawset(L, -3); - - /* performs a full garbage-collection cycle. */ - lua_gc(L, LUA_GCCOLLECT, 0); - err = 0; - } else { - lua_pop(L, 1); - } - } - lua_pop(L, 1); - return err; -} - -static int task_remove_module(struct task_struct *task, void *arg) -{ - struct lua_lsm_task *llt = lua_lsm_task(task); - struct lua_lsm_module *module = arg; - struct lvm_state *lvm; - lua_State *L; - int err; - - if (task_curr(task) && task != current) - return -EBUSY; - - /* Unregister must not lazily create a VM for untouched tasks. */ - /* Pairs with cmpxchg() in lvm_get_task_state(). */ - lvm = smp_load_acquire(&llt->lvm); - if (!lvm || !smp_load_acquire(&lvm->L)) - return -ENOENT; - - L = lvm_get_from_task(task, true); - if (!L) - return -EAGAIN; - - err = lvm_remove_module(L, module); - lvm_put_to_task(task, L); - return err; -} - -static int tasks_lvm_remove_module(struct lua_lsm_module *module, int *nbusy) -{ - struct task_struct *g, *task; - int count = 0; - int err; - - *nbusy = 0; - /* remove loaded module from every Lua VM */ - read_lock(&tasklist_lock); - for_each_process_thread(g, task) { - if (task == current) - err = task_remove_module(task, module); - else - err = task_call_func(task, task_remove_module, module); - - if (!err) { - count++; - __log_info("<%s>: err = [ OK ] \t<%s>: %d-%d\n", module->name, - task->comm, task_tgid_nr(task), task_pid_nr(task)); - } else if (err == -EBUSY || err == -EAGAIN) { - *nbusy += 1; - __log_info("<%s>: err = %s \t<%s>: %d-%d\n", - module->name, err == -EBUSY ? "EBUSY" : "EAGAIN", - task->comm, task_tgid_nr(task), task_pid_nr(task)); - } else if (err != -ENOENT) { - __log_info_ratelimited("<%s>: err = %d \t<%s>: %d-%d\n", - module->name, err, - task->comm, task_tgid_nr(task), task_pid_nr(task)); - } - } - read_unlock(&tasklist_lock); - return count; -} - /* * Due to the limitations of schedule_on_each_cpu(), global variables * are used to pass parameters to the callback function. @@ -1298,7 +1295,7 @@ static void softirq_lvm_remove_module(struct work_struct *work) * changing the Lua VM environment. */ local_bh_disable(); - err = lvm_remove_module(per_cpu(irq_lvms, cpu)->L, module); + err = lvm_purge_module(per_cpu(irq_lvms, cpu)->L, module); if (!err) { atomic_inc(&work_ctx_remove_count); __log_info("<%s>: err = [ OK ] \t, count = %d\n", @@ -1307,16 +1304,50 @@ static void softirq_lvm_remove_module(struct work_struct *work) local_bh_enable(); } +static void lua_lsm_module_unlink_shdicts(struct lua_lsm_module *module) +{ + struct lua_lsm_module_shdict *shdict, *tmp; + unsigned long flags; + + spin_lock_irqsave(&module->shdict_lock, flags); + list_for_each_entry_safe(shdict, tmp, &module->shdicts, list) { + list_del_rcu(&shdict->list); + atomic_dec(&module->shdict_count); + lua_lsm_shdict_put(shdict); + } + spin_unlock_irqrestore(&module->shdict_lock, flags); +} + +static void lua_lsm_module_disable_hooks(struct lua_lsm_module *module) +{ + int i; + + for (i = 0; lua_lsm_hook_stats[i].name; i++) { + if (__BITMAP_ISSET(i, &module->hookfuncs)) + atomic_dec(&lua_lsm_hook_stats[i].nhooks); + } +} + +static void lua_lsm_module_finish_unregister(struct lua_lsm_module *module) +{ + WARN_ON_ONCE(atomic_read(&module->nloaded) != 0); + + lua_lsm_module_disable_hooks(module); + list_del_rcu(&module->list); + lua_lsm_module_unlink_shdicts(module); + synchronize_srcu(&modules_ss); + lua_lsm_module_free(module); + atomic_dec(&modules_unloading); +} + int lua_lsm_module_unregister(const char *name) { struct lua_lsm_module *module; - struct lua_lsm_module_shdict *shdict, *tmp; - int count = 0, nloaded, nbusy; + struct lua_lsm_module_shdict *shdict; + int count = 0, nloaded, remaining; unsigned long flags; - unsigned int cpu; int found = 0; int err; - int i; mutex_lock(&modules_mutex); list_for_each_entry(module, &lsm_modules, list) { @@ -1325,14 +1356,13 @@ int lua_lsm_module_unregister(const char *name) break; } } - if (found && module->state == LMS_STATE_LIVE) { - module->state = LMS_STATE_GOING; - static_branch_dec(&lua_lsm_modules_active); - - for (i = 0; lua_lsm_hook_stats[i].name; i++) { - if (__BITMAP_ISSET(i, &module->hookfuncs)) - atomic_dec(&lua_lsm_hook_stats[i].nhooks); - } + if (found && READ_ONCE(module->state) == LMS_STATE_LIVE) { + /* + * Keep the hook counts until final removal so tasks keep + * entering Lua-LSM and can purge their own VMs. + */ + WRITE_ONCE(module->state, LMS_STATE_GOING); + atomic_inc(&modules_unloading); } if (!found) { @@ -1340,6 +1370,13 @@ int lua_lsm_module_unregister(const char *name) return -ENOENT; } + if (READ_ONCE(module->state) == LMS_STATE_ZOMBIE && + atomic_read(&module->nloaded) == 0) { + lua_lsm_module_finish_unregister(module); + mutex_unlock(&modules_mutex); + return 0; + } + pr_info("Prepare to unregister module <%s> ...\n", name); synchronize_srcu(&modules_ss); @@ -1357,12 +1394,18 @@ int lua_lsm_module_unregister(const char *name) kvcache_module_nodes_gc(module); nloaded = atomic_read(&module->nloaded); - /* remove loaded module from every Lua VM */ if (nloaded > 0) { - count += tasks_lvm_remove_module(module, &nbusy); + lua_State *L = lvm_get_from_task(current, true); - __log_info("Unregister module <%s> from task, freed = %d/%d, nbusy = %d\n", - name, count, nloaded, nbusy); + if (L) { + err = lvm_purge_module(L, module); + lvm_put_to_task(current, L); + if (!err) + count++; + } + + __log_info("Unregister module <%s> from current task, freed = %d/%d\n", + name, count, nloaded); } /* @@ -1370,7 +1413,7 @@ int lua_lsm_module_unregister(const char *name) * so the nloaded will be updated in the air. */ nloaded = atomic_read(&module->nloaded); - if (count < nloaded) { + if (nloaded > 0) { /* * Since global variables are used, locking ensures that only * one instance of the softirq LuaVM offload is executed. @@ -1386,63 +1429,18 @@ int lua_lsm_module_unregister(const char *name) name, count, nloaded); } - nloaded = atomic_read(&module->nloaded); - if (count < nloaded) { - /* ditto for the idle 'swapper' tasks */ - cpus_read_lock(); - for_each_possible_cpu(cpu) { - /* TODO: remove 'swapper' tasks Lua VM */ - err = task_call_func(idle_task(cpu), task_remove_module, module); - if (!err) - count++; - } - cpus_read_unlock(); - - __log_info("Unregister module <%s> from swapper, freed = %d/%d\n", - name, count, atomic_read(&module->nloaded)); - } - - nloaded = atomic_read(&module->nloaded); - if (count < nloaded && nbusy > 0) { - for (i = 1; i <= 5; i++) { - count += tasks_lvm_remove_module(module, &nbusy); - WARN_ON(count > nloaded); - - __log_info("Unregister module <%s> from task, freed = %d/%d, nbusy = %d, loop = %d\n", - name, count, nloaded, nbusy, i); - - nloaded = atomic_read(&module->nloaded); - if (count == nloaded || nbusy == 0) - break; - - msleep(500 * i); - - nloaded = atomic_read(&module->nloaded); - if (count == nloaded) - break; - } - } - - if (atomic_sub_return(count, &module->nloaded) == 0) { - list_del_rcu(&module->list); - spin_lock_irqsave(&module->shdict_lock, flags); - list_for_each_entry_safe(shdict, tmp, &module->shdicts, list) { - list_del_rcu(&shdict->list); - atomic_dec(&module->shdict_count); - lua_lsm_shdict_put(shdict); - } - spin_unlock_irqrestore(&module->shdict_lock, flags); - synchronize_srcu(&modules_ss); - lua_lsm_module_free(module); + remaining = atomic_read(&module->nloaded); + if (remaining == 0) { + lua_lsm_module_finish_unregister(module); err = 0; } else { - module->state = LMS_STATE_ZOMBIE; + WRITE_ONCE(module->state, LMS_STATE_ZOMBIE); err = -EBUSY; } mutex_unlock(&modules_mutex); - pr_info("Unregistered module <%s> from %d/%d Lua VMs, vm_nusage = %d\n", - name, count, nloaded, atomic_read(&vm_nusage)); + pr_info("Unregister module <%s>: purged %d Lua VMs, remaining = %d, vm_nusage = %d\n", + name, count, remaining, atomic_read(&vm_nusage)); return err; } diff --git a/security/lua/lsm_defs.h b/security/lua/lsm_defs.h index 47966158575b..6cd8e77dd2b6 100644 --- a/security/lua/lsm_defs.h +++ b/security/lua/lsm_defs.h @@ -275,7 +275,7 @@ static inline bool lua_lsm_hook_has_inactive_cleanup(unsigned int nr) srcu_read_lock_held(&modules_ss)) { \ if (!__BITMAP_ISSET(__LL_NR_ ## NAME, &module->hookfuncs)) \ continue; \ - if (module->state != LMS_STATE_LIVE) \ + if (READ_ONCE(module->state) != LMS_STATE_LIVE) \ continue; \ lua_getfield(L, -1, module->name); \ lua_getfield(L, -1, #NAME); \ From 485ad09886fb0516237879e2d3c37fe1790f95d7 Mon Sep 17 00:00:00 2001 From: Zongyao Chen Date: Wed, 8 Jul 2026 20:35:02 +0800 Subject: [PATCH 3/6] lua-lsm: finalize drained modules after lazy unload Lazy unload can drop a module's last VM-local reference after unregister() has already returned -EBUSY. In that case the module used to stay on the unloading list until another unregister() retried the final teardown. Queue a worker when lazy purge or task teardown drains nloaded to zero. The worker detaches the drained zombie under modules_mutex, waits for the modules_ss SRCU grace period, and then frees the module outside the mutex. Also skip purge attempts once nloaded is already zero and make lua_modules_free() explicitly rely on the caller's existing SRCU read-side protection. Signed-off-by: Zongyao Chen --- security/lua/lsm.c | 148 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 126 insertions(+), 22 deletions(-) diff --git a/security/lua/lsm.c b/security/lua/lsm.c index f362830654bc..c6e325a32d9f 100644 --- a/security/lua/lsm.c +++ b/security/lua/lsm.c @@ -270,6 +270,14 @@ static DEFINE_PER_CPU(struct lvm_state *, irq_lvms); static lua_State *lvm_build_lua_state(struct lvm_state *lvm); static void *lvm_alloc(void *ud, void *ptr, size_t osize, size_t nsize); +static void lua_lsm_module_schedule_finalize(void); +static void lua_lsm_module_finalize_workfn(struct work_struct *work); +static DECLARE_WORK(lua_lsm_module_finalize_work, + lua_lsm_module_finalize_workfn); +static DEFINE_SPINLOCK(lua_lsm_module_finalize_lock); +static bool lua_lsm_module_finalize_pending; +static bool lua_lsm_module_finalize_running; + static int lua_state_alloc(struct lvm_state *lvm); static void lua_state_free(struct lvm_state *lvm); @@ -532,6 +540,8 @@ static int lvm_purge_module(lua_State *L, struct lua_lsm_module *module) if (!err) { nloaded = atomic_dec_return(&module->nloaded); WARN_ON_ONCE(nloaded < 0); + if (nloaded == 0 && READ_ONCE(module->state) == LMS_STATE_ZOMBIE) + lua_lsm_module_schedule_finalize(); __log_info("<%s>: %d-%d purged module <%s>, nloaded = %d\n", current->comm, task_tgid_nr(current), @@ -540,6 +550,14 @@ static int lvm_purge_module(lua_State *L, struct lua_lsm_module *module) return err; } +static bool lvm_module_needs_purge(struct lua_lsm_module *module) +{ + enum lua_lsm_module_state state = READ_ONCE(module->state); + + return atomic_read(&module->nloaded) > 0 && + (state == LMS_STATE_GOING || state == LMS_STATE_ZOMBIE); +} + static void lvm_purge_unloading(lua_State *L) { struct lua_lsm_module *module; @@ -551,8 +569,7 @@ static void lvm_purge_unloading(lua_State *L) idx = srcu_read_lock(&modules_ss); list_for_each_entry_srcu(module, &lsm_modules, list, srcu_read_lock_held(&modules_ss)) { - if (READ_ONCE(module->state) == LMS_STATE_GOING || - READ_ONCE(module->state) == LMS_STATE_ZOMBIE) + if (lvm_module_needs_purge(module)) lvm_purge_module(L, module); } srcu_read_unlock(&modules_ss, idx); @@ -880,6 +897,7 @@ static int lua_modules_index(lua_State *L) static void lua_modules_free(struct task_struct *task, lua_State *L) { struct lua_lsm_module *module; + int nloaded; lua_getfield(L, LUA_REGISTRYINDEX, "_MODULES"); if (!lua_istable(L, -1)) { @@ -887,14 +905,22 @@ static void lua_modules_free(struct task_struct *task, lua_State *L) return; } - list_for_each_entry_srcu(module, &lsm_modules, list, srcu_read_lock_held(&modules_ss)) { + list_for_each_entry_srcu(module, &lsm_modules, list, + srcu_read_lock_held(&modules_ss)) { lua_pushstring(L, module->name); lua_rawget(L, -2); if (lua_istable(L, -1)) { - atomic_dec(&module->nloaded); - __log_info("<%s>: %d-%d freed module <%s>, nloaded = %d\n", - task->comm, task_tgid_nr(task), task_pid_nr(task), - module->name, atomic_read(&module->nloaded)); + nloaded = atomic_dec_return(&module->nloaded); + WARN_ON_ONCE(nloaded < 0); + if (nloaded == 0 && READ_ONCE(module->state) == LMS_STATE_ZOMBIE) + lua_lsm_module_schedule_finalize(); + if (task) + __log_info("<%s>: %d-%d dropped module <%s>, nloaded = %d\n", + task->comm, task_tgid_nr(task), task_pid_nr(task), + module->name, nloaded); + else + __log_info("dropped module <%s>, nloaded = %d\n", + module->name, nloaded); } lua_pop(L, 1); } @@ -1304,6 +1330,12 @@ static void softirq_lvm_remove_module(struct work_struct *work) local_bh_enable(); } +static bool lua_lsm_module_drained(struct lua_lsm_module *module) +{ + return READ_ONCE(module->state) == LMS_STATE_ZOMBIE && + atomic_read(&module->nloaded) == 0; +} + static void lua_lsm_module_unlink_shdicts(struct lua_lsm_module *module) { struct lua_lsm_module_shdict *shdict, *tmp; @@ -1328,16 +1360,83 @@ static void lua_lsm_module_disable_hooks(struct lua_lsm_module *module) } } -static void lua_lsm_module_finish_unregister(struct lua_lsm_module *module) +static void lua_lsm_module_detach_locked(struct lua_lsm_module *module) { WARN_ON_ONCE(atomic_read(&module->nloaded) != 0); + WARN_ON_ONCE(READ_ONCE(module->state) != LMS_STATE_GOING && + READ_ONCE(module->state) != LMS_STATE_ZOMBIE); lua_lsm_module_disable_hooks(module); list_del_rcu(&module->list); lua_lsm_module_unlink_shdicts(module); + atomic_dec(&modules_unloading); +} + +static void lua_lsm_module_free_detached(struct lua_lsm_module *module) +{ synchronize_srcu(&modules_ss); lua_lsm_module_free(module); - atomic_dec(&modules_unloading); +} + +static void lua_lsm_module_schedule_finalize(void) +{ + unsigned long flags; + bool queue = false; + + spin_lock_irqsave(&lua_lsm_module_finalize_lock, flags); + lua_lsm_module_finalize_pending = true; + if (!lua_lsm_module_finalize_running) { + lua_lsm_module_finalize_running = true; + queue = true; + } + spin_unlock_irqrestore(&lua_lsm_module_finalize_lock, flags); + + if (queue) + schedule_work(&lua_lsm_module_finalize_work); +} + +static void lua_lsm_module_finalize_workfn(struct work_struct *work) +{ + struct lua_lsm_module *module; + unsigned long flags; + bool found; + + for (;;) { + found = false; + + spin_lock_irqsave(&lua_lsm_module_finalize_lock, flags); + lua_lsm_module_finalize_pending = false; + spin_unlock_irqrestore(&lua_lsm_module_finalize_lock, flags); + + mutex_lock(&modules_mutex); + list_for_each_entry(module, &lsm_modules, list) { + if (lua_lsm_module_drained(module)) { + lua_lsm_module_detach_locked(module); + found = true; + break; + } + } + mutex_unlock(&modules_mutex); + + if (found) { + lua_lsm_module_free_detached(module); + continue; + } + + spin_lock_irqsave(&lua_lsm_module_finalize_lock, flags); + if (!lua_lsm_module_finalize_pending) { + lua_lsm_module_finalize_running = false; + spin_unlock_irqrestore(&lua_lsm_module_finalize_lock, flags); + return; + } + spin_unlock_irqrestore(&lua_lsm_module_finalize_lock, flags); + } +} + +static void lua_lsm_module_finish_unregister(struct lua_lsm_module *module) +{ + lua_lsm_module_detach_locked(module); + lua_lsm_module_free_detached(module); } int lua_lsm_module_unregister(const char *name) @@ -1370,12 +1469,8 @@ int lua_lsm_module_unregister(const char *name) return -ENOENT; } - if (READ_ONCE(module->state) == LMS_STATE_ZOMBIE && - atomic_read(&module->nloaded) == 0) { - lua_lsm_module_finish_unregister(module); - mutex_unlock(&modules_mutex); - return 0; - } + if (lua_lsm_module_drained(module)) + goto out_detach; pr_info("Prepare to unregister module <%s> ...\n", name); @@ -1430,13 +1525,22 @@ int lua_lsm_module_unregister(const char *name) } remaining = atomic_read(&module->nloaded); - if (remaining == 0) { - lua_lsm_module_finish_unregister(module); - err = 0; - } else { - WRITE_ONCE(module->state, LMS_STATE_ZOMBIE); - err = -EBUSY; - } + if (remaining == 0) + goto out_detach; + + WRITE_ONCE(module->state, LMS_STATE_ZOMBIE); + if (lua_lsm_module_drained(module)) + goto out_detach; + + remaining = atomic_read(&module->nloaded); + err = -EBUSY; + goto out_unlock; + +out_detach: + remaining = 0; + lua_lsm_module_finish_unregister(module); + err = 0; +out_unlock: mutex_unlock(&modules_mutex); pr_info("Unregister module <%s>: purged %d Lua VMs, remaining = %d, vm_nusage = %d\n", From 229619b9f68f6cdc70099cc2f6d298eef4b24f38 Mon Sep 17 00:00:00 2001 From: Zongyao Chen Date: Thu, 9 Jul 2026 14:57:04 +0800 Subject: [PATCH 4/6] lua-lsm: clarify module unload lifecycle Rely on workqueue serialization for async module finalization. Use SRCU callbacks for delayed module free after list removal. Rename VM-side unload helpers around dropping modules from Lua VMs. Name the loaded VM count explicitly. Signed-off-by: Zongyao Chen --- security/lua/lsm.c | 230 ++++++++++++++++++++------------------------- security/lua/lsm.h | 4 +- 2 files changed, 104 insertions(+), 130 deletions(-) diff --git a/security/lua/lsm.c b/security/lua/lsm.c index c6e325a32d9f..29ebd24d8c68 100644 --- a/security/lua/lsm.c +++ b/security/lua/lsm.c @@ -25,6 +25,8 @@ #include #include #include +#include +#include #include "lsm.h" #include "lua_object.h" #include "lsm_defs.h" @@ -270,13 +272,10 @@ static DEFINE_PER_CPU(struct lvm_state *, irq_lvms); static lua_State *lvm_build_lua_state(struct lvm_state *lvm); static void *lvm_alloc(void *ud, void *ptr, size_t osize, size_t nsize); -static void lua_lsm_module_schedule_finalize(void); +static int lua_lsm_module_drop_from_vm(struct lua_lsm_module *module); static void lua_lsm_module_finalize_workfn(struct work_struct *work); static DECLARE_WORK(lua_lsm_module_finalize_work, lua_lsm_module_finalize_workfn); -static DEFINE_SPINLOCK(lua_lsm_module_finalize_lock); -static bool lua_lsm_module_finalize_pending; -static bool lua_lsm_module_finalize_running; static int lua_state_alloc(struct lvm_state *lvm); static void lua_state_free(struct lvm_state *lvm); @@ -491,7 +490,7 @@ lvm_get_task_state(const struct task_struct *task, bool create) return lvm; } -static int lvm_remove_module(lua_State *L, struct lua_lsm_module *module) +static int lvm_forget_module(lua_State *L, struct lua_lsm_module *module) { int err = -ENOENT; int modules_idx; @@ -528,37 +527,33 @@ static int lvm_remove_module(lua_State *L, struct lua_lsm_module *module) return err; } -static int lvm_purge_module(lua_State *L, struct lua_lsm_module *module) +static int lvm_drop_module(lua_State *L, struct lua_lsm_module *module) { - int nloaded; int err; if (!module) return -ENOENT; - err = lvm_remove_module(L, module); + err = lvm_forget_module(L, module); if (!err) { - nloaded = atomic_dec_return(&module->nloaded); - WARN_ON_ONCE(nloaded < 0); - if (nloaded == 0 && READ_ONCE(module->state) == LMS_STATE_ZOMBIE) - lua_lsm_module_schedule_finalize(); + int loaded_vms = lua_lsm_module_drop_from_vm(module); - __log_info("<%s>: %d-%d purged module <%s>, nloaded = %d\n", + __log_info("<%s>: %d-%d dropped module <%s>, loaded_vms = %d\n", current->comm, task_tgid_nr(current), - task_pid_nr(current), module->name, nloaded); + task_pid_nr(current), module->name, loaded_vms); } return err; } -static bool lvm_module_needs_purge(struct lua_lsm_module *module) +static bool lvm_should_drop_module(struct lua_lsm_module *module) { enum lua_lsm_module_state state = READ_ONCE(module->state); - return atomic_read(&module->nloaded) > 0 && + return atomic_read(&module->loaded_vm_count) > 0 && (state == LMS_STATE_GOING || state == LMS_STATE_ZOMBIE); } -static void lvm_purge_unloading(lua_State *L) +static void lvm_drop_unloading_modules(lua_State *L) { struct lua_lsm_module *module; int idx; @@ -569,8 +564,8 @@ static void lvm_purge_unloading(lua_State *L) idx = srcu_read_lock(&modules_ss); list_for_each_entry_srcu(module, &lsm_modules, list, srcu_read_lock_held(&modules_ss)) { - if (lvm_module_needs_purge(module)) - lvm_purge_module(L, module); + if (lvm_should_drop_module(module)) + lvm_drop_module(L, module); } srcu_read_unlock(&modules_ss, idx); } @@ -659,7 +654,7 @@ lua_State *lvm_get(void) if (in_task()) { L = lvm_get_from_task(current, false); if (L) - lvm_purge_unloading(L); + lvm_drop_unloading_modules(L); return L; } else { return get_cpu_var(irq_lvms)->L; @@ -882,7 +877,7 @@ static int lua_modules_index(lua_State *L) /* stack: [table, thunk, key, thunk] */ lua_rawset(L, 1); /* table[key] = thunk */ - atomic_inc(&module->nloaded); + atomic_inc(&module->loaded_vm_count); return 1; /* return the thunk */ } @@ -891,13 +886,11 @@ static int lua_modules_index(lua_State *L) } /* - * When LuaVM is destroyed, iterate over the modules loaded in the VM - * and update the load count in the module. + * Drop each policy module reference recorded in a Lua VM's _MODULES table. */ -static void lua_modules_free(struct task_struct *task, lua_State *L) +static void lvm_put_loaded_modules(struct task_struct *task, lua_State *L) { struct lua_lsm_module *module; - int nloaded; lua_getfield(L, LUA_REGISTRYINDEX, "_MODULES"); if (!lua_istable(L, -1)) { @@ -910,17 +903,15 @@ static void lua_modules_free(struct task_struct *task, lua_State *L) lua_pushstring(L, module->name); lua_rawget(L, -2); if (lua_istable(L, -1)) { - nloaded = atomic_dec_return(&module->nloaded); - WARN_ON_ONCE(nloaded < 0); - if (nloaded == 0 && READ_ONCE(module->state) == LMS_STATE_ZOMBIE) - lua_lsm_module_schedule_finalize(); + int loaded_vms = lua_lsm_module_drop_from_vm(module); + if (task) - __log_info("<%s>: %d-%d dropped module <%s>, nloaded = %d\n", + __log_info("<%s>: %d-%d dropped module <%s>, loaded_vms = %d\n", task->comm, task_tgid_nr(task), task_pid_nr(task), - module->name, nloaded); + module->name, loaded_vms); else - __log_info("dropped module <%s>, nloaded = %d\n", - module->name, nloaded); + __log_info("dropped module <%s>, loaded_vms = %d\n", + module->name, loaded_vms); } lua_pop(L, 1); } @@ -1248,7 +1239,7 @@ int lua_lsm_module_register(const char *code, size_t len) goto err_free_module; memcpy(module->chunk, chunk, chunk_len); module->chunk_len = chunk_len; - atomic_set(&module->nloaded, 0); + atomic_set(&module->loaded_vm_count, 0); INIT_LIST_HEAD(&module->shdicts); spin_lock_init(&module->shdict_lock); @@ -1309,7 +1300,7 @@ int lua_lsm_module_register(const char *code, size_t len) static struct lua_lsm_module *work_ctx_remove_module; static atomic_t work_ctx_remove_count; -static void softirq_lvm_remove_module(struct work_struct *work) +static void softirq_lvm_drop_module(struct work_struct *work) { struct lua_lsm_module *module = work_ctx_remove_module; int cpu = smp_processor_id(); @@ -1321,7 +1312,7 @@ static void softirq_lvm_remove_module(struct work_struct *work) * changing the Lua VM environment. */ local_bh_disable(); - err = lvm_purge_module(per_cpu(irq_lvms, cpu)->L, module); + err = lvm_drop_module(per_cpu(irq_lvms, cpu)->L, module); if (!err) { atomic_inc(&work_ctx_remove_count); __log_info("<%s>: err = [ OK ] \t, count = %d\n", @@ -1330,19 +1321,42 @@ static void softirq_lvm_remove_module(struct work_struct *work) local_bh_enable(); } -static bool lua_lsm_module_drained(struct lua_lsm_module *module) +static bool lua_lsm_module_ready_to_finalize(struct lua_lsm_module *module) { return READ_ONCE(module->state) == LMS_STATE_ZOMBIE && - atomic_read(&module->nloaded) == 0; + atomic_read(&module->loaded_vm_count) == 0; +} + +static int lua_lsm_module_drop_from_vm(struct lua_lsm_module *module) +{ + int loaded_vms = atomic_dec_return(&module->loaded_vm_count); + + WARN_ON_ONCE(loaded_vms < 0); + if (loaded_vms == 0 && lua_lsm_module_ready_to_finalize(module)) + schedule_work(&lua_lsm_module_finalize_work); + + return loaded_vms; } -static void lua_lsm_module_unlink_shdicts(struct lua_lsm_module *module) +static void lua_lsm_module_mark_shdicts_dead(struct lua_lsm_module *module) +{ + struct lua_lsm_module_shdict *shdict; + unsigned long flags; + + spin_lock_irqsave(&module->shdict_lock, flags); + list_for_each_entry(shdict, &module->shdicts, list) + WRITE_ONCE(shdict->dead, true); + spin_unlock_irqrestore(&module->shdict_lock, flags); +} + +static void lua_lsm_module_release_shdicts(struct lua_lsm_module *module) { struct lua_lsm_module_shdict *shdict, *tmp; unsigned long flags; spin_lock_irqsave(&module->shdict_lock, flags); list_for_each_entry_safe(shdict, tmp, &module->shdicts, list) { + WRITE_ONCE(shdict->dead, true); list_del_rcu(&shdict->list); atomic_dec(&module->shdict_count); lua_lsm_shdict_put(shdict); @@ -1350,7 +1364,7 @@ static void lua_lsm_module_unlink_shdicts(struct lua_lsm_module *module) spin_unlock_irqrestore(&module->shdict_lock, flags); } -static void lua_lsm_module_disable_hooks(struct lua_lsm_module *module) +static void lua_lsm_module_unregister_hooks(struct lua_lsm_module *module) { int i; @@ -1360,91 +1374,49 @@ static void lua_lsm_module_disable_hooks(struct lua_lsm_module *module) } } -static void lua_lsm_module_detach_locked(struct lua_lsm_module *module) +static void lua_lsm_module_free_srcu(struct rcu_head *rcu) { - WARN_ON_ONCE(atomic_read(&module->nloaded) != 0); - WARN_ON_ONCE(READ_ONCE(module->state) != LMS_STATE_GOING && - READ_ONCE(module->state) != LMS_STATE_ZOMBIE); - - lua_lsm_module_disable_hooks(module); - list_del_rcu(&module->list); - lua_lsm_module_unlink_shdicts(module); - atomic_dec(&modules_unloading); -} + struct lua_lsm_module *module; -static void lua_lsm_module_free_detached(struct lua_lsm_module *module) -{ - synchronize_srcu(&modules_ss); + module = container_of(rcu, struct lua_lsm_module, rcu); lua_lsm_module_free(module); } -static void lua_lsm_module_schedule_finalize(void) +/* + * Remove the module from all global lookup paths. The object itself stays + * alive until pre-existing modules_ss readers have left their critical section. + */ +static void lua_lsm_module_finalize_locked(struct lua_lsm_module *module) { - unsigned long flags; - bool queue = false; + lockdep_assert_held(&modules_mutex); - spin_lock_irqsave(&lua_lsm_module_finalize_lock, flags); - lua_lsm_module_finalize_pending = true; - if (!lua_lsm_module_finalize_running) { - lua_lsm_module_finalize_running = true; - queue = true; - } - spin_unlock_irqrestore(&lua_lsm_module_finalize_lock, flags); + WARN_ON_ONCE(atomic_read(&module->loaded_vm_count) != 0); + WARN_ON_ONCE(READ_ONCE(module->state) != LMS_STATE_GOING && + READ_ONCE(module->state) != LMS_STATE_ZOMBIE); - if (queue) - schedule_work(&lua_lsm_module_finalize_work); + lua_lsm_module_unregister_hooks(module); + list_del_rcu(&module->list); + lua_lsm_module_release_shdicts(module); + atomic_dec(&modules_unloading); + call_srcu(&modules_ss, &module->rcu, lua_lsm_module_free_srcu); } static void lua_lsm_module_finalize_workfn(struct work_struct *work) { - struct lua_lsm_module *module; - unsigned long flags; - bool found; - - for (;;) { - found = false; + struct lua_lsm_module *module, *tmp; - spin_lock_irqsave(&lua_lsm_module_finalize_lock, flags); - lua_lsm_module_finalize_pending = false; - spin_unlock_irqrestore(&lua_lsm_module_finalize_lock, flags); - - mutex_lock(&modules_mutex); - list_for_each_entry(module, &lsm_modules, list) { - if (lua_lsm_module_drained(module)) { - lua_lsm_module_detach_locked(module); - found = true; - break; - } - } - mutex_unlock(&modules_mutex); - - if (found) { - lua_lsm_module_free_detached(module); - continue; - } - - spin_lock_irqsave(&lua_lsm_module_finalize_lock, flags); - if (!lua_lsm_module_finalize_pending) { - lua_lsm_module_finalize_running = false; - spin_unlock_irqrestore(&lua_lsm_module_finalize_lock, flags); - return; - } - spin_unlock_irqrestore(&lua_lsm_module_finalize_lock, flags); + mutex_lock(&modules_mutex); + list_for_each_entry_safe(module, tmp, &lsm_modules, list) { + if (lua_lsm_module_ready_to_finalize(module)) + lua_lsm_module_finalize_locked(module); } -} - -static void lua_lsm_module_finish_unregister(struct lua_lsm_module *module) -{ - lua_lsm_module_detach_locked(module); - lua_lsm_module_free_detached(module); + mutex_unlock(&modules_mutex); } int lua_lsm_module_unregister(const char *name) { struct lua_lsm_module *module; - struct lua_lsm_module_shdict *shdict; - int count = 0, nloaded, remaining; - unsigned long flags; + int count = 0, loaded_vms, remaining; int found = 0; int err; @@ -1458,7 +1430,7 @@ int lua_lsm_module_unregister(const char *name) if (found && READ_ONCE(module->state) == LMS_STATE_LIVE) { /* * Keep the hook counts until final removal so tasks keep - * entering Lua-LSM and can purge their own VMs. + * entering Lua-LSM and can drop the module from their own VMs. */ WRITE_ONCE(module->state, LMS_STATE_GOING); atomic_inc(&modules_unloading); @@ -1469,7 +1441,7 @@ int lua_lsm_module_unregister(const char *name) return -ENOENT; } - if (lua_lsm_module_drained(module)) + if (lua_lsm_module_ready_to_finalize(module)) goto out_detach; pr_info("Prepare to unregister module <%s> ...\n", name); @@ -1477,38 +1449,35 @@ int lua_lsm_module_unregister(const char *name) synchronize_srcu(&modules_ss); /* - * Existing Lua userdata may outlive the VM purge attempt. Tombstone + * Existing Lua userdata may outlive the VM drop attempt. Tombstone * shared dicts now and drop the module list refs only once unregister * can complete. */ - spin_lock_irqsave(&module->shdict_lock, flags); - list_for_each_entry(shdict, &module->shdicts, list) - WRITE_ONCE(shdict->dead, true); - spin_unlock_irqrestore(&module->shdict_lock, flags); + lua_lsm_module_mark_shdicts_dead(module); kvcache_module_nodes_gc(module); - nloaded = atomic_read(&module->nloaded); - if (nloaded > 0) { + loaded_vms = atomic_read(&module->loaded_vm_count); + if (loaded_vms > 0) { lua_State *L = lvm_get_from_task(current, true); if (L) { - err = lvm_purge_module(L, module); + err = lvm_drop_module(L, module); lvm_put_to_task(current, L); if (!err) count++; } __log_info("Unregister module <%s> from current task, freed = %d/%d\n", - name, count, nloaded); + name, count, loaded_vms); } /* * At this time, LuaVM may still be released asynchronously, - * so the nloaded will be updated in the air. + * so loaded_vm_count may change concurrently. */ - nloaded = atomic_read(&module->nloaded); - if (nloaded > 0) { + loaded_vms = atomic_read(&module->loaded_vm_count); + if (loaded_vms > 0) { /* * Since global variables are used, locking ensures that only * one instance of the softirq LuaVM offload is executed. @@ -1516,34 +1485,34 @@ int lua_lsm_module_unregister(const char *name) work_ctx_remove_module = module; atomic_set(&work_ctx_remove_count, 0); - err = schedule_on_each_cpu(softirq_lvm_remove_module); + err = schedule_on_each_cpu(softirq_lvm_drop_module); WARN_ON(err); count += atomic_read(&work_ctx_remove_count); __log_info("Unregister module <%s> from pcpu, freed = %d/%d\n", - name, count, nloaded); + name, count, loaded_vms); } - remaining = atomic_read(&module->nloaded); + remaining = atomic_read(&module->loaded_vm_count); if (remaining == 0) goto out_detach; WRITE_ONCE(module->state, LMS_STATE_ZOMBIE); - if (lua_lsm_module_drained(module)) + if (lua_lsm_module_ready_to_finalize(module)) goto out_detach; - remaining = atomic_read(&module->nloaded); + remaining = atomic_read(&module->loaded_vm_count); err = -EBUSY; goto out_unlock; out_detach: remaining = 0; - lua_lsm_module_finish_unregister(module); + lua_lsm_module_finalize_locked(module); err = 0; out_unlock: mutex_unlock(&modules_mutex); - pr_info("Unregister module <%s>: purged %d Lua VMs, remaining = %d, vm_nusage = %d\n", + pr_info("Unregister module <%s>: dropped from %d Lua VMs, remaining = %d, vm_nusage = %d\n", name, count, remaining, atomic_read(&vm_nusage)); return err; @@ -1564,7 +1533,7 @@ int modules_show(struct seq_file *m, void *v) list_for_each_entry_srcu(module, &lsm_modules, list, srcu_read_lock_held(&modules_ss)) { seq_printf(m, "%-20s %-10s %6zu %4d %5d %6d %6d %-34s\n", module->name, module->license, module->chunk_len, - module->nhooks, atomic_read(&module->nloaded), + module->nhooks, atomic_read(&module->loaded_vm_count), atomic_read(&module->shdict_count), atomic_read(&module->kvnodes_count), module->author); } @@ -1596,8 +1565,11 @@ void task_blob_free(struct task_struct *task) /* Pairs with cmpxchg() in lvm_get_task_state(). */ lvm = smp_load_acquire(&llt->lvm); if (lvm && READ_ONCE(lvm->L) && lvm->dirty) { - lua_modules_free(task, lvm->L); + int idx = srcu_read_lock(&modules_ss); + + lvm_put_loaded_modules(task, lvm->L); lvm_vm_reset(lvm); + srcu_read_unlock(&modules_ss, idx); lvm->dirty = false; } diff --git a/security/lua/lsm.h b/security/lua/lsm.h index 3fa8ac91dc26..8d3595ec0d83 100644 --- a/security/lua/lsm.h +++ b/security/lua/lsm.h @@ -118,12 +118,14 @@ struct lua_lsm_module { int version; enum lua_lsm_module_state state; struct list_head list; + struct rcu_head rcu; __BITMAP_TYPE(, uint32_t, __LL_NR_MAX) hookfuncs; int nhooks; char *chunk; size_t chunk_len; - atomic_t nloaded; + /* Lua VMs that currently have this module in their _MODULES table. */ + atomic_t loaded_vm_count; struct list_head shdicts; /* Protects shdicts and shdict_count. */ From 96cfe3ee9fe0a25f8dc850ef586f9b0d144108fe Mon Sep 17 00:00:00 2001 From: Zongyao Chen Date: Wed, 22 Jul 2026 16:26:18 +0800 Subject: [PATCH 5/6] lua-lsm: report success and expose zombie state on unregister Unregister used to return -EBUSY whenever a deactivated module still had loaded Lua VMs, even though the module's hooks no longer run and the finalize worker reclaims it asynchronously once loaded_vm_count drains to zero. Reporting an error for an operation that has, from the caller's point of view, already succeeded (the policy is stopped) is misleading and hard to script against. Return 0 in that case. The module stays on the list as ZOMBIE until the background worker frees it. Expose the module state (live/coming/going/ zombie) as a new column in /sys/kernel/security/lua/modules so the pending cleanup is observable. Signed-off-by: Zongyao Chen --- security/lua/lsm.c | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/security/lua/lsm.c b/security/lua/lsm.c index 29ebd24d8c68..a26c0dcdaadb 100644 --- a/security/lua/lsm.c +++ b/security/lua/lsm.c @@ -1501,8 +1501,15 @@ int lua_lsm_module_unregister(const char *name) if (lua_lsm_module_ready_to_finalize(module)) goto out_detach; + /* + * The module is deactivated -- its hooks no longer run -- but some Lua + * VMs still hold a reference to it. Report success: the remaining VMs + * drop it lazily on their next Lua-LSM entry, and the finalize worker + * frees it once loaded_vm_count reaches zero. Until then the module + * stays on the list and is visible as ZOMBIE in /modules. + */ remaining = atomic_read(&module->loaded_vm_count); - err = -EBUSY; + err = 0; goto out_unlock; out_detach: @@ -1512,30 +1519,50 @@ int lua_lsm_module_unregister(const char *name) out_unlock: mutex_unlock(&modules_mutex); - pr_info("Unregister module <%s>: dropped from %d Lua VMs, remaining = %d, vm_nusage = %d\n", - name, count, remaining, atomic_read(&vm_nusage)); + pr_info("Unregister module <%s>: dropped from %d Lua VMs, remaining = %d%s, vm_nusage = %d\n", + name, count, remaining, + remaining ? " (zombie, background cleanup pending)" : "", + atomic_read(&vm_nusage)); return err; } +static const char *lua_lsm_module_state_name(enum lua_lsm_module_state state) +{ + switch (state) { + case LMS_STATE_LIVE: + return "live"; + case LMS_STATE_COMING: + return "coming"; + case LMS_STATE_GOING: + return "going"; + case LMS_STATE_ZOMBIE: + return "zombie"; + default: + return "?"; + } +} + int modules_show(struct seq_file *m, void *v) { struct lua_lsm_module *module; int idx; seq_printf(m, "modules for lua-lsm\n"); - seq_printf(m, "%-20s %-10s %6s %4s %5s %6s %6s %-34s\n", + seq_printf(m, "%-20s %-10s %6s %4s %5s %6s %6s %-7s %-34s\n", "name", "license", "size", "nlsm", - "nload", "shdict", "kvnode", "author"); + "nload", "shdict", "kvnode", "state", "author"); seq_printf(m, "%s\n", TABLINE); idx = srcu_read_lock(&modules_ss); list_for_each_entry_srcu(module, &lsm_modules, list, srcu_read_lock_held(&modules_ss)) { - seq_printf(m, "%-20s %-10s %6zu %4d %5d %6d %6d %-34s\n", + seq_printf(m, "%-20s %-10s %6zu %4d %5d %6d %6d %-7s %-34s\n", module->name, module->license, module->chunk_len, module->nhooks, atomic_read(&module->loaded_vm_count), atomic_read(&module->shdict_count), - atomic_read(&module->kvnodes_count), module->author); + atomic_read(&module->kvnodes_count), + lua_lsm_module_state_name(READ_ONCE(module->state)), + module->author); } srcu_read_unlock(&modules_ss, idx); return 0; From f90a7a14ee6391ad50108eedf4a4205dd7846b4d Mon Sep 17 00:00:00 2001 From: Zongyao Chen Date: Wed, 22 Jul 2026 16:31:30 +0800 Subject: [PATCH 6/6] lua-lsm: mark loaded_vms __maybe_unused to fix build When lua-lsm debug logging is disabled, __log_info() expands to an empty statement, so the loaded_vms locals in lvm_drop_module() and lvm_put_loaded_modules() are unused and trip -Werror=unused-variable. The lua_lsm_module_drop_from_vm() call has side effects (it decrements loaded_vm_count and may queue the finalize worker) and must stay, so annotate the locals with __maybe_unused. Signed-off-by: Zongyao Chen --- security/lua/lsm.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/security/lua/lsm.c b/security/lua/lsm.c index a26c0dcdaadb..dcdeb0b522d3 100644 --- a/security/lua/lsm.c +++ b/security/lua/lsm.c @@ -536,7 +536,7 @@ static int lvm_drop_module(lua_State *L, struct lua_lsm_module *module) err = lvm_forget_module(L, module); if (!err) { - int loaded_vms = lua_lsm_module_drop_from_vm(module); + int __maybe_unused loaded_vms = lua_lsm_module_drop_from_vm(module); __log_info("<%s>: %d-%d dropped module <%s>, loaded_vms = %d\n", current->comm, task_tgid_nr(current), @@ -903,7 +903,7 @@ static void lvm_put_loaded_modules(struct task_struct *task, lua_State *L) lua_pushstring(L, module->name); lua_rawget(L, -2); if (lua_istable(L, -1)) { - int loaded_vms = lua_lsm_module_drop_from_vm(module); + int __maybe_unused loaded_vms = lua_lsm_module_drop_from_vm(module); if (task) __log_info("<%s>: %d-%d dropped module <%s>, loaded_vms = %d\n",