Redis 8.2.2: Hardening the Lua Engine Against Four Critical Vulnerabilities
Introduction
Redis is an open-source, in-memory data store widely used as a cache, message broker, and high-performance NoSQL database. It offers rich data structures like strings, hashes, lists, sets, sorted sets, bitmaps, HyperLogLogs, and streams, backed by atomic operations and very low latency. Persistence is available via RDB snapshots and AOF, and high availability is delivered through replication, Sentinel, and Cluster. Redis also supports server-side scripting with Lua to execute complex operations atomically. Its speed, flexibility, and mature ecosystem have made it a core building block for modern, latency-sensitive systems.
In October 2025, Redis shipped version 8.2.2, a security release that fixes four vulnerabilities in the embedded Lua engine. The most critical is a Lua use-after-free that can let attackers escape the scripting sandbox and execute code on the host when untrusted users can run arbitrary Lua. The release also addresses an integer overflow in unpack, a cross-user script execution flaw, and an out-of-bounds read in the Lua lexer. In this post, we break down each CVE, walk through the patches, and outline practical steps to harden your Redis deployments.
Vulnerabilities Overview
Before diving into code, here’s the quick map of what 8.2.2 fixes in the Lua engine.
CVE-2025-46817: Integer overflow in unpack (CVSS 9.8, Critical)
Extreme arguments to unpack(tbl, i, j) can overflow the internal count of values to return. That broken count can bypass stack checks and lead to crashes or memory corruption, making it a useful primitive for exploitation.
CVE-2025-46818: Cross-user script execution via shared runtime state (CVSS 7.3, High)
Loose controls around metatables and environment-related APIs allowed one script to influence how others run. In some setups, this meant a less-privileged user could affect execution in another user’s context, weakening isolation.
CVE-2025-46819: Lua out-of-bounds read in long-string parsing (CVSS 7.1, High)
Brittle handling of long-string/long-comment delimiters ([=[ … ]=], etc.) in the Lua lexer could push it past the end of its buffer. That can crash Redis and, in edge cases, expose nearby memory.
CVE-2025-49844: Lua use-after-free in parser (CVSS 10.0, Critical)
A crafted Lua script can manipulate garbage collection and parsing in a way that hits a use-after-free in the embedded Lua engine.
Next, we’ll look at each of these in more detail, with the relevant patch snippets
Inside the fixes: Vulnerability deep dives
CVE-2025-46817: Integer overflow in unpack
This issue affects Lua 5.1’s unpack(tbl, i, j) as embedded in Redis. With extreme i/j values, the calculation of “how many results to return” could overflow, produce a bogus count, and slip past stack checks. That can lead to a crash or memory corruption and is usable as a building block toward RCE in hostile scenarios.
The patch for this CVE can be referenced here: fc9abc7
Patch analysis
The fix makes unpack’s range handling explicit and safe.
- Count calculation switched to safe unsigned math
Before patch:
static int luaB_unpack (lua_State *L) {
int i, e, n;
luaL_checktype(L, 1, LUA_TTABLE);
i = luaL_optint(L, 2, 1);
e = luaL_opt(L, luaL_checkint, 3, luaL_getn(L, 1));
if (i > e) return 0; /* empty range */
n = e – i + 1; /* may overflow (signed) */
if (n <= 0 || !lua_checkstack(L, n))
return luaL_error(L, “too many results to unpack”);
/* push n results… */
}
Here, e – i + 1 is done in signed int. With extreme values (e.g. 0 and 2147483647), this can overflow, and n <= 0 is not a reliable guard.
After patch:
static int luaB_unpack (lua_State *L) {
int i, e;
unsigned int n;
luaL_checktype(L, 1, LUA_TTABLE);
i = luaL_optint(L, 2, 1);
e = luaL_opt(L, luaL_checkint, 3, luaL_getn(L, 1));
if (i > e) return 0; /* empty range */
n = (unsigned int)e – (unsigned int)i; /* elements minus 1 */
if (n >= INT_MAX || !lua_checkstack(L, ++n))
return luaL_error(L, “too many results to unpack”);
lua_rawgeti(L, 1, i); /* push arg[i] */
while (i++ < e) {
lua_rawgeti(L, 1, i);
}
return n;
}
Key points:
- Uses unsigned int for the span to avoid negative/overflowed values.
- Treats n as “count – 1” and increments once before the stack check.
- Enforces a hard upper bound (n >= INT_MAX → error).
- Ensures the stack has room for all results before pushing anything.
- Array indexing made explicit
Related tightening in the table code:
After patch
if (1 <= key && key <= t->sizearray)
return &t->array[key-1];
Instead of relying on an unsigned cast trick, it uses a clear bounds check, which avoids odd behaviour for huge or negative indexes.
Together, these changes mean extreme unpack ranges no longer cause wrapped counts or unsafe pushes. Oversized calls now fail with a clear “too many results to unpack” error instead of risking a crash or memory corruption.
CVE-2025-46819: Lua out-of-bounds read in long-string parsing
This issue sits in Lua’s lexer, specifically in how it parses long strings and long comments like [=[ … ]=]. In vulnerable versions, certain malformed or extreme delimiter patterns could push the lexer past the end of its input buffer. That can crash Redis (DoS), and in some edge cases may expose bytes from nearby memory.
The patch for this CVE can be referenced here: 3a1624d
Patch analysis
The fix makes long-string handling more strict and less fragile.
Delimiter parsing is made unambiguous
The helper that parses sequences like [===[ and ]===] is updated to use a safer type and return clear, consistent values:
After patch
static size_t skip_sep(LexState *ls) {
size_t count = 0;
int s = ls->current; /* ‘[‘ or ‘]’ */
lua_assert(s == ‘[‘ || s == ‘]’);
save_and_next(ls); /* consume it */
while (ls->current == ‘=’) { /* count ‘=’ */
save_and_next(ls);
count++;
}
if (ls->current == s) /* matched second ‘[‘ or ‘]’ */
return count + 2; /* valid long delimiter */
else if (count == 0)
return 1; /* not a long string/comment */
else
return 0; /* malformed */
}
- Valid long-string delimiters now return a well-defined length (>= 2).
- Plain [ falls back cleanly (1).
- Malformed [==… patterns return 0 and are treated as errors.
This closes the gap where odd or incomplete patterns could confuse the lexer into reading beyond the buffer.
Offsets for slicing the body are corrected
When extracting the actual string content, the patch aligns the slice math with the skip_sep result:
After patch:
if (seminfo) {
seminfo->ts = luaX_newstring(
ls,
luaZ_buffer(ls->buff) + sep,
luaZ_bufflen(ls->buff) – 2 * sep
);
}
Previously, the offsets didn’t properly match the delimiter semantics, which could lead to subtle off-by-N behaviour. Now the start and length are derived directly from sep, so the lexer stays within bounds.
Types match what the code really does
Counters and indexes involved in this logic now use appropriate unsigned/size types instead of overloaded signed ints, reducing the risk of negative values or wraparound creeping into pointer arithmetic.
All of this means that even with huge or intentionally broken [=…=[ … ]=…=] sequences, Lua now either parses them correctly or rejects them with a normal error. There’s no longer a path where a hostile script can nudge the lexer into looking past the end of the buffer.
CVE-2025-46818: Lua script can run in another user’s context
This issue is about keeping Lua scripts scoped to the user and environment they belong to. In vulnerable versions, shared runtime elements—like metatables on core types and legacy environment APIs—were loose enough that one script could influence how others ran. In some deployments, that could blur isolation between users.
The patch for this CVE can be referenced here: 45eac02
Patch analysis
The fix does two main things: protects core metatables and gates risky legacy APIs.
- Core type metatables are protected
Before, core Lua types exposed to scripts (strings, numbers, booleans, etc.) did not have a strict barrier preventing metatable changes from user scripts. That meant a modification to a shared metatable could affect other code running in the same engine.
The patched code explicitly hardens those metatables during Lua environment setup. In simplified form:
Patched code: mark primitive metatables as protected
static void luaProtectPrimitiveMetatables(lua_State *L) {
const int types[] = {LUA_TSTRING, LUA_TNUMBER, LUA_TBOOLEAN, LUA_TNIL};
for (size_t i = 0; i < sizeof(types)/sizeof(types[0]); i++) {
luaL_getmetatable(L, lua_typename(L, types[i]));
if (!lua_isnil(L, -1)) {
lua_pushliteral(L, “protected”);
lua_setfield(L, -2, “__metatable”); /* lock metatable */
}
lua_pop(L, 1);
}
}
This __metatable protection means user scripts can still see these types but can’t replace or rewrite their metatables globally. Any attempt to do so fails cleanly instead of silently changing behaviour for everyone else.
- Legacy environment APIs are explicitly opt-in
Older Lua APIs that can manipulate environments are now disabled by default and only exposed when an operator explicitly enables them.
Conceptually, the change looks like this:
Before patch: deprecated APIs always available in the script env
static const luaL_Reg redis_compat_funcs[] = {
{“getfenv”, luaB_getfenv},
{“setfenv”, luaB_setfenv},
{“newproxy”, luaB_newproxy},
{NULL, NULL}
};
scriptCreateEnv(…) {
luaL_register(L, “_G”, redis_compat_funcs);
}
After patch: only register when lua-enable-deprecated-api is set
scriptCreateEnv(…) {
if (server.lua_enable_deprecated_api) {
luaL_register(L, “_G”, redis_compat_funcs);
}
}
If lua-enable-deprecated-api is not enabled, these functions simply aren’t present in the script environment, removing a powerful set of levers for cross-context manipulation.
Put together, these changes ensure:
- Scripts can’t silently patch fundamental behaviour for all other scripts by rewriting core metatables.
- Environment-manipulation primitives are only available if an operator deliberately turns them on.
That makes the Lua runtime more predictable and keeps each script much closer to its intended privilege and context—important for any shared or multi-tenant Redis deployment.
CVE-2025-49844: Lua use-after-free in parser
This bug lives in how Redis integrates Lua’s parser. Under specific conditions, a crafted Lua script can trigger garbage collection at just the wrong time and make Redis read through a pointer to memory that has already been freed.
The patch for this CVE can be referenced here: d5728cb5795
Patch analysis
The key change is that Redis now keeps a proper reference to the Lua objects the parser is using (like the chunk name) on the Lua stack while parsing, so the garbage collector sees them as alive and can’t free or move them early.

Previously, the chunk “name” was created inline and passed directly into the lexer:
luaX_setinput(L, &lexstate, z, luaS_new(L, name));
Because that value wasn’t clearly anchored (for example, on the Lua stack), an unlucky interleaving of GC or defrag plus aggressive scripting could, in theory, leave the parser holding a pointer to something that had been moved or freed.
The patched code makes that ownership explicit:
TString *tname = luaS_new(L, name);
setsvalue2s(L, L->top, tname); /* keep the name alive on the stack */
incr_top(L);
luaX_setinput(L, &lexstate, z, tname); /* safely use it for lexing/parsing */
/* … parse the chunk … */
–L->top; /* drop the reference when done */
By keeping this state referenced for the entire parse, GC and defrag now treat it as live and cannot reclaim or relocate it mid-flight. That effectively removes the narrow timing window that made the use-after-free possible.
Deployment scenarios at risk
These issues are particularly relevant in environments where:
- Applications use Lua scripting in Redis.
- The same Redis cluster is shared across teams or tenants.
- Redis can be reached, directly or via your application – from untrusted or user-controlled input.
Recommended actions
1. Patch first
- Upgrade to patched Redis 8.2.2 build.
- Treat this as high priority anywhere untrusted or semi-trusted code can run Lua (for example via EVAL / SCRIPT LOAD) or where Redis clusters are shared across teams/tenants.
2. Lock down Lua scripting
- Use ACLs to restrict Lua scripting to trusted services only.
- Keep Redis on private networks, no direct internet exposure.
- Leave lua-enable-deprecated-api disabled unless you have a clear, audited need.
3. Segment sensitive workloads
- Run untrusted or customer-driven Lua on separate Redis instances.
- Don’t mix tenant-controlled scripts with clusters that hold critical or sensitive data.
4. Use security controls to add visibility (not as a substitute for patching)
- Configure IPS/IDS/WAF to detect and block direct Redis access and suspicious Redis protocol use.
- Use EDR/AV/runtime protection on Redis hosts to spot exploit-like behaviour
References:
- https://github.com/redis/redis/commits/8.2.2/
- https://nvd.nist.gov/vuln/detail/CVE-2025-46817
- https://nvd.nist.gov/vuln/detail/CVE-2025-46818
- https://nvd.nist.gov/vuln/detail/CVE-2025-46819
- https://nvd.nist.gov/vuln/detail/CVE-2025-49844
Authors:
Vinay Kumar
Adrip
Nandini
Suvarnjeet
The post Redis 8.2.2: Hardening the Lua Engine Against Four Critical Vulnerabilities appeared first on Blogs on Information Technology, Network & Cybersecurity | Seqrite.
