Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
77ea719
Reject untrusted Lua bytecode
Sep 15, 2026
91d6652
Clarify Lua bytecode regression tests
Sep 15, 2026
74078ba
Clarify Lua compilation boundary test
Sep 15, 2026
2508af6
Use neutral Lua input terminology
Sep 15, 2026
fde3ed9
Simplify Lua bytecode regression setup
Sep 15, 2026
adfe785
Assert rejected Lua bytecode has no side effect
Sep 15, 2026
ae90c7f
Centralize persistent script hash allocation
Sep 15, 2026
57403d8
Remove internal script cache test
Sep 15, 2026
5e15493
Keep script hash handling unchanged
Sep 15, 2026
382b171
Merge branch 'main' into tiagonapoli/fix-lua-bytecode
tiagonapoli Sep 16, 2026
baefea5
Make Lua source handle creation explicit
Sep 16, 2026
9120748
Simplify Lua script cache branches
Sep 16, 2026
8e5b33f
Restore LuaRunner constructor formatting
Sep 16, 2026
c28562a
Finish Lua formatting cleanup
Sep 16, 2026
4e7c1d2
Clarify Lua source factory name
Sep 16, 2026
49cbfbc
Remove redundant Lua loader comments
Sep 16, 2026
1291fa8
Clarify Lua script cache control flow
Sep 16, 2026
15efd15
Separate Lua source and cache loading
Sep 16, 2026
fa64aaf
Centralize Lua script loading
Sep 16, 2026
9bde2d9
Simplify LuaRunner chunk construction
Sep 16, 2026
0d184d1
Clarify Lua load failure handling
Sep 16, 2026
27ccbd5
Format Lua load failure comment
Sep 16, 2026
a3dd5aa
Clarify Lua runner cache naming
Sep 16, 2026
e56277f
Deduplicate Lua out-of-memory responses
Sep 16, 2026
70650ee
Verify Lua parser error extraction
Sep 16, 2026
3769546
Clarify Lua stack error helper
Sep 16, 2026
514e05a
Require generated bytecode in script cache
Sep 16, 2026
4f8f299
Restore Lua cache exception handling
Sep 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions benchmark/BDN.benchmark/Lua/LuaScriptCacheOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,28 @@ public void GlobalSetup()

outerHitDigest = GC.AllocateUninitializedArray<byte>(SessionScriptCache.SHA1Len, pinned: true);
sessionScriptCache.GetScriptDigest("return 1"u8, outerHitDigest);
if (!storeWrapper.storeScriptCache.TryAdd(new(outerHitDigest), new("return 1"u8.ToArray())))
if (!storeWrapper.storeScriptCache.TryAdd(new(outerHitDigest), CompileScript("return 1"u8)))
{
throw new InvalidOperationException("Should have been able to load into global cache");
}

innerHitDigest = GC.AllocateUninitializedArray<byte>(SessionScriptCache.SHA1Len, pinned: true);
sessionScriptCache.GetScriptDigest("return 1 + 1"u8, innerHitDigest);
if (!storeWrapper.storeScriptCache.TryAdd(new(innerHitDigest), new("return 1 + 1"u8.ToArray())))
if (!storeWrapper.storeScriptCache.TryAdd(new(innerHitDigest), CompileScript("return 1 + 1"u8)))
{
throw new InvalidOperationException("Should have been able to load into global cache");
}

missDigest = GC.AllocateUninitializedArray<byte>(SessionScriptCache.SHA1Len, pinned: true);
sessionScriptCache.GetScriptDigest("foobar"u8, missDigest);

static LuaScriptHandle CompileScript(ReadOnlySpan<byte> source)
{
if (!LuaRunner.TryCompileSource(source, out var generatedBytecode, out var error))
throw new InvalidOperationException(error);

return new(generatedBytecode.Data);
}
}

