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
+}

Reply via email to