This is an automated email from the ASF dual-hosted git repository.
git-hulk pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks.git
The following commit(s) were added to refs/heads/unstable by this push:
new 41ee6ae54 fix(scripting): bump LuaJIT to reject bytecode loading
(#3492)
41ee6ae54 is described below
commit 41ee6ae547941ae1261e16540ca18af25c607f23
Author: hulk <[email protected]>
AuthorDate: Mon May 18 19:47:02 2026 +0800
fix(scripting): bump LuaJIT to reject bytecode loading (#3492)
Upgrade LuaJIT to a build that disables loading binary bytecode chunks,
mitigating a DoS where crafted bytecode could crash the server. Add a
regression test that confirms `loadstring` on malformed bytecode is
refused with "attempt to load chunk with wrong mode" and the server
keeps serving requests.
---
.github/workflows/kvrocks.yaml | 3 +
cmake/luajit.cmake | 17 ++-
.../gocase/unit/scripting/luajit_bytecode_dos.lua | 120 +++++++++++++++++++++
tests/gocase/unit/scripting/scripting_test.go | 22 ++++
tests/gocase/util/flags.go | 5 +
5 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/kvrocks.yaml b/.github/workflows/kvrocks.yaml
index f50c72a19..13f8f282e 100644
--- a/.github/workflows/kvrocks.yaml
+++ b/.github/workflows/kvrocks.yaml
@@ -403,6 +403,9 @@ jobs:
cp minica.pem tests/gocase/tls/cert/ca.crt
GOCASE_RUN_ARGS="-tlsEnable"
fi
+ if [[ -n "${{ matrix.without_luajit }}" ]]; then
+ GOCASE_RUN_ARGS="$GOCASE_RUN_ARGS -luaJITEnable=false"
+ fi
./x.py test go build -parallel 2 $GOCASE_RUN_ARGS ${{
matrix.ignore_when_tsan}} ${{ matrix.ignore_when_asan}} ${{
matrix.ignore_when_ubsan}}
- name: Install redis-py
diff --git a/cmake/luajit.cmake b/cmake/luajit.cmake
index eedf29f01..83718f137 100644
--- a/cmake/luajit.cmake
+++ b/cmake/luajit.cmake
@@ -40,8 +40,8 @@ if ((${CMAKE_SYSTEM_NAME} MATCHES "Darwin") AND (NOT
CMAKE_OSX_DEPLOYMENT_TARGET
endif ()
FetchContent_DeclareGitHubWithMirror(luajit
- RocksLabs/LuaJIT c0a8e68325ec261a77bde1c8eabad398168ffe74
- MD5=7ff3e5ca4ddec59be2c2f97c5ff881d0)
+ RocksLabs/LuaJIT 02dfcc34e93e57ac96e566d123c66ee01e650299
+ MD5=f1dd7a1bbf120b5a3daee71d896f5d08)
FetchContent_GetProperties(luajit)
if (NOT lua_POPULATED)
@@ -57,14 +57,23 @@ if (NOT lua_POPULATED)
set(MACOSX_TARGET
"MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET}")
endif ()
- add_custom_target(make_luajit COMMAND ${MAKE_COMMAND} libluajit.a
${NINJA_MAKE_JOBS_FLAG}
- "CFLAGS=${LUA_CFLAGS}" ${MACOSX_TARGET}
+ add_custom_target(make_luajit
+ COMMAND ${MAKE_COMMAND} libluajit.a ${NINJA_MAKE_JOBS_FLAG}
+ "CFLAGS=${LUA_CFLAGS}" ${MACOSX_TARGET}
+ COMMAND ${CMAKE_COMMAND} -E make_directory ${luajit_BINARY_DIR}/include
+ COMMAND sh -c "cp ${luajit_SOURCE_DIR}/src/*.h
${luajit_SOURCE_DIR}/src/*.hpp ${luajit_BINARY_DIR}/include/"
WORKING_DIRECTORY ${luajit_SOURCE_DIR}/src
BYPRODUCTS ${luajit_SOURCE_DIR}/src/libluajit.a
)
file(GLOB LUA_PUBLIC_HEADERS "${luajit_SOURCE_DIR}/src/*.hpp"
"${luajit_SOURCE_DIR}/src/*.h")
file(COPY ${LUA_PUBLIC_HEADERS} DESTINATION ${luajit_BINARY_DIR}/include)
+ # luajit.h is generated by LuaJIT's Makefile from luajit_rolling.h
(host/genversion.lua
+ # substitutes the ROLLING token with a git-derived version). Seed it at
configure time
+ # so tools that parse includes without building LuaJIT (e.g. clang-tidy) can
resolve
+ # `#include "luajit.h"` pulled in via lua.hpp. The real build overwrites
this file.
+ configure_file(${luajit_SOURCE_DIR}/src/luajit_rolling.h
+ ${luajit_BINARY_DIR}/include/luajit.h COPYONLY)
endif()
add_library(luajit INTERFACE)
diff --git a/tests/gocase/unit/scripting/luajit_bytecode_dos.lua
b/tests/gocase/unit/scripting/luajit_bytecode_dos.lua
new file mode 100644
index 000000000..c799bf0ef
--- /dev/null
+++ b/tests/gocase/unit/scripting/luajit_bytecode_dos.lua
@@ -0,0 +1,120 @@
+local results = {}
+local ls = rawget(_G, "loadstring")
+if not ls then return {"ERROR:no_loadstring"} end
+
+local function find_kpri_true(bc)
+ for i = 1, #bc - 3 do
+ if bc:byte(i) == 0x2b and bc:byte(i + 1) == 0 and
+ bc:byte(i + 2) == 2 and bc:byte(i + 3) == 0 then
+ return i
+ end
+ end
+end
+
+local function find_kshort_42(bc)
+ for i = 1, #bc - 3 do
+ if bc:byte(i) == 0x29 then
+ local d = bc:byte(i + 2) + bc:byte(i + 3) * 256
+ if d == 42 then
+ return i
+ end
+ end
+ end
+end
+
+local base_fn = function() return true end
+local bc = string.dump(base_fn)
+local kpri_pos = find_kpri_true(bc)
+if not kpri_pos then
+ return {"ERROR:pattern_not_found"}
+end
+
+local crash_bc = bc:sub(1, kpri_pos - 1) ..
+ string.char(0x27, 0x00, 0x00, 0x00) ..
+ bc:sub(kpri_pos + 4)
+local fn, err = ls(crash_bc)
+if not fn then
+ return {"load_failed:" .. tostring(err)}
+end
+
+local ok, val = pcall(fn)
+if ok then
+ results[1] = "call_ok:" .. type(val) .. ":" .. tostring(val)
+ if type(val) == "string" then
+ results[2] = "leaked_string_len:" .. #val
+ local hex = {}
+ for i = 1, math.min(32, #val) do
+ hex[#hex + 1] = string.format("%02x", val:byte(i))
+ end
+ results[3] = "leaked_hex:" .. table.concat(hex, " ")
+ else
+ results[2] = "leaked_type:" .. type(val)
+ end
+else
+ results[1] = "call_error:" .. tostring(val)
+end
+
+local fn2 = function() return 42 end
+local bc2 = string.dump(fn2)
+local kshort_pos = find_kshort_42(bc2)
+if kshort_pos then
+ local crash_bc2 = bc2:sub(1, kshort_pos - 1) ..
+ string.char(0x27) ..
+ bc2:sub(kshort_pos + 1)
+ local fn2b, err2 = ls(crash_bc2)
+ if fn2b then
+ local ok2, val2 = pcall(fn2b)
+ if ok2 then
+ results[4] = "fn2_ok:" .. type(val2) .. ":" .. tostring(val2)
+ else
+ results[4] = "fn2_error:" .. tostring(val2)
+ end
+ else
+ results[4] = "fn2_load_err:" .. tostring(err2)
+ end
+else
+ results[4] = "kshort_not_found"
+end
+
+local fn3 = function() return true end
+local bc3 = string.dump(fn3)
+local kpri_pos3 = find_kpri_true(bc3)
+if kpri_pos3 then
+ local crash_bc3 = bc3:sub(1, kpri_pos3 - 1) ..
+ string.char(0x33, 0x00, 0x00, 0x00) ..
+ bc3:sub(kpri_pos3 + 4)
+ local fn3b, err3 = ls(crash_bc3)
+ if fn3b then
+ local ok3, val3 = pcall(fn3b)
+ if ok3 then
+ results[5] = "fnew_ok:" .. type(val3)
+ else
+ results[5] = "fnew_error:" .. tostring(val3)
+ end
+ else
+ results[5] = "fnew_load_err:" .. tostring(err3)
+ end
+end
+
+local fn4 = function() return 42 end
+local bc4 = string.dump(fn4)
+local pos4 = find_kshort_42(bc4)
+if pos4 then
+ local crash_bc4 = bc4:sub(1, pos4 - 1) ..
+ string.char(0xFE) ..
+ bc4:sub(pos4 + 1)
+ local fn4b, err4 = ls(crash_bc4)
+ if fn4b then
+ local ok4, val4 = pcall(fn4b)
+ if ok4 then
+ results[6] = "invalid_op_call_ok:" .. type(val4) .. ":" .. tostring(val4)
+ else
+ results[6] = "invalid_op_call_err:" .. tostring(val4)
+ end
+ else
+ results[6] = "invalid_op_load_err:" .. tostring(err4)
+ end
+end
+
+return results
+
diff --git a/tests/gocase/unit/scripting/scripting_test.go
b/tests/gocase/unit/scripting/scripting_test.go
index ce606a04f..f48492c9f 100644
--- a/tests/gocase/unit/scripting/scripting_test.go
+++ b/tests/gocase/unit/scripting/scripting_test.go
@@ -21,6 +21,7 @@ package scripting
import (
"context"
+ _ "embed"
"fmt"
"math/big"
"testing"
@@ -32,6 +33,9 @@ import (
"golang.org/x/exp/slices"
)
+//go:embed luajit_bytecode_dos.lua
+var luaJITBytecodeDoSPayload string
+
func TestScripting(t *testing.T) {
srv := util.StartServer(t, map[string]string{"resp3-enabled": "no"})
defer srv.Close()
@@ -925,3 +929,21 @@ func TestEvalScriptInStrictMode(t *testing.T) {
require.NoError(t, rdb.Eval(ctx, "return redis.call('set', 'a',
1)", []string{}).Err())
})
}
+
+func TestLuaJITBytecodeDoS(t *testing.T) {
+ if !util.LuaJITEnable() {
+ t.Skip("LuaJIT bytecode DoS test runs only when LuaJIT is
enabled.")
+ }
+
+ srv := util.StartServer(t, map[string]string{"resp3-enabled": "no"})
+ defer srv.Close()
+
+ ctx := context.Background()
+ rdb := srv.NewClient()
+ defer func() { require.NoError(t, rdb.Close()) }()
+
+ r := rdb.Eval(ctx, luaJITBytecodeDoSPayload, nil)
+ require.NoError(t, r.Err())
+ require.Equal(t, []interface{}{"load_failed:attempt to load chunk with
wrong mode"}, r.Val())
+ require.NoError(t, rdb.Ping(ctx).Err())
+}
diff --git a/tests/gocase/util/flags.go b/tests/gocase/util/flags.go
index 67321027c..bf3e850f9 100644
--- a/tests/gocase/util/flags.go
+++ b/tests/gocase/util/flags.go
@@ -26,6 +26,7 @@ var workspace = flag.String("workspace", "", "directory of
cases workspace")
var deleteOnExit = flag.Bool("deleteOnExit", false, "whether to delete
workspace on exit")
var cliPath = flag.String("cliPath", "redis-cli", "path to redis-cli")
var tlsEnable = flag.Bool("tlsEnable", false, "enable TLS-related test cases")
+var luaJITEnable = flag.Bool("luaJITEnable", true, "enable LuaJIT-specific
test cases")
func CLIPath() string {
return *cliPath
@@ -34,3 +35,7 @@ func CLIPath() string {
func TLSEnable() bool {
return *tlsEnable
}
+
+func LuaJITEnable() bool {
+ return *luaJITEnable
+}