[GlobalCleanup]
Expand All @@ -81,8 +89,7 @@ public void IterationSetup()
sessionScriptCache.Clear();

// Make outer hit available for every iteration
LuaScriptHandle scriptHandle = null;
if (!sessionScriptCache.TryLoad(session, "return 1"u8, new(outerHitDigest), ref scriptHandle, out _, out _))
if (!sessionScriptCache.TryGetOrCreateRunnerFromSource(session, "return 1"u8, new(outerHitDigest), out _, out _, out _))
{
throw new InvalidOperationException("Should have been able to load");
}
Expand Down Expand Up @@ -147,10 +154,9 @@ private void LoadScript(Span<byte> digest)
{
if (storeWrapper.storeScriptCache.TryGetValue(digestKey, out var scriptHandle))
{
LuaScriptHandle newScriptHandle = null;
if (!sessionScriptCache.TryLoad(session, scriptHandle.ScriptData.Span, digestKey, ref newScriptHandle, out runner, out _))
if (!sessionScriptCache.TryGetOrCreateRunnerFromCachedScript(session, digestKey, scriptHandle, out runner))
{
// TryLoad will have written an error out, it any
// The loading error was already written, if any

_ = storeWrapper.storeScriptCache.TryRemove(digestKey, out _);
}
Expand Down
107 changes: 37 additions & 70 deletions libs/server/Lua/LuaCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ private unsafe bool TryEVALSHA()
{
if (storeWrapper.storeScriptCache.TryGetValue(scriptKey, out var globalScriptHandle))
{
if (!sessionScriptCache.TryLoad(this, globalScriptHandle.ScriptData.Span, scriptKey, ref globalScriptHandle, out runner, out _))
if (!sessionScriptCache.TryGetOrCreateRunnerFromCachedScript(this, scriptKey, globalScriptHandle, out runner))
{
// TryLoad will have written an error out, it any
// The loading error was already written, if any
//
// Note we DON'T dispose the script handle because this is just the session cache
_ = storeWrapper.storeScriptCache.TryRemove(scriptKey, out _);
Expand Down Expand Up @@ -114,43 +114,8 @@ private unsafe bool TryEVAL()
sessionScriptCache.GetScriptDigest(script.ReadOnlySpan, digest);

var onStackScriptKey = new ScriptHashKey(digest);
_ = storeWrapper.storeScriptCache.TryGetValue(onStackScriptKey, out var globalScriptHandle);

var sessionScriptHandle = globalScriptHandle;

if (!sessionScriptCache.TryLoad(this, script.ReadOnlySpan, onStackScriptKey, ref sessionScriptHandle, out var runner, out var digestOnHeap))
{
// TryLoad will have written any errors out
return true;
}
else if (sessionScriptHandle != globalScriptHandle)
{
// Add script to the store dictionary IF we didn't already have it cached
//
// This may strike you as odd, but it is how Redis behaves
if (digestOnHeap == null)
{
var newAlloc = GC.AllocateUninitializedArray<byte>(SessionScriptCache.SHA1Len, pinned: true);
digest.CopyTo(newAlloc);
if (!storeWrapper.storeScriptCache.TryAdd(new(newAlloc), sessionScriptHandle))
{
// Some other session loaded the script, toss our new handle
//
// Next time this script is run, it'll be pulled from the global cache
sessionScriptHandle.Dispose();
}
}
else
{
if (!storeWrapper.storeScriptCache.TryAdd(digestOnHeap.Value, sessionScriptHandle))
{
// Some other session loaded the script, toss our new handle
//
// Next time this script is run, it'll be pulled from the global cache
sessionScriptHandle.Dispose();
}
}
}
if (!TryGetOrCreateScriptRunner(script.ReadOnlySpan, digest, onStackScriptKey, out var runner))
return true; // The loading error was written to the response.

if (runner == null)
{
Expand Down Expand Up @@ -275,45 +240,41 @@ private bool NetworkScriptLoad()
sessionScriptCache.GetScriptDigest(source.Span, digest);

var onStackScriptHashKey = new ScriptHashKey(digest);
_ = storeWrapper.storeScriptCache.TryGetValue(onStackScriptHashKey, out var globalScriptHandle);

var sessionScriptHandle = globalScriptHandle;
if (sessionScriptCache.TryLoad(this, source.ReadOnlySpan, onStackScriptHashKey, ref sessionScriptHandle, out _, out var digestOnHeap))
if (TryGetOrCreateScriptRunner(source.ReadOnlySpan, digest, onStackScriptHashKey, out _))
{
// TryLoad will write any errors out

// Add script to the global store dictionary if not already in there
if (globalScriptHandle != sessionScriptHandle)
{
if (digestOnHeap == null)
{
var newAlloc = GC.AllocateUninitializedArray<byte>(SessionScriptCache.SHA1Len, pinned: true);
digest.CopyTo(newAlloc);
if (!storeWrapper.storeScriptCache.TryAdd(new(newAlloc), sessionScriptHandle))
{
// Some other caller added the script already, our new handle is dead
// but we'll load it from the shared cache on next invocation
sessionScriptHandle.Dispose();
}
}
else
{
if (!storeWrapper.storeScriptCache.TryAdd(digestOnHeap.Value, sessionScriptHandle))
{
// Some other caller added the script already, our new handle is dead
// but we'll load it from the shared cache on next invocation
sessionScriptHandle.Dispose();
}
}
}

while (!RespWriteUtils.TryWriteBulkString(digest, ref dcurr, dend))
SendAndReset();
}

return true;
}

private bool TryGetOrCreateScriptRunner(ReadOnlySpan<byte> source, ReadOnlySpan<byte> digest, ScriptHashKey scriptKey, out LuaRunner runner)
{
if (storeWrapper.storeScriptCache.TryGetValue(scriptKey, out var globalScriptHandle))
return sessionScriptCache.TryGetOrCreateRunnerFromCachedScript(this, scriptKey, globalScriptHandle, out runner);

if (!sessionScriptCache.TryGetOrCreateRunnerFromSource(this, source, scriptKey, out var sessionScriptHandle, out runner, out var digestOnHeap))
return false;

ScriptHashKey globalScriptKey;
if (digestOnHeap != null)
{
globalScriptKey = digestOnHeap.Value;
}
else
{
var digestCopy = GC.AllocateUninitializedArray<byte>(SessionScriptCache.SHA1Len, pinned: true);
digest.CopyTo(digestCopy);
globalScriptKey = new(digestCopy);
}

if (!storeWrapper.storeScriptCache.TryAdd(globalScriptKey, sessionScriptHandle))
sessionScriptHandle.Dispose();

return true;
}

/// <summary>
/// Returns true if Lua is enabled.
///
Expand All @@ -332,6 +293,12 @@ private bool CheckLuaEnabled()
return true;
}

internal void WriteLuaCompilationError(string error)
{
while (!RespWriteUtils.TryWriteError($"Compilation error: {error}", ref dcurr, dend))
SendAndReset();
}

/// <summary>
/// Run a resolved script for the current session.
///
Expand Down
28 changes: 18 additions & 10 deletions libs/server/Lua/LuaRunner.Functions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,7 @@ internal int LoadString(nint luaStatePtr)
return LuaWrappedError(1, constStrs.InsufficientLuaStackSpace);
}

var res = state.LoadString(buff);
var res = state.LoadTextBuffer(buff);
if (res != LuaStatus.OK)
{
state.ClearStack();
Expand Down Expand Up @@ -3071,8 +3071,8 @@ int luaArgCount
private unsafe int CompileCommon<TResponse>(nint luaState, ref TResponse resp)
where TResponse : struct, IResponseAdapter
{
// 1 for function, 1 for code string
const int NeededStackSpace = 2;
// 1 for function, 1 for code string, 1 for load mode
const int NeededStackSpace = 3;

Debug.Assert(functionRegistryIndex == -1, "Shouldn't compile multiple times");

Expand All @@ -3081,15 +3081,19 @@ private unsafe int CompileCommon<TResponse>(nint luaState, ref TResponse resp)
Debug.Assert(state.TryEnsureMinimumStackCapacity(NeededStackSpace), "LUA_MIN_STACK should be high enough that this cannot happen");

_ = state.RawGetInteger(LuaType.Function, (int)LuaRegistry.Index, loadSandboxedRegistryIndex);
if (!state.TryPushBuffer(source.Span))
if (!state.TryPushBuffer(source.Data.Span))
{
while (!RespWriteUtils.TryWriteError(CmdStrings.LUA_out_of_memory, ref resp.BufferCur, resp.BufferEnd))
resp.SendAndReset();
WriteOutOfMemoryError(ref resp);
return 0;
}

if (!state.TryPushBuffer(source.Kind == LuaScriptChunkKind.GarnetGeneratedBinary ? "b"u8 : "t"u8))
{
WriteOutOfMemoryError(ref resp);
return 0;
}

var callRes = state.PCall(1, 2);
var callRes = state.PCall(2, 2);

// On success the stack will have two things on it:
// 1. The error (nil if not error)
Expand All @@ -3104,9 +3108,7 @@ private unsafe int CompileCommon<TResponse>(nint luaState, ref TResponse resp)
if (!state.TryRef(out functionRegistryIndex))
{
// Uh-oh, couldn't save the function under the registry
while (!RespWriteUtils.TryWriteError(CmdStrings.LUA_out_of_memory, ref resp.BufferCur, resp.BufferEnd))
resp.SendAndReset();

WriteOutOfMemoryError(ref resp);
return 0;
}
}
Expand All @@ -3131,6 +3133,12 @@ private unsafe int CompileCommon<TResponse>(nint luaState, ref TResponse resp)
return 0;
}

private static unsafe void WriteOutOfMemoryError<TResponse>(ref TResponse resp) where TResponse : struct, IResponseAdapter
{
while (!RespWriteUtils.TryWriteError(CmdStrings.LUA_out_of_memory, ref resp.BufferCur, resp.BufferEnd))
resp.SendAndReset();
}

/// <summary>
/// Entry point method for executing commands from a Lua Script
/// </summary>
Expand Down
36 changes: 26 additions & 10 deletions libs/server/Lua/LuaRunner.Loader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,8 @@ function reset_keys_and_argv(fromKey, fromArgv)
-- force new 'global' environment to be readonly
recursively_readonly_table(sandbox_env)
-- responsible for sandboxing user provided code
function load_sandboxed(source)
local rawFunc, err = load(source, nil, nil, sandbox_env)
function load_sandboxed(source, mode)
local rawFunc, err = load(source, nil, mode, sandbox_env)

return err, rawFunc
end
Expand Down Expand Up @@ -448,7 +448,7 @@ internal static ReadOnlyMemory<byte> PrepareLoaderBlockBytes(HashSet<string> all

compilingState.Remove(1);

if (compilingState.LoadString(Encoding.UTF8.GetBytes(finalLoaderBlock)) != LuaStatus.OK)
if (compilingState.LoadTextBuffer(Encoding.UTF8.GetBytes(finalLoaderBlock)) != LuaStatus.OK)
{
throw new InvalidOperationException("Compiling function should not fail");
}
Expand Down Expand Up @@ -479,7 +479,7 @@ internal static ReadOnlyMemory<byte> PrepareLoaderBlockBytes(HashSet<string> all
///
/// These ops are faster to load into a runtime than parsing the whole source file again.
/// </summary>
internal static byte[] CompileSource(ReadOnlySpan<byte> source)
internal static bool TryCompileSource(ReadOnlySpan<byte> source, out LuaScriptChunk compiledSource, out string error)
{
// This is equivalent to calling
//
Expand All @@ -496,23 +496,39 @@ internal static byte[] CompileSource(ReadOnlySpan<byte> source)

state.Remove(1);

if (state.LoadString(source) != LuaStatus.OK)
if (state.LoadTextBuffer(source) != LuaStatus.OK)
{
// If we're going to fail, just keep the source as is - a future load attempt will fail it too
return source.ToArray();
compiledSource = default;
error = GetErrorFromStackTop(state);
return false;
}

state.PushBoolean(true);

if (state.PCall(2, 1) != LuaStatus.OK)
{
// If we're going to fail, just keep the source as is - a future load attempt will fail it too
return source.ToArray();
compiledSource = default;
error = GetErrorFromStackTop(state);
return false;
}

state.KnownStringToBuffer(1, out var ops);

return ops.ToArray();
compiledSource = new(ops.ToArray(), LuaScriptChunkKind.GarnetGeneratedBinary);
error = null;
return true;

static string GetErrorFromStackTop(LuaStateWrapper state)
{
var errorIndex = state.StackTop;
if (errorIndex >= 1 && state.Type(errorIndex) == LuaType.String)
{
state.KnownStringToBuffer(errorIndex, out var errorBuffer);
return Encoding.UTF8.GetString(errorBuffer);
}

return "cause unknown";
}
}
}
}
8 changes: 4 additions & 4 deletions libs/server/Lua/LuaRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ public void SendAndReset()

readonly LuaLoggingMode logMode;
readonly HashSet<string> allowedFunctions;
readonly ReadOnlyMemory<byte> source;
readonly LuaScriptChunk source;
readonly ScratchBufferNetworkSender scratchBufferNetworkSender;
readonly RespServerSession respServerSession;

Expand Down Expand Up @@ -208,7 +208,7 @@ public unsafe LuaRunner(
int? memLimitBytes,
LuaLoggingMode logMode,
HashSet<string> allowedFunctions,
ReadOnlyMemory<byte> source,
LuaScriptChunk source,
bool txnMode = false,
RespServerSession respServerSession = null,
ScratchBufferNetworkSender scratchBufferNetworkSender = null,
Expand Down Expand Up @@ -328,7 +328,7 @@ public unsafe LuaRunner(
throw new GarnetException("Insufficient space in Lua VM for redis version number global");
}

var loadRes = state.LoadBuffer(PrepareLoaderBlockBytes(allowedFunctions, logger).Span);
var loadRes = state.LoadBinaryBuffer(PrepareLoaderBlockBytes(allowedFunctions, logger).Span);
if (loadRes != LuaStatus.OK)
{
if (state.StackTop == 1 && state.Type(1) == LuaType.String)
Expand Down Expand Up @@ -409,7 +409,7 @@ static void Register(ref LuaStateWrapper state, ReadOnlySpan<byte> name, delegat
/// Creates a new runner with the source of the script
/// </summary>
public LuaRunner(LuaOptions options, string source, bool txnMode = false, RespServerSession respServerSession = null, ScratchBufferNetworkSender scratchBufferNetworkSender = null, string redisVersion = "0.0.0.0", ILogger logger = null)
: this(options.MemoryManagementMode, options.GetMemoryLimitBytes(), options.LogMode, options.AllowedFunctions, Encoding.UTF8.GetBytes(source), txnMode, respServerSession, scratchBufferNetworkSender, redisVersion, logger)
: this(options.MemoryManagementMode, options.GetMemoryLimitBytes(), options.LogMode, options.AllowedFunctions, new LuaScriptChunk(Encoding.UTF8.GetBytes(source), LuaScriptChunkKind.Text), txnMode, respServerSession, scratchBufferNetworkSender, redisVersion, logger)
{
}

Expand Down
Loading
Loading