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
{{ message }}
Repository navigation
Lua m.getvar()/m.getvars(): long transformation tables crash, and "none" doesn't reset in v3 #3737
@airween, here's another one from the #3730/#3731 code review. It's in the transformation argument of Lua m.getvar()/m.getvars(), i.e. Lua::applyTransformations() in v3 and resolve_tfns() in v2.
1. A long transformation table crashes the process (v3, confirmed)
The table loop calls lua_rawgeti(L, idx, i) once per entry and never pops the value (src/engine/lua.cc:425 on v3/master 9d45c6d). A C function only has LUA_MINSTACK (20) free stack slots unless it calls lua_checkstack(), so a long enough table writes past the Lua stack.
With SecRuleScript in phase 1 (macOS, Lua 5.5.1), regression_tests exits with a segmentation fault (exit 139) at 100 entries and above. 25 and 50 entries still work. The size only comes from the script, not from the request, but a script that builds the list dynamically, or just has a long one, takes the worker down.
v2's resolve_tfns() (apache2/msc_lua.c, around line 177) has the same loop without a lua_pop(). I haven't reproduced a crash there.
Fix:lua_pop(L, 1) after reading each name (on the continue path too), or copy the name and pop before using it.
2. "none" doesn't start over in v3 (behaviour differs from v2)
Both engines have the comment /* A "none" means start over */. v2 does that: it empties the list of transformations collected so far (tfn_arr->nelts = 0). v3 applies each transformation to the value as soon as it reads it, so its continue can't undo the ones already applied:
v3 returns abc. Following v2 and the comment, it should be ABC. The fix would be to collect the transformation names first, resetting on "none" as v2 does, and only then apply them to a copy of the value.
3. luaL_checkstring() on a table entry can skip C++ destructors (v3)
If an entry isn't a string (e.g. {"lowercase", {}}), luaL_checkstring(L, -1) raises a Lua error with longjmp while getvar()'s std::string var and the loop's temporaries are alive. Their destructors don't run, so the value buffer leaks on every such call (and it's undefined behaviour if Lua is built as C++). This is the same pattern #3732 fixes in setvar(). Collecting the names first, as in point 2, before any std::string exists, would fix it too.
Points 1 and 2 need a regression case each. The 100-entry script above and the {"lowercase", "none"} one would do.
Thanks @fzipi, I've checked all three against the code, and they're right:
Lua::applyTransformations() in v3 and resolve_tfns() in v2 push every table entry with lua_rawgeti() and never pop it, and neither calls lua_checkstack(), so a table longer than LUA_MINSTACK writes past the stack.
v3 applies each transformation as soon as it reads the name, so "none" can't undo the ones already applied; v2 resets the list with tfn_arr->nelts = 0.
In v3, luaL_checkstring() on a table entry can longjmp while getvar()'s std::string var is alive.
I found a fourth one while reading it:
v3 m.getvars() ignores the transformation argument completely.Lua::getvars() never calls applyTransformations(); the only call is in getvar(). v2 calls resolve_tfns() in both l_getvar() and l_getvars(). So m.getvars("ARGS", { "lowercase", "htmlEntityDecode" }), which is the example in the SecRuleScript documentation, returns the raw values in v3. A script that looks for <script that way finds it in v2 but misses <SCRIPT in v3.
I'll fix these:
v3: collect the transformation names first, popping each table entry and resetting on "none" as v2 does, before any C++ object with a destructor exists, then apply them to the value; and apply them in getvars() too, to each value. Regression cases for a 100-entry table, {"lowercase", "none"}, and m.getvars() with a transformation.
v2: pop each entry in resolve_tfns(), with a regression case for the long table.
Once the v3 fix is in, I'll also add a note to the documentation about m.getvars() in earlier versions.
Activity
airween commentedon Nov 2, 2026
Thanks @fzipi, I've checked all three against the code, and they're right:
Lua::applyTransformations()in v3 andresolve_tfns()in v2 push every table entry withlua_rawgeti()and never pop it, and neither callslua_checkstack(), so a table longer thanLUA_MINSTACKwrites past the stack."none"can't undo the ones already applied; v2 resets the list withtfn_arr->nelts = 0.luaL_checkstring()on a table entry canlongjmpwhilegetvar()'sstd::string varis alive.I found a fourth one while reading it:
m.getvars()ignores the transformation argument completely.Lua::getvars()never callsapplyTransformations(); the only call is ingetvar(). v2 callsresolve_tfns()in bothl_getvar()andl_getvars(). Som.getvars("ARGS", { "lowercase", "htmlEntityDecode" }), which is the example in the SecRuleScript documentation, returns the raw values in v3. A script that looks for<scriptthat way finds it in v2 but misses<SCRIPTin v3.I'll fix these:
"none"as v2 does, before any C++ object with a destructor exists, then apply them to the value; and apply them ingetvars()too, to each value. Regression cases for a 100-entry table,{"lowercase", "none"}, andm.getvars()with a transformation.resolve_tfns(), with a regression case for the long table.Once the v3 fix is in, I'll also add a note to the documentation about
m.getvars()in earlier versions.fix: skip a Lua transformation table entry that isn't a string (v2)
fix: log the right type for a bad Lua transformation parameter (v2)