You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Sol's tutorials depict an approach to writing script bindings that can lead to heap corruption if those bindings are invoked from a coroutine, even if the coroutine is created and operated by scripts. This can come as a nasty surprise to library users who neglect to read the documentation subpages about threads because they aren't using sol's coroutine API.
The technical problem as I understand it — take this with a grain of salt:
Heap corruption can occur whether the coroutine is created and operated by scripts or the host app, so long as the host app either stores sol::reference-based values somewhere in its own datamodel or (worse yet) passes native data types containing any type of sol::reference back to a script. Sol bindings written in the 'standard style' will tend to use the lua_State of the coroutine that invoked them, and create these references in a non-main registry. When the coroutine completes, that registry may be deleted or recycled, causing all of these references go out of scope. This invalidates those sol::references; what happens next is undefined behavior but typically corrupts a heap or freelist causing a crash with unpredictable timing deep in the runtime.
This is a major pitfall, responsible for a number of issues on the repo. I think it could be resolved by adding a prominent item to Sol's table of contents with a title like "how to write coroutine-safe code".
This could either lead to an updated version of the thread page or to a new page written as a guide. Ideally it should explain when and why types like main_reference and this_main_state are necessary without venturing into topics like CPU threading.
"Heap corruption" might be a little too far. What you create are dangling references (and using them is a use-after-free). In my opinion, the page should include:
Each coroutine has its own Lua state
References (e.g. tables or functions) are stored in the "current" Lua state.
References prefixed with main_ are stored in the main Lua state.
If a reference is used across Lua invocations (i.e. create reference → Lua executes → use reference), almost always, you'd want the main_ variant. This assumes you're running unknown code.
Examples of where you can accidentally create dangling references.
Examples of how to do it correctly.
Mention that creating usertypes should almost always use a main_state (because these might create references that should be visible everywhere)
I am a little confused now because I've come across some apocrypha that says coroutines do use the same registry as the main lua_State, but this seems to conflict with my firsthand experience using sol with luajit. Perhaps the coroutine's registry "inherits" from the main one..? I would write a guide myself if I had a stronger understanding of the specifics.
The main offender in my program was a C++ function exposed to Lua that returned a class containing a collection of Lua references. That class was never retained by C++ code, instead going into the possession of Lua's garbage collector. If the coroutine ended, the references (created from the coroutine state) would cause issues when the garbage collector finalized the C++ object. This caused some kind of double-free heisenbug which would variously corrupt either the registry's freelist or luajit's bulk allocator heap.
Here's a stripped-down version of that "coroutine-unsafe" code:
// An ever-changing list we expose to Lua from time to time.static std::vector<Bogo*> bogos = {...};
// A userdata type that represents a snapshot of the list. Pretend it has some useful methods.structLuaBogos {std::vector<sol::userdata> bogos;};
// A function Lua scripts can call to get the snapshot.
LuaBogos get_bogos(sol::this_state lua)
{
LuaBogos result;
for (auto &bogo : bogos) result.bogos.emplace_back(sol::make_userdata(lua, bogo));
return result;
}
I think I've run afoul of a second pitfall with sol_lua_push functions, which provide a lua_State* for accessing the current thread's stack. It's important to always grab the main thread if anything will be added to the registry (ie, if you'll create any sol::reference, sol::table, sol::object or sol::userdata so that C++ can 'own' values). In push customizations, sol::main_thread(L,L) can be used to map the lua_State pointer to the main one.
After a few rounds of having allocator issues and auditing my code for stuff like this, I think the way to go might be to scan each non-main thread's registry in developer builds. All of my program's data should go in the main registry, so I'll just issue a warning if anything else appears in there.
Using this_main_state and a few other measures throughout the codebase has insulated me from crashes but I'm still learning about other pitfalls.
The latest is that comparisons between references (eg, sol::table) from different threads may return false even when the tables in question are the same. In my case, a function that searches for a specified Lua class (eg, HumanAI) passed as a regular sol::table reference will be associated with the calling coroutine's thread. When I go through my game's list of Lua components (sol::table references associated with the main thread) the match fails.
Here it appears that the solution is to change the argument to use main references such as sol::main_table rather than table or stack_table. This converts the argument to a main-thread reference that can be compared to the game engine's references. This would also be necessary for any function that might cause the game engine to store a reference (object, table, userdata, etc) passed in from Lua.
Activity
Nerixyz commentedon Nov 22, 2025
"Heap corruption" might be a little too far. What you create are dangling references (and using them is a use-after-free). In my opinion, the page should include:
main_are stored in the main Lua state.main_variant. This assumes you're running unknown code.main_state(because these might create references that should be visible everywhere)EvanBalster commentedon Nov 24, 2025
I am a little confused now because I've come across some apocrypha that says coroutines do use the same registry as the main lua_State, but this seems to conflict with my firsthand experience using sol with luajit. Perhaps the coroutine's registry "inherits" from the main one..? I would write a guide myself if I had a stronger understanding of the specifics.
The main offender in my program was a C++ function exposed to Lua that returned a class containing a collection of Lua references. That class was never retained by C++ code, instead going into the possession of Lua's garbage collector. If the coroutine ended, the references (created from the coroutine state) would cause issues when the garbage collector finalized the C++ object. This caused some kind of double-free heisenbug which would variously corrupt either the registry's freelist or luajit's bulk allocator heap.
Here's a stripped-down version of that "coroutine-unsafe" code:
EvanBalster commentedon Feb 23, 2026
I think I've run afoul of a second pitfall with
sol_lua_pushfunctions, which provide alua_State*for accessing the current thread's stack. It's important to always grab the main thread if anything will be added to the registry (ie, if you'll create anysol::reference,sol::table,sol::objectorsol::userdataso that C++ can 'own' values). In push customizations,sol::main_thread(L,L)can be used to map the lua_State pointer to the main one.After a few rounds of having allocator issues and auditing my code for stuff like this, I think the way to go might be to scan each non-main thread's registry in developer builds. All of my program's data should go in the main registry, so I'll just issue a warning if anything else appears in there.
EvanBalster commentedon Jun 13, 2026
Using
this_main_stateand a few other measures throughout the codebase has insulated me from crashes but I'm still learning about other pitfalls.The latest is that comparisons between references (eg,
sol::table) from different threads may return false even when the tables in question are the same. In my case, a function that searches for a specified Lua class (eg,HumanAI) passed as a regularsol::tablereference will be associated with the calling coroutine's thread. When I go through my game's list of Lua components (sol::tablereferences associated with the main thread) the match fails.Here it appears that the solution is to change the argument to use
mainreferences such assol::main_tablerather thantableorstack_table. This converts the argument to a main-thread reference that can be compared to the game engine's references. This would also be necessary for any function that might cause the game engine to store a reference (object, table, userdata, etc) passed in from Lua